I want the green dot to follow the touch point in a circular path, but it doesn't seem to be doing it right.
It seems like there is an unwanted offset somewhere but I can't find it on my own for quite some time.
Here is my code:
#Preview
#Composable
fun Test() {
val touchPoint = remember { mutableStateOf(Offset.Zero) }
Scaffold {
Column() {
Box(Modifier.height(100.dp).fillMaxWidth().background(Color.Blue))
Layout(
modifier = Modifier.aspectRatio(1f).fillMaxSize(),
content = {
Box(
Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.Green)
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
change.consumeAllChanges()
touchPoint.value += dragAmount
}
)
}
)
}
) { measurables, constraints ->
val dot = measurables.first().measure(constraints.copy(minHeight = 0, minWidth = 0))
val width = constraints.maxWidth
val height = constraints.maxHeight
val centerX = width / 2
val centerY = height / 2
val lengthFromCenter = width / 2 - dot.width / 2
val touchX = touchPoint.value.x
val touchY = touchPoint.value.y
layout(width, height) {
// I planned to achieve the desired behaviour with the following steps:
// 1. Convert cartesian coordinates to polar ones
val r = sqrt(touchX.pow(2) + touchY.pow(2))
val angle = atan2(touchY.toDouble(), touchX.toDouble())
// 2. Use fixed polar radius
val rFixed = lengthFromCenter
// 3. Convert it back to cartesian coordinates
val x = rFixed * cos(angle)
val y = rFixed * sin(angle)
// 4. Layout on screen
dot.place(
x = (x + centerX - dot.width / 2).roundToInt(),
y = (y + centerY - dot.height / 2).roundToInt()
)
}
}
Box(Modifier.fillMaxSize().background(Color.Blue))
}
}
}
I'm definitely missing something but don't know what exactly. What am I doing wrong?
touchPoint.value += dragAmount
Is in pixel values, and you're updating the position of the dot with pixel values, where it requires dp values. If you update that with
private fun Float.pxToDp(context: Context): Dp = // Float or Int, depends on the value you have, or Double
(this / context.resources.displayMetrics.density).dp
The amount with which it will be moved, will be smaller and reflect the dragging made by the user
You can easily achieve this by using some math:
#Composable
fun CircularView(
content: #Composable () -> Unit
) {
var middle by remember {
mutableStateOf(Offset.Zero)
}
var size by remember {
mutableStateOf(0.dp)
}
var dragAngle by remember {
mutableStateOf(0f)
}
Canvas(modifier = Modifier.size(size)) {
drawCircle(
color = Color.Red,
center = middle,
style = Stroke(1.dp.toPx())
)
}
Layout(
content = content,
modifier = Modifier.pointerInput(true) {
detectDragGestures(
onDrag = { change, _ ->
change.consumeAllChanges()
val positionOfDrag = change.position
val previousPosition = change.previousPosition
dragAngle += atan2(
positionOfDrag.x - middle.x,
positionOfDrag.y - middle.y
) - atan2(
previousPosition.x - middle.x,
previousPosition.y - middle.y
)
}
)
}
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
layout(layoutWidth, layoutHeight) {
val childCount = placeables.size
if (childCount == 0) return#layout
val middleX = layoutWidth / 2f
val middleY = layoutHeight / 2f
middle = Offset(middleX, middleY)
val angleBetween = 2 * PI / childCount
val radius =
min(
layoutWidth - (placeables.maxByOrNull { it.width }?.width ?: 0),
layoutHeight - (placeables.maxByOrNull { it.height }?.height ?: 0)
) / 2
size = (radius * 2).toDp()
placeables.forEachIndexed { index, placeable ->
val angle = index * angleBetween - PI / 2 - dragAngle
val x = middleX + (radius) * cos(angle) - placeable.width / 2f
val y = middleY + (radius) * sin(angle) - placeable.height / 2f
placeable.placeRelative(x = x.toInt(), y = y.toInt())
}
}
}
}
On the calling side:
CircularView {
repeat(10) {
Box(
modifier = Modifier
.background(
Color(
red = random.nextInt(255),
green = random.nextInt(255),
blue = random.nextInt(255)
), shape = CircleShape
)
.size(50.dp),
contentAlignment = Alignment.Center
) {
Text(text = it.toString(), fontSize = 12.sp, color = Color.White)
}
}
}
Related
Is there an options to round the corners on inside like in the picture? I have tried with CutCornerShape but it cuts them straight.
thy something like this
#Composable
private fun DrawTicketPathWithArc() {
Canvas(modifier = canvasModifier) {
val canvasWidth = size.width
val canvasHeight = size.height
// Black background
val ticketBackgroundWidth = canvasWidth * .8f
val horizontalSpace = (canvasWidth - ticketBackgroundWidth) / 2
val ticketBackgroundHeight = canvasHeight * .8f
val verticalSpace = (canvasHeight - ticketBackgroundHeight) / 2
// Get ticket path for background
val path1 = ticketPath(
topLeft = Offset(horizontalSpace, verticalSpace),
size = Size(ticketBackgroundWidth, ticketBackgroundHeight),
cornerRadius = 20.dp.toPx()
)
drawPath(path1, color = Color.Black)
// Dashed path in foreground
val ticketForegroundWidth = ticketBackgroundWidth * .95f
val horizontalSpace2 = (canvasWidth - ticketForegroundWidth) / 2
val ticketForegroundHeight = ticketBackgroundHeight * .9f
val verticalSpace2 = (canvasHeight - ticketForegroundHeight) / 2
// Get ticket path for background
val path2 = ticketPath(
topLeft = Offset(horizontalSpace2, verticalSpace2),
size = Size(ticketForegroundWidth, ticketForegroundHeight),
cornerRadius = 20.dp.toPx()
)
drawPath(
path2,
color = Color.Red,
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(20f, 20f)
)
)
)
}
}
check this page
I found a solution which looks like that
#Composable
private fun DrawDashLine() {
val pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 10f), 0f)
Canvas(modifier = Modifier.fillMaxWidth()) {
drawLine(
color = Color.Black,
strokeWidth = 3f,
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
pathEffect = pathEffect
)
drawCircle(
color = Color.Black,
center = Offset(x = 1f, y = 2f),
radius = 20f
)
drawCircle(
color = Color.Black,
center = Offset(x = size.width, y = 2f),
radius = 20f
)
}
}
The background must be black, and the front is white and the round circles just come above the white.
I'm making an isometric grid and using padding for it cells. Currently as parameters I'm waiting for its items width and height. But how can I avoid this and use final cells size in an optimal way?
I tried some solutions (commented code is the original way) from this question but it doesn't look legit for the case: I have extra multiple calculations, an error: Padding must be non-negative and it doesn't show correctly in Android Studio preview.
Modifier.onGloballyPositioned also looks same and incorrect for the case, what's the right way?
#Composable
fun <T> IsometricGrid(
gridWidth: Int,
gridHeight: Int,
cellWidth: Int,
cellHeight: Int,
data: List<T>,
itemContent: #Composable (Int, T) -> Unit
) {
var width by remember { mutableStateOf(0) }
var height by remember { mutableStateOf(0) }
for (y in 0 until gridHeight) {
for (x in 0 until gridWidth) {
// val start = (y % 2 * 0.5 + x) * cellWidth
// val top = y * cellHeight * 0.5
val index = x * gridHeight + y
Box(
modifier = Modifier
.onSizeChanged {
width = it.width
height = it.height
Timber.i("$width, $height")
}
// .padding(start = start.dp, top = top.dp)
.padding(start = ((y % 2 * 0.5 + x) * cellWidth).dp, top = (y * height * 0.5).dp)
) {
itemContent(index, data[index])
}
}
}
}
Added usage:
IsometricGrid(4, 4, 100, 50, listOf<Foo>(...)) { index: Int, foo: Foo ->
Icon(...)
}
start = (y % 2 * 0.5 + x * width).dp, top = (y * height * 0.5).dp
This line is not correct because you add dp extension to pixel instead of converting pixel to dp
What you should be doing
val density = LocalDensity.current
density.run{(y % 2 * 0.5 + x * width).toDp()}
because dp value of any pixel values is calculated as
dpValue = valueInPixel/density
let's say you have 100px with a density = 2.0f
your dp value should be 50.dp. If you calculate it as in your question you find that it returns 100.dp
Try this. I don't have data, so i'm not able to try your function.
#Composable
fun <T> IsometricGrid(
gridWidth: Int,
gridHeight: Int,
cellWidth: Int,
cellHeight: Int,
data: List<T>,
itemContent: #Composable (Int, T) -> Unit
) {
val density = LocalDensity.current
var width by remember { mutableStateOf(0) }
var height by remember { mutableStateOf(0) }
for (y in 0 until gridHeight) {
for (x in 0 until gridWidth) {
// val start = (y % 2 * 0.5 + x) * cellWidth
// val top = y * cellHeight * 0.5
val index = x * gridHeight + y
val startInDp = density.run { ((y % 2 * 0.5f + x) * cellWidth).toDp() }
val topInDp = density.run { (y * height * 0.5f).toDp() }
Box(
modifier = Modifier
.onSizeChanged {
width = it.width
height = it.height
}
// .padding(start = start.dp, top = top.dp)
.padding(
start = startInDp,
top = topInDp
)
) {
itemContent(index, data[index])
}
}
}
}
toDp() is extension for Float in Density interface
/** Convert a [Float] pixel value to a Dp */
#Stable
fun Float.toDp(): Dp = (this / density).dp
I have a layout with two texts, one on left side and one on right side. If both texts are long, left one should occupy 60%, and right one 40% of the width. But if right text is shorter than 40%, the left one should take all the available space.
Here are the examples:
and:
So I would like to write something like this:
Row {
Text(text = left, modifier = modifier.padding(8.dp).weight(<min 0.6f>))
Text(text = right, modifier = modifier.padding(8.dp).weight(<max 0.4f>))
}
Is there any way to achieve this?
Finally, I figured it out. Here is the modifier:
fun Modifier.maxWidth(
fraction: Float = 1f,
) = layout { measurable, constraints ->
val maxWidth = (constraints.maxWidth * fraction).roundToInt()
val width = measurable.maxIntrinsicWidth(constraints.maxHeight).coerceAtMost(maxWidth)
val placeable = measurable.measure(Constraints(constraints.minWidth, width, constraints.minHeight, constraints.maxHeight))
layout(width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
So we can use it like this:
Row {
Text(text = left, modifier = modifier.padding(8.dp).weight(1f)) // left text uses all the available space
Text(text = right, modifier = modifier.padding(8.dp).maxWidth(fraction = 0.4f)) // right text can be 40% of the parent or less
}
Measure the length of each string and calculate their weights. Adjust the weights based on the maximum allowed for the right column:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
val paint = android.graphics.Paint()
// First row
val text1 = "This long text should take up maximum 60% of the space with wrapping"
val text2 = "This is not very long"
val width1 = paint.measureText(text1)
val width2 = paint.measureText(text2)
var w2 = width2 / (width1 + width2)
if (w2 > 0.4f) {
w2 = 0.4f
}
val w1 = 1f - w2
// Second row
val text3 = "This text should take up more than 60% of the space"
val text4 = "Short"
val width3 = paint.measureText(text3)
val width4 = paint.measureText(text4)
var w4 = width4 / (width3 + width4)
if (w4 > 0.4f) {
w4 = 0.4f
}
val w3 = 1f - w4
setContent {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 20.dp)
) {
Text(text1, modifier = Modifier.weight(w1))
Text(text2, modifier = Modifier.weight(w2), color = Color.Red)
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 20.dp)
) {
Text(text3, modifier = Modifier.weight(w3))
Text(text4, modifier = Modifier.weight(w4), color = Color.Red)
}
}
}
}
}
I'm using XamForms.Controls.Calendar in my Xamarin.Forms project everything works great, but the texts entered in the "Text" section of the SpeacialDates property appear blurred on the iOS platform, how can I solve it?
DrawText feature ios also have the same way, despite the intense research of this feature could not solve the blur problem
https://imgur.com/a/E0UZnD9 "Screenshot"
protected void DrawText(CGContext g, Pattern p, CGRect r)
{
if (string.IsNullOrEmpty(p.Text)) return;//Copperplate-Light" KohinoorTelugu-Light
string fontname = Device.Idiom == TargetIdiom.Tablet ? "KohinoorTelugu-Light" : "GillSans-UltraBold";//GillSans-UltraBold
var bounds = p.Text.StringSize(UIFont.FromName(fontname, p.TextSize));
//"GillSans-UltraBold
var al = (int)p.TextAlign;
var x = r.X;
if ((al & 2) == 2) // center
{
x = r.X + (int)Math.Round(r.Width / 2.0) - (int)Math.Round(bounds.Width / 2.0);
}
else if ((al & 4) == 4) // right
{
x = (r.X + r.Width) - bounds.Width - 2;
}
var y = r.Y + (int)Math.Round(bounds.Height / 2.0) + 2;
if ((al & 16) == 16) // middle
{
y = r.Y + (int)Math.Ceiling(r.Height / 2.0) + (int)Math.Round(bounds.Height / 5.0);
}
else if ((al & 32) == 32) // bottom
{
y = (r.Y + r.Height) - 2;
}
g.SaveState();
g.SetShouldAntialias(true);
g.SetShouldSmoothFonts(true);
g.SetAllowsFontSmoothing(true);
g.SetAllowsAntialiasing(true);
g.SetShouldSubpixelPositionFonts(false);
g.InterpolationQuality = CGInterpolationQuality.High;
g.SetAllowsSubpixelPositioning(false);
g.SetAllowsFontSubpixelQuantization(false);
g.InterpolationQuality = CGInterpolationQuality.High;
g.TranslateCTM(0, Bounds.Height);
g.ScaleCTM(1, -1);
g.SetStrokeColor(p.TextColor.ToCGColor());
g.DrawPath(CGPathDrawingMode.EOFillStroke);
g.SetFillColor(p.TextColor.ToCGColor());
g.SetTextDrawingMode(CGTextDrawingMode.FillStrokeClip);
g.SelectFont(fontname, p.TextSize, CGTextEncoding.MacRoman);
g.ShowTextAtPoint(x, Bounds.Height - y, p.Text);
g.RestoreState();
}
}
When using UIPinchGestureRecognizer what is the best way to detect/read the pinch scale in horizontal and vertical directions individually? I saw this post
UIPinchGestureRecognizer Scale view in different x and y directions
but I noticed there were so many going back and forth for such a seemingly routine task that I am not sure that is the best answer/way.
If not using UIPinchGestureRecognizer altogether for this purpose is the answer, what's the best way to detect pinch scale in two different directions?
Basically do this,
func _mode(_ sender: UIPinchGestureRecognizer)->String {
// very important:
if sender.numberOfTouches < 2 {
print("avoided an obscure crash!!")
return ""
}
let A = sender.location(ofTouch: 0, in: self.view)
let B = sender.location(ofTouch: 1, in: self.view)
let xD = fabs( A.x - B.x )
let yD = fabs( A.y - B.y )
if (xD == 0) { return "V" }
if (yD == 0) { return "H" }
let ratio = xD / yD
// print(ratio)
if (ratio > 2) { return "H" }
if (ratio < 0.5) { return "V" }
return "D"
}
That function will return H, V, D for you .. horizontal, vertical, diagonal.
You would use it something like this ...
func yourSelector(_ sender: UIPinchGestureRecognizer) {
// your usual code such as ..
// if sender.state == .ended { return } .. etc
let mode = _mode(sender)
print("the mode is \(mode) !!!")
// in this example, we only care about horizontal pinches...
if mode != "H" { return }
let vel = sender.velocity
if vel < 0 {
print("you're squeezing the screen!")
}
}
In my C# I do the following
private double _firstDistance = 0;
private int _firstScaling = 0;
private void PinchHandler(UIPinchGestureRecognizer pinchRecognizer)
{
nfloat x1, y1, x2, y2 = 0;
var t1 = pinchRecognizer.LocationOfTouch(0, _previewView);
x1 = t1.X;
y1 = t1.Y;
var t2 = pinchRecognizer.LocationOfTouch(1, _previewView);
x2 = t2.X;
y2 = t2.Y;
if (pinchRecognizer.State == UIGestureRecognizerState.Began)
{
_firstDistance = Math.Sqrt(Math.Pow((x2 - x1), 2) + Math.Pow((y2 - y1), 2));
_firstScaling = _task.TextTemplates[_selectedTextTemplate].FontScaling;
}
if (pinchRecognizer.State == UIGestureRecognizerState.Changed)
{
var distance = Math.Sqrt(Math.Pow((x2 - x1), 2) + Math.Pow((y2 - y1), 2));
var fontScaling = Convert.ToInt32((distance - _firstDistance) / _previewView.Frame.Height * 100);
fontScaling += _firstScaling;
_task.TextTemplates[_selectedTextTemplate].FontScaling = fontScaling;
UpdateBitmapPreview();
}
}
I calculate the distance between the two points when pinch "began" and hold that value in two privates. Then I calculate a scaling (fontScaling) based on the first measured distance and the second one (in "changed").
I use my own view (_previewView) to set as base (100%), but you can use View.Bounds.height instead or the width for that matter. in my case, I always have a square view, so height == width in my app.