I created an interactive line, but that might be irrelevant. Even if there was no interaction, this renders unexpected results:-
#Composable
fun PowerClock() {
var dynamicAngle by remember { mutableStateOf(90f.toRadians()) }
val angle by animateFloatAsState(targetValue = dynamicAngle)
Canvas(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.pointerInput(Unit) { //Irrelevent, the results go wrong even without invoking this at all
coroutineScope {
while (true) {
// val touchDownPointerId = awaitPointerEventScope { awaitFirstDown().id }
detectDragGestures { _, dragAmount ->
dynamicAngle += atan(dragAmount.x / dragAmount.y)
}
}
}
}
) {
val length = 500
val path = Path().apply {
moveTo(size.width / 2, size.height / 2)
relativeLineTo(length * cos(angle), length * sin(angle))
}
drawPath(path, Color.Blue, style = Stroke(10f))
}
}
Here's a bit of a preview,
An intriguing behaviour portrayed by Cavnas is that looking at my implementation, the angle should change based on both the x and y change, right? But in actuality, y is out ignored. I have tested this.
Is this a bug in Cavnas or am I implementing something wrong?
I've followed this answer and adopted code to Compose:
var touchPosition by remember { mutableStateOf(Offset.Zero) }
Canvas(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.pointerInput(Unit) { //Irrelevent, the results go wrong even without invoking this at all
while (true) {
detectDragGestures { change, _ ->
touchPosition = change.position
}
}
}
) {
val rect = Rect(Offset.Zero, size)
val length = 500
val path = Path().apply {
moveTo(rect.center.x, rect.center.y)
val angle = (touchPosition - rect.center).let { atan2(it.y, it.x) }
relativeLineTo(length * cos(angle), length * sin(angle))
}
drawPath(path, Color.Blue, style = Stroke(10f))
}
Result:
Related
I have a toggle issue with Switch that can look as following:
In the picture you can see only two of many possible states.
I have complex business logics in ViewModel that updates my whole screen state after clicking on switch.
However, in order to make it easier and demonstrate you the problem I found a simple example that is similar to my real-life scenario.
#Composable
fun MyCoolWidget() {
var isChecked by remember { mutableStateOf(true) }
val scope = rememberCoroutineScope()
Switch(
checked = isChecked,
onCheckedChange = {
scope.launch {
delay(50) // to mimic the business logic and state update delay
// I use the 50 millis delay and then update the state
isChecked = it
}
},
)
}
Now you can test it by putting your finger to the one edge, holding the finger on the screen and moving it to the opposite edge. (Don't click on switch, SWIPE it!)
Observe the result.
How can I fix this problem?
Dependencies:
androidx.compose.material:material:1.1.1. Jetpack Compose version - 1.2.0-rc01. Kotlin version - 1.6.21
Thanks, best wishes!
It looks like a bug.
I would code my custom switch if i were you because you have to disable swipe feature.
Here is my custom ios like switch :
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MySwitch(
modifier: Modifier = Modifier,
isChecked: Int,
onCheckedChange: (checked: Int) -> Unit
) {
var size by remember {
mutableStateOf(IntSize.Zero)
}
val marbleSize by remember(size) {
mutableStateOf(size.height.div(2))
}
val yOffset by remember(size, marbleSize) {
mutableStateOf((size.height.div(2) - marbleSize.div(2f)).toInt())
}
val marblePadding = 4.dp.value
val scope = rememberCoroutineScope()
val swipeableState = rememberSwipeableState(isChecked)
val backgroundColor = animateColorAsState(
targetValue = if (swipeableState.currentValue != 0) Color(0xFF34C759) else Color(0xD6787880)
)
val sizePx = size.width.minus(marbleSize + marblePadding.times(2))
val anchors = mapOf(0f to 0, sizePx - 1f to 1)
LaunchedEffect(key1 = swipeableState.currentValue, block = {
onCheckedChange.invoke(swipeableState.currentValue)
})
Box(
modifier = modifier
.aspectRatio(2f)
.clip(CircleShape)
.swipeable(
state = swipeableState,
anchors = anchors,
enabled = false, //because you need to disable swipe
orientation = Orientation.Horizontal
)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (it.x > size.width.div(2))
scope.launch {
swipeableState.animateTo(
1,
anim = tween(250, easing = LinearEasing)
)
}
else
scope.launch {
swipeableState.animateTo(
0,
anim = tween(250, easing = LinearEasing)
)
}
}
)
}
.background(backgroundColor.value)
.onSizeChanged {
size = it
}
) {
Box(
modifier = Modifier
.padding(horizontal = marblePadding.dp)
.offset {
IntOffset(
x = swipeableState.offset.value.roundToInt(),
y = yOffset
)
}
.size(with(LocalDensity.current) { marbleSize.toDp() })
.clip(CircleShape)
.background(Color.Red)
)
}
}
I hope it helps you.
I'm trying to animate the movement of multiple circles on canvas at once. So far I managed to animate one, that moves to a random spot on canvas on every user click using Animatable. Now I want to add another 2 circles that do the same but move to another, also randomly chosen spot. Is there a way to achieve it easily without launching multiple coroutines?
My code so far:
#Composable
fun CanvasScreen(){
val animationScope = rememberCoroutineScope()
val animationX = remember{Animatable(0f)}
val animationY = remember{Animatable(0f)}
val randomColor = Color((Math.random() * 16777215).toInt() or (0xFF shl 24))
Canvas(
modifier = Modifier
.fillMaxSize()
.clickable {
animationScope.launch {
launch {
animationX.animateTo(
targetValue = (90..1000)
.random()
.toFloat()
)
}
launch {
animationY.animateTo(
targetValue = (90..1500)
.random()
.toFloat()
)
}
}
}
){
drawCircle(
color = randomColor,
radius = 90f,
center = Offset(animationX.value, animationY.value),
)
}
}
it took me a while but i finally got it done, there may be a simpler way of course but this can work.
#Composable
fun CanvasScreen(){
val circleNumber = 10
val animationScope = rememberCoroutineScope()
val randomColorList = remember { arrayListOf<Color>()}
val animationXList = remember { arrayListOf<Animatable<Float, AnimationVector1D>>() }
val animationYList = remember { arrayListOf<Animatable<Float, AnimationVector1D>>() }
for(i in 0 until circleNumber){
randomColorList.add(Color((Math.random() * 16777215).toInt() or (0xFF shl 24)))
animationXList.add(Animatable(0f))
animationYList.add(Animatable(0f))
}
Canvas(
modifier = Modifier
.fillMaxSize()
.clickable {
animationXList.forEach {
animationScope.launch {
launch {
it.animateTo(
targetValue = (90..1000)
.random()
.toFloat()
)
}
}
}
animationYList.forEach {
animationScope.launch {
launch {
it.animateTo(
targetValue = (90..1500)
.random()
.toFloat()
)
}
}
}
}
){
for(i in 0 until circleNumber) {
drawCircle(
color = randomColorList[i],
radius = 90f,
center = Offset(animationXList[i].value, animationYList[i].value),
)
}
}
}
How to implement a progress bar like animation to the second rectangle, as well as control the animation such as start & pause
#Composable
fun ProgressBar(modifier: Modifier = Modifier.size(300.dp, 180.dp)) {
Canvas(modifier = modifier.fillMaxSize().padding(16.dp)) {
val canvasWidth = size.width
val canvasHeight = size.height
val canvasSize = size
val percentage = 0.2F
drawRoundRect(
color = Pink80,
topLeft = Offset(x = 0F, y = 0F),
size = Size(canvasSize.width, 55F),
cornerRadius = CornerRadius(60F, 60F),
)
drawRoundRect(
color = Purple40,
topLeft = Offset(x = 0F, y = 0F),
size = Size(canvasWidth * 0.2F , 55F),
cornerRadius = CornerRadius(60F, 60F),
)
}
}
Here are two solutions. The first one uses Compose's own LinearProgressIndicator. The second one is a custom built one. It is very simple and has the advantage that you can easily customize it.
No need to use a canvas. When the progress bar reaches it's maximum, the code resets its value back to zero, but you can leave it set to 100% by not resetting the value. The LaunchedEffect is only being used to simulate updating the value. In a production app, you wouldn't use this but rather update the value with some other means that aligns with your code's business logic...
LinearProgressIndicator:
See: LinearProgressIndicator
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var enabled by remember { mutableStateOf(false) }
var progress by remember { mutableStateOf(0.1f) }
val animatedProgress by animateFloatAsState(
targetValue = progress,
)
LaunchedEffect(enabled) {
while ((progress < 1) && enabled) {
progress += 0.005f
delay(10)
}
}
if (progress >= 1f) {
enabled = false
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(progress = animatedProgress, modifier = Modifier.requiredHeight(20.dp))
Spacer(Modifier.requiredHeight(30.dp))
Button(
onClick = {
enabled = !enabled
}
) {
if (enabled) {
Text("Pause")
} else {
Text("Start")
}
}
}
}
}
}
Custom Progress Indicator:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var enabled by remember { mutableStateOf(false) }.apply { this.value }
var progressValue by remember { mutableStateOf(0) }
LaunchedEffect(enabled) {
while ((progressValue < 100) && enabled) {
progressValue++
delay(10)
}
if (progressValue == 100) {
enabled = false
progressValue = 0
}
}
Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
ProgressBar(value = progressValue)
Button(
onClick = {
enabled = !enabled
}
) {
if (enabled) {
Text("Pause")
} else {
Text("Start")
}
}
}
}
}
}
#Composable
fun ProgressBar(
value: Int
) {
Box(
modifier = Modifier
.fillMaxWidth()
.requiredHeight(20.dp)
.border(width = 1.dp, color = Color.Black)
) {
Box(
modifier = Modifier
.fillMaxWidth(value.toFloat() / 100f)
.fillMaxHeight()
.background(color = Color.Red)
) {
}
}
}
https://github.com/DogusTeknoloji/compose-progress
You can check this library as support animated and also step progress. The library base on canvas
I want to animate a composable based on a fixed value and the $width of the composable.
How can I get the $width to use it for the animate function?
This is my code
#Composable
fun ExpandingCircle() {
val (checked, setChecked) = remember { mutableStateOf(false) }
val radius = if (checked) **$width** else 4.dp
val radiusAnimated = animate(radius)
Canvas(
modifier = Modifier.fillMaxSize()
.clickable(onClick = { setChecked(!checked) }),
onDraw = {
drawCircle(color = Color.Black, radius = radiusAnimated.toPx())
}
)
}
We can get the size from DrawScope, from the size we can get the width and height of the Canvas, So you can do animation like this.
#Composable
fun ExpandingCircle() {
val (checked, setChecked) = remember { mutableStateOf(false) }
val unCheckedRadius = 4.dp
Canvas(
modifier = Modifier.fillMaxSize()
.clickable(onClick = { setChecked(!checked) }),
onDraw = {
val width = size.width
drawCircle(color = Color.Black, radius = if (checked) width/2 else unCheckedRadius.toPx())
}
)
}
I realized I don't need the width already for animate, but i can just use a animated / interpolating float to use it for the calculation in the DrawScope
#Composable
fun ExpandingCircle() {
val (checked, setChecked) = remember { mutableStateOf(false) }
val radiusExpandFactor = if (checked) 1f else 0f
val radiusExpandFactorAnimated = animate(radiusExpandFactor)
Canvas(
modifier = Modifier.fillMaxSize()
.clickable(onClick = { setChecked(!checked) }),
onDraw = {
val radius = 4.dp.toPx() + (radiusExpandFactorAnimated * (size.width / 2 - 4.dp.toPx()))
drawCircle(color = Color.Black, radius = radius)
}
)
}
I want to detect a gesture in #Composable, which will allow me to drag an element across the screen in any direction.
I tried using LongPressDragObserver but after dragging for a bit, it snaps to a single Orientation (either Horizontally or Vertically) and Offset doesn't change for the other Orientation at all (it will equal to 0 all the time)
Example functionality I want to achieve:
Long press on the FAB and drag it around the screen so that it's position is constantly under user's finger.
I'm using Compose 1.0.0-alpha04
Example code which drags in only one direction (thanks to Rafsanjani)
.dragGestureFilter(dragObserver = object : DragObserver {
override fun onDrag(dragDistance: Offset): Offset {
val newX = dragDistance.x + verticalOffset.value
val newY = dragDistance.y + horizontalOffset.value
verticalOffset.value = newX
horizontalOffset.value = newY
return dragDistance
}
})
You can use Modifier.pointerInput with detectDragGestures to do exactly the same as you want.
Example:
#Composable
fun Drag2DGestures() {
var size by remember { mutableStateOf(400.dp) }
val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
Box(modifier = Modifier.size(size)){
Box(
Modifier
.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX.value = (offsetX.value + dragAmount.x)
.coerceIn(0f, size.width.toFloat() - 50.dp.toPx())
offsetY.value = (offsetY.value + dragAmount.y)
.coerceIn(0f, size.height.toFloat() - 50.dp.toPx())
}
}
)
Text("Drag the box around", Modifier.align(Alignment.Center))
}
}
will produce this result:
ٍٍٍSorry for the jank/drop in frames, the built-in emulator recorder cannot record 60fps smoothly
Compose version: alpha-11
Here's how to make any component draggable:
#Composable
fun DraggableComponent(content: #Composable () -> Unit) {
val offset = remember { mutableStateOf(IntOffset.Zero) }
Box(
content = { content() },
modifier = Modifier
.offset { offset.value }
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
val offsetChange = IntOffset(dragAmount.x.roundToInt(), dragAmount.y.roundToInt())
offset.value = offset.value.plus(offsetChange)
}
}
)
}
Use it like this:
DraggableComponent {
FloatingActionButton(
...
)
}
More info: https://developer.android.com/jetpack/compose/gestures#dragging