How to disable detectTransfromGestures() on horizontall scroll? - android

I have the HorizontalPager with images. If the image has detectTransformGestures, the HorizontalPager will not scroll. If it's not there, then everything works. How can detectTransformGestures be disabled when scrolling horizontally.
var size by remember { mutableStateOf(Size.Zero) }
var scale by remember { mutableStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
HorizontalPager(
count = postPictures.size,
modifier = Modifier
.background(Color.Black.copy(alpha = 0.7f))
.fillMaxSize()
.onSizeChanged { size = it.toSize() }
) { image ->
GlideImage(
previewPlaceholder = 0,
imageOptions = ImageOptions(
contentScale = ContentScale.FillWidth
),
requestOptions = { RequestOptions.skipMemoryCacheOf(false) },
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { _, pan, zoom, _ ->
val newScale: Float = (scale * zoom).coerceIn(1f, 4f)
val newOffset = offset + pan
scale = newScale
val maxX = (size.width * (scale - 1) / 2f)
val maxY = (size.height * (scale - 1) / 2f)
offset = Offset(
newOffset.x.coerceIn(-maxX, maxX),
newOffset.y.coerceIn(-maxY, maxY)
)
}
)
}
}
.graphicsLayer {
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
},
imageModel = postPictures[image],
)

You need to write you own detectTransformGestures that selectively calls PointerInputChange.consume to prevent scrolling. I wrote a library that extends default gestures that returns PointerInputChange that you can consume based on your preferences. It's here
Modifier.pointerInput(Unit) {
detectTransformGestures(
onGestureStart = {
},
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
gestureRotate: Float,
mainPointerInputChange: PointerInputChange,
pointerList: List<PointerInputChange> ->
},
onGestureEnd = {
}
)
}
calling mainPointerInputChange.consume will prevent scrolling and you can call consume based on rotation, zoom or anything you prefer.
If you wish to have a zoom modifier that already does this with other options such as fling and moving back to bounds or limiting pan as in your code you can check out this library.
fun Modifier.enhancedZoom(
key: Any? = Unit,
clip: Boolean = true,
enhancedZoomState: EnhancedZoomState,
enabled: (Float, Offset, Float) -> Boolean = DefaultEnabled,
zoomOnDoubleTap: (ZoomLevel) -> Float = enhancedZoomState.DefaultOnDoubleTap,
onGestureStart: ((EnhancedZoomData) -> Unit)? = null,
onGesture: ((EnhancedZoomData) -> Unit)? = null,
onGestureEnd: ((EnhancedZoomData) -> Unit)? = null,
)
enabled lambda returns zoom, pan and rotation info to selectively enable or disable scrolling of other Composables.
Usage
Image(
modifier = Modifier
.background(Color.LightGray)
.border(2.dp, Color.Red)
.fillMaxWidth()
.aspectRatio(4 / 3f)
.enhancedZoom(
clip = true,
enhancedZoomState = rememberEnhancedZoomState(
minZoom = .5f,
imageSize = IntSize(width, height),
limitPan = true,
moveToBounds = true
),
enabled = { zoom, pan, rotation ->
(zoom > 1f)
}
),
bitmap = imageBitmap,
contentDescription = "",
contentScale = ContentScale.FillBounds
)

Related

Squirecle Image was not shown on Android 10 devices (Compose)

I am trying to draw Squircle Image using the Android Jetpack Compose.
I found simple code set on Github: https://github.com/Size0f/android.compose.squircle/blob/master/squircle/src/main/java/com/sizeof/libraries/compose/squircle/SquircleShape.kt
Here is SquuircleShape compose:
class SquircleShape : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
) = Outline.Generic(
path = createSquirclePath(size, SMOOTHING)
)
private fun createSquirclePath(size: Size, smoothing: Double): androidx.compose.ui.graphics.Path {
return Path().apply {
val oversize = size.width * OVERSAMPLING_MULTIPLIER
val squircleRadius = (oversize / 2F).toInt()
// power radius before for optimization
val poweredRadius = squircleRadius
.toDouble()
.pow(smoothing)
// generate Y coordinates for path
val yCoordinates = (-squircleRadius..squircleRadius).map { x ->
x.toFloat() to evalSquircleFun(x, poweredRadius, smoothing)
}
// generate Y coordinates for mirror half of squircle shape
val yMirroredCoordinates = yCoordinates.map { (x, y) -> Pair(x, -y) }
var currentX = 0F
var currentY = 0F
// set path by using quadraticBezier
(yCoordinates + yMirroredCoordinates).forEach { (x, y) ->
quadTo(currentX, currentY, x, y)
currentX = x
currentY = y
}
close()
// scale down to original size - for better corners without anti-alias
transform(
scaleMatrix(
sx = 1 / OVERSAMPLING_MULTIPLIER,
sy = 1 / OVERSAMPLING_MULTIPLIER
)
)
// translate path to center
transform(
translationMatrix(
tx = size.width / 2,
ty = size.height / 2
)
)
}.asComposePath()
}
// squircle formula: | (r^smoothing) - |x|^5 | ^ (1 / smoothing)
private fun evalSquircleFun(x: Int, poweredRadius: Double, smoothing: Double) =
(poweredRadius - abs(x.toDouble().pow(smoothing))).pow(1 / smoothing).toFloat()
companion object {
private const val SMOOTHING = 3.0
private const val OVERSAMPLING_MULTIPLIER = 4F
}
}
Here is SquircleImage compose:
#Composable
fun SquircleImage(
modifier: Modifier = Modifier,
imageRequest: ImageRequest,
size: Dp,
backgroundColor: Color = Gray10,
borderColor: Color = Gray20,
borderSize: Dp = 0.5.dp,
radius: Dp = 24.dp
) {
Box(
modifier = modifier
.size(size)
.clip(SquircleShape())
.background(borderColor),
contentAlignment = Alignment.Center
) {
Image(
painter = rememberAsyncImagePainter(
model = imageRequest,
contentScale = ContentScale.Crop
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(SquircleShape())
.background(backgroundColor)
.size(size - borderSize - borderSize),
)
}
}
In general, it works fine like:
But if I navigate other pages again and again repeatedly, the Squircle images are gone!:
There is no exception or meaningful logs.
It doesn't happen always.
Occurrence frequency maybe 1/10?
And It occurs on Android 10.
Somebody help me, please?

Offset a wide image for horizontal parallax effect in Android Compose

I am trying to create a parallax effect with a wide image lets say:
https://placekitten.com/2000/400
On top of it i show a LazyRow with items. Whilst the user goes through those i would like to offset the image so that it 'moves along' slowly with the items.
The image should basically FillHeight and align to the Start so that it can move left to right.
The calculation part of the offset is done and works as it should. So does overlaying the lazy row. Now displaying the image properly is where i struggle.
I tried variations of this:
Image(
modifier = Modifier
.height(BG_IMAGE_HEIGHT)
.graphicsLayer {
translationX = -parallaxOffset
},
painter = painter,
contentDescription = "",
alignment = Alignment.CenterStart,
contentScale = ContentScale.FillHeight
)
Unfortunately though the rendered image is chopped off at the end of the initially visible portion so when the image moves there is just empty space coming up.
DEMO
As you can see while going through the list white space appears on the right instead of the remaining image.
How do i do this properly?
Image is too smart and doesn't draw anything beyond the bounds. translationX doesn't change the bound but only moves the view.
Here's how you can draw it manually:
val painter = painterResource(id = R.drawable.my_image_1)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(BG_IMAGE_HEIGHT)
) {
translate(
left = -parallaxOffset,
) {
with(painter) {
draw(Size(width = painter.intrinsicSize.aspectRatio * size.height, height = size.height))
}
}
}
I don't see your code that calculates parallaxOffset, but just in case, I suggest you watch this video to get the best performance.
You can do it by drawing image to Canvas and setting srcOffset to set which section of the image should be drawn and dstOffset to where it should be drawn in canvas of drawImage function
#Composable
private fun MyComposable() {
Column {
var parallaxOffset by remember { mutableStateOf(0f) }
Spacer(modifier = Modifier.height(100.dp))
Slider(
value = parallaxOffset, onValueChange = {
parallaxOffset = it
},
valueRange = 0f..1500f
)
val imageBitmap = ImageBitmap.imageResource(id = R.drawable.kitty)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.border(2.dp, Color.Red)
) {
val canvasWidth = size.width.toInt()
val canvasHeight = size.height.toInt()
val imageHeight = imageBitmap.height
val imageWidth = imageBitmap.width
drawImage(
image = imageBitmap,
srcOffset = IntOffset(
parallaxOffset.toInt().coerceAtMost(kotlin.math.abs(canvasWidth - imageWidth)),
0
),
dstOffset = IntOffset(0, kotlin.math.abs(imageHeight - canvasHeight) /2)
)
}
}
}
Result
I'm leaving my solution here...
#Composable
private fun ListBg(
firstVisibleIndex: Int,
totalVisibleItems: Int,
firstVisibleItemOffset: Int,
itemsCount: Int,
itemWidth: Dp,
maxWidth: Dp
) {
val density = LocalDensity.current
val firstItemOffsetDp = with(density) { firstVisibleItemOffset.toDp() }
val hasNoScroll = itemsCount <= totalVisibleItems
val totalWidth = if (hasNoScroll) maxWidth else maxWidth * 2
val scrollableBgWidth = if (hasNoScroll) maxWidth else totalWidth - maxWidth
val scrollStep = scrollableBgWidth / itemsCount
val firstVisibleScrollPercentage = firstItemOffsetDp.value / itemWidth.value
val xOffset =
if (hasNoScroll) 0.dp else -(scrollStep * firstVisibleIndex) - (scrollStep * firstVisibleScrollPercentage)
Box(
Modifier
.wrapContentWidth(unbounded = true, align = Alignment.Start)
.offset { IntOffset(x = xOffset.roundToPx(), y = 0) }
) {
Image(
painter = rememberAsyncImagePainter(
model = "https://placekitten.com/2000/400",
contentScale = ContentScale.FillWidth,
),
contentDescription = null,
alignment = Alignment.TopCenter,
modifier = Modifier
.height(232.dp)
.width(totalWidth)
)
}
}
#Composable
fun ListWithParallaxImageScreen() {
val lazyListState = rememberLazyListState()
val firstVisibleIndex by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex
}
}
val totalVisibleItems by remember {
derivedStateOf {
lazyListState.layoutInfo.visibleItemsInfo.size
}
}
val firstVisibleItemOffset by remember {
derivedStateOf {
lazyListState.firstVisibleItemScrollOffset
}
}
val itemsCount = 10
val itemWidth = 300.dp
val itemPadding = 16.dp
BoxWithConstraints(Modifier.fillMaxSize()) {
ListBg(
firstVisibleIndex,
totalVisibleItems,
firstVisibleItemOffset,
itemsCount,
itemWidth + (itemPadding * 2),
maxWidth
)
LazyRow(state = lazyListState, modifier = Modifier.fillMaxSize()) {
items(itemsCount) {
Card(
backgroundColor = Color.LightGray.copy(alpha = .5f),
modifier = Modifier
.padding(itemPadding)
.width(itemWidth)
.height(200.dp)
) {
Text(
text = "Item $it",
Modifier
.padding(horizontal = 16.dp, vertical = 6.dp)
)
}
}
}
}
}
Here is the result:

Drag Image to the bounds of the Box - Jetpack Compose

I'm trying to create a Zoomable Image in Jetpack Compose. I've enabled zoom in/out functionality, but I'm not sure how should I set the limit on translationX property so that I cannot move the image horizontally outside of the Box bounds? Any solutions?
Example:
#Composable
fun ZoomableImage(
painter: Painter
) {
val scale = remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.welcomeScreenBackgroundColor)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
scale.value *= zoom
}
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.pointerInput(Unit) {
detectHorizontalDragGestures { change, dragAmount ->
offsetX += dragAmount
}
}
.graphicsLayer(
translationX = offsetX,
scaleX = maxOf(1f, minOf(3f, scale.value)),
scaleY = maxOf(1f, minOf(3f, scale.value))
),
contentDescription = "Image",
painter = painter,
contentScale = ContentScale.Fit
)
}
}
I'm not sure if this is a good idea, but you could use onPlace.
And a WIP example:
#OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
#Composable
fun ZoomableImage(
painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,
onLongPress: ((Offset) -> Unit)? = null,
onTap: ((Offset) -> Unit)? = null
) {
val scope = rememberCoroutineScope()
var layout: LayoutCoordinates? = null
var scale by remember { mutableStateOf(1f) }
var translation by remember { mutableStateOf(Offset.Zero) }
val transformableState = rememberTransformableState { zoomChange, panChange, _ ->
scale *= zoomChange
translation += panChange.times(scale)
}
Box(
modifier = modifier
.clipToBounds()
.transformable(state = transformableState)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = onLongPress,
onDoubleTap = {
val maxScale = 2f
val midScale = 1.5f
val minScale = 1f
val targetScale = when {
scale >= minScale -> midScale
scale >= midScale -> maxScale
scale >= maxScale -> minScale
else -> minScale
}
scope.launch {
transformableState.animateZoomBy(targetScale / scale)
}
},
onTap = onTap
)
}
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
drag(down.id) {
if (layout == null) return#drag
val maxX = layout!!.size.width * (scale - 1) / 2f
val maxY = layout!!.size.height * (scale - 1) / 2f
val targetTranslation = (it.positionChange() + translation)
if (targetTranslation.x > -maxX && targetTranslation.x < maxX &&
targetTranslation.y > -maxY && targetTranslation.y < maxY
) {
translation = targetTranslation
it.consumePositionChange()
}
}
}
}
}
) {
Image(
painter = painter,
contentDescription = contentDescription,
modifier = Modifier
.matchParentSize()
.onPlaced { layout = it }
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = translation.x,
translationY = translation.y
),
contentScale = ContentScale.Fit
)
LaunchedEffect(transformableState.isTransformInProgress) {
if (!transformableState.isTransformInProgress) {
if (scale < 1f) {
val originScale = scale
val originTranslation = translation
AnimationState(initialValue = 0f).animateTo(
1f,
SpringSpec(stiffness = Spring.StiffnessLow)
) {
scale = originScale + (1 - originScale) * this.value
translation = originTranslation * (1 - this.value)
}
} else {
if (layout == null) return#LaunchedEffect
val maxX = layout!!.size.width * (scale - 1) / 2f
val maxY = layout!!.size.height * (scale - 1) / 2f
val target = Offset(
translation.x.coerceIn(-maxX, maxX),
translation.y.coerceIn(-maxY, maxY)
)
AnimationState(
typeConverter = Offset.VectorConverter,
initialValue = translation
).animateTo(target, SpringSpec(stiffness = Spring.StiffnessLow)) {
translation = this.value
}
}
}
}
}
}
If you only need to constraint pan inside your image instead of animating back to desired levels it's very simple. Constraint it using your Composable size and zoom level as
val maxX = (size.width * (zoom - 1) / 2f)
val maxY = (size.height * (zoom - 1) / 2f)
offset = newOffset
offset = Offset(
newOffset.x.coerceIn(-maxX, maxX),
newOffset.y.coerceIn(-maxY, maxY)
-max, +max is the range and you set it between -max to +max when your TransformOrigin for graphicsLayer is (0.5,0.5) which is the default value. Be mindful about moving range when you change TransformOrigin
Full Implementation
States
var zoom by remember { mutableStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
var size by remember { mutableStateOf(IntSize.Zero) }
Modifier
val imageModifier = Modifier
.fillMaxSize()
.aspectRatio(4/3f)
.clipToBounds()
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { _, gesturePan, gestureZoom, _ ->
val newScale = (zoom * gestureZoom).coerceIn(1f, 3f)
val newOffset = offset + gesturePan
zoom = newScale
val maxX = (size.width * (zoom - 1) / 2f)
val maxY = (size.height * (zoom - 1) / 2f)
offset = Offset(
newOffset.x.coerceIn(-maxX, maxX),
newOffset.y.coerceIn(-maxY, maxY)
)
}
)
}
.onSizeChanged {
size = it
}
.graphicsLayer {
translationX = offset.x
translationY = offset.y
scaleX = zoom
scaleY = zoom
}

Android Jetpack Compose: how to zoom a image in a "box"?

I am going to build a zoomable image view inside a box, like the first screenshot. But when I zoom in on the image, it will go out of the box. Is there a way to zoom the image, but keep the size? Without view or fragment, only box seems not enough. I am expecting the image to get bigger, but still stay inside the red box, but I got the second screenshot after zooming in.
Thanks to nglauber and Amirhosein, I got the final solution with having the zooming and dragging features at the same time inside a "box"(fixed area) with the following code as the new screenshot as shown below.
val imageBitmap = imageResource(id = R.drawable.android)
Image(
modifier = Modifier
.preferredSize(400.dp, 300.dp)
.clip(RectangleShape)
.zoomable(onZoomDelta = { scale.value *= it })
.rawDragGestureFilter(
object : DragObserver {
override fun onDrag(dragDistance: Offset): Offset {
translate.value = translate.value.plus(dragDistance)
return super.onDrag(dragDistance)
}
})
.graphicsLayer(
scaleX = scale.value,
scaleY = scale.value,
translationX = translate.value.x,
translationY = translate.value.y
),
contentDescription = null,
bitmap = imageBitmap
)
Here is my solution... Might be helpful for someone...
#Composable
fun ZoomableImage() {
val scale = remember { mutableStateOf(1f) }
val rotationState = remember { mutableStateOf(1f) }
Box(
modifier = Modifier
.clip(RectangleShape) // Clip the box content
.fillMaxSize() // Give the size you want...
.background(Color.Gray)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
scale.value *= zoom
rotationState.value += rotation
}
}
) {
Image(
modifier = Modifier
.align(Alignment.Center) // keep the image centralized into the Box
.graphicsLayer(
// adding some zoom limits (min 50%, max 200%)
scaleX = maxOf(.5f, minOf(3f, scale.value)),
scaleY = maxOf(.5f, minOf(3f, scale.value)),
rotationZ = rotationState.value
),
contentDescription = null,
painter = painterResource(R.drawable.dog)
)
}
}
I have a bit more general solution that supports panning. This is based on the answers provided by nglauber and arun-padiyan.
#Composable
fun ZoomableBox(
modifier: Modifier = Modifier,
minScale: Float = 0.1f,
maxScale: Float = 5f,
content: #Composable ZoomableBoxScope.() -> Unit
) {
var scale by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var size by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier = modifier
.clip(RectangleShape)
.onSizeChanged { size = it }
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
scale = maxOf(minScale, minOf(scale * zoom, maxScale))
val maxX = (size.width * (scale - 1)) / 2
val minX = -maxX
offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x))
val maxY = (size.height * (scale - 1)) / 2
val minY = -maxY
offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y))
}
}
) {
val scope = ZoomableBoxScopeImpl(scale, offsetX, offsetY)
scope.content()
}
}
interface ZoomableBoxScope {
val scale: Float
val offsetX: Float
val offsetY: Float
}
private data class ZoomableBoxScopeImpl(
override val scale: Float,
override val offsetX: Float,
override val offsetY: Float
) : ZoomableBoxScope
The usage is then something like:
ZoomableBox {
Image(
modifier = Modifier
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = offsetX,
translationY = offsetY
),
bitmap = bitmap,
contentDescription = null
)
}
zoomable has deprecated can use PointerInputScope.detectTransformGestures
#Composable
fun ImagePreview(link: String) {
Box(modifier = Modifier.fillMaxSize()) {
var angle by remember { mutableStateOf(0f) }
var zoom by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
CoilImage(
data = link,
contentDescription = "image",
contentScale = ContentScale.Fit,
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.graphicsLayer(
scaleX = zoom,
scaleY = zoom,
rotationZ = angle
)
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { _, pan, gestureZoom, gestureRotate ->
angle += gestureRotate
zoom *= gestureZoom
val x = pan.x * zoom
val y = pan.y * zoom
val angleRad = angle * PI / 180.0
offsetX += (x * cos(angleRad) - y * sin(angleRad)).toFloat()
offsetY += (x * sin(angleRad) + y * cos(angleRad)).toFloat()
}
)
}
.fillMaxSize()
)
}
}
Just set zoomable and rawDragGestureFilter on Image instead of Box :
#Preview
#Composable
fun Zoomable(){
val scale = remember { mutableStateOf(1f) }
val translate = remember { mutableStateOf(Offset(0f, 0f)) }
Box(
modifier = Modifier.preferredSize(300.dp)
) {
val imageBitmap = imageResource(id = R.drawable.cover)
Image(
modifier = Modifier
.zoomable(onZoomDelta = { scale.value *= it })
.rawDragGestureFilter(
object : DragObserver {
override fun onDrag(dragDistance: Offset): Offset {
translate.value = translate.value.plus(dragDistance)
return super.onDrag(dragDistance)
}
})
.graphicsLayer(
scaleX = scale.value,
scaleY = scale.value,
translationX = translate.value.x,
translationY = translate.value.y
),
contentDescription = null,
bitmap = imageBitmap
)
}
}
Using Jetpack Compose AsyncImage from coil.
Copied code from nglauber and adjusted for my requirements.
Add zoom/pan screen overflow restrictions. No image Rotation.
I didn't want to add dependency just to show an accessible image.
#Composable
fun ImagePreview(model: Any, contentDescription: String? = null) {
Box(modifier = Modifier.fillMaxSize()) {
val angle by remember { mutableStateOf(0f) }
var zoom by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp.value
val screenHeight = configuration.screenHeightDp.dp.value
AsyncImage(
model,
contentDescription = contentDescription,
contentScale = ContentScale.Fit,
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.graphicsLayer(
scaleX = zoom,
scaleY = zoom,
rotationZ = angle
)
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { _, pan, gestureZoom, _ ->
zoom = (zoom * gestureZoom).coerceIn(1F..4F)
if (zoom > 1) {
val x = (pan.x * zoom)
val y = (pan.y * zoom)
val angleRad = angle * PI / 180.0
offsetX =
(offsetX + (x * cos(angleRad) - y * sin(angleRad)).toFloat()).coerceIn(
-(screenWidth * zoom)..(screenWidth * zoom)
)
offsetY =
(offsetY + (x * sin(angleRad) + y * cos(angleRad)).toFloat()).coerceIn(
-(screenHeight * zoom)..(screenHeight * zoom)
)
} else {
offsetX = 0F
offsetY = 0F
}
}
)
}
.fillMaxSize()
)
}
}

Animate based on dimensions

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)
}
)
}

Categories

Resources