Circular draggable slider in jetpack compose - android

I need to prepare a UI with a circular slider where I can pass value or move drag the pointer to slide in the slider similar to the screenshot attached.
How can I achieve it?

You can check out this quick solution. I have 0 degrees at the right but you can reverse the quadrants to suit your needs.
#Composable
fun Content() {
var radius by remember {
mutableStateOf(0f)
}
var shapeCenter by remember {
mutableStateOf(Offset.Zero)
}
var handleCenter by remember {
mutableStateOf(Offset.Zero)
}
var angle by remember {
mutableStateOf(20.0)
}
Canvas(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
handleCenter += dragAmount
angle = getRotationAngle(handleCenter, shapeCenter)
change.consumeAllChanges()
}
}
.padding(30.dp)
) {
shapeCenter = center
radius = size.minDimension / 2
val x = (shapeCenter.x + cos(Math.toRadians(angle)) * radius).toFloat()
val y = (shapeCenter.y + sin(Math.toRadians(angle)) * radius).toFloat()
handleCenter = Offset(x, y)
drawCircle(color = Color.Black.copy(alpha = 0.10f), style = Stroke(20f), radius = radius)
drawArc(
color = Color.Yellow,
startAngle = 0f,
sweepAngle = angle.toFloat(),
useCenter = false,
style = Stroke(20f)
)
drawCircle(color = Color.Cyan, center = handleCenter, radius = 60f)
}
}
private fun getRotationAngle(currentPosition: Offset, center: Offset): Double {
val (dx, dy) = currentPosition - center
val theta = atan2(dy, dx).toDouble()
var angle = Math.toDegrees(theta)
if (angle < 0) {
angle += 360.0
}
return angle
}

Check out the solution in this code on Github.
Just replace the angle value in Canvas.

Related

How to implement vertical likert scale with jetpack compose

I am attempting to create a vertical Likert scale using Jetpack Compose. Each field should include a vertical line that extends beyond its boundaries. I have also included an image to give an idea of what I am trying to achieve.
You can use a Box to put a Canvas on top of the row items.
Something like:
Box(Modifier.fillMaxWidth()){
//Just a simple for the row items
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
){
for (i in 1..5) {
RoundedIem()
}
}
//vertical Likert scale
Canvas(modifier = Modifier.fillMaxWidth()){
val height = 60.dp.toPx() //height of items
val verticalOffset = 76.dp.toPx() //height + vertical padding
val strokeWidthCircle =1f //stroke circle width
val radiusCircle = 30f //radius circle
for (i in 0..4) {
val circleCenterX = size.width-100f
val circleCenterY = height/2+ i*verticalOffset
//Inner white circle
drawCircle(
color = White,
radius = radiusCircle,
center = Offset(circleCenterX ,circleCenterY),
)
//Stroke circle
drawCircle(
color = DarkGray,
radius = radiusCircle,
center = Offset(circleCenterX, circleCenterY),
style = Stroke(width = strokeWidthCircle)
)
//vertical line
if (i < 4) {
val startY = circleCenterY + radiusCircle + strokeWidthCircle
drawLine(
color = DarkGray,
start = Offset(
x = circleCenterX,
y = startY),
end = Offset(
x = circleCenterX,
y = startY + verticalOffset - strokeWidthCircle),
strokeWidth = strokeWidthCircle
)
}
}
}
}
#Composable
fun RoundedIem(){
Row(
modifier= Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.height(60.dp)
.clip(RoundedCornerShape(8.dp))
.background(LightGray.copy(alpha = 0.5f))
.padding(start = 10.dp),
verticalAlignment = Alignment.CenterVertically,
){
Text("Option")
}
}
If you want to add also an icon in the circle you can draw the icon in the Canvas using:
//Icon
val painter = rememberVectorPainter(Icons.Default.Done)
Canvas(modifier = Modifier.fillMaxWidth()){
//previous code
val iconSize = 60f
//circle center - iconSize/2
translate(
left = circleCenterX - iconSize/2,
top = circleCenterY - iconSize/2
) {
with(painter) {
draw(
size = Size(iconSize,iconSize),//painter.intrinsicSize,
colorFilter = ColorFilter.tint(Color.Blue)
)
}
}
}
With a background:
drawCircle(
color = Blue,
radius = radiusCircle-8f,
center = Offset(circleCenterX,circleCenterY),
)
//circle center - iconSize/2
val iconSize = 48f
translate(
left = circleCenterX- iconSize/2,
top = circleCenterY -iconSize/2
) {
with(painter) {
draw(
size = Size(iconSize,iconSize),//painter.intrinsicSize,
colorFilter = ColorFilter.tint(Color.White)
)
}
}

How to get transparent while erasing the canvas in Jetpack Compose , now I'm getting white color?

how can I make some parts of canvas transparent? I want user to be able to erase parts of an photo like this link shows to be transparent. my canvas code:
Canvas(
modifier = modifier
.background(Color.Transparent)
) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
drawImage(
image = bitmap,
srcSize = IntSize(bitmap.width, bitmap.height),
dstSize = IntSize(canvasWidth, canvasHeight)
)
drawPath(
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear,
color = Color.Transparent,
)
restoreToCount(checkPoint)
}
}
What you get as Transparent is Color(0x00000000), white you get is the color of your background, even if you Canvas has transparent background, color of your root or parent Composable is white.
You need to draw checker layout or checker image first, inside Layer you should draw your image and path with BlendMode.Clear
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
val space = 20.dp.roundToPx()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstBitmap,
dstOffset = IntOffset(
space / 2,
space / 2
),
dstSize = IntSize(
canvasWidth - space, canvasHeight - space
)
)
// Source
drawPath(
color = Color.Transparent,
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
Full implementation
#Composable
private fun MyImageDrawer(modifier: Modifier) {
// This is the image to draw onto
val dstBitmap = ImageBitmap.imageResource(id = R.drawable.landscape1)
// Path used for erasing. In this example erasing is faked by drawing with canvas color
// above draw path.
val erasePath = remember { Path() }
var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
// This is our motion event we get from touch motion
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
// This is previous motion event before next touch is saved into this current position
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }
val drawModifier = modifier
.pointerMotionEvents(Unit,
onDown = { pointerInputChange ->
motionEvent = MotionEvent.Down
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onMove = { pointerInputChange ->
motionEvent = MotionEvent.Move
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onUp = { pointerInputChange ->
motionEvent = MotionEvent.Up
pointerInputChange.consume()
}
)
Canvas(modifier = drawModifier) {
val canvasWidth = size.width.roundToInt()
val canvasHeight = size.height.roundToInt()
// Draw or erase depending on erase mode is active or not
when (motionEvent) {
MotionEvent.Down -> {
erasePath.moveTo(currentPosition.x, currentPosition.y)
previousPosition = currentPosition
}
MotionEvent.Move -> {
erasePath.quadraticBezierTo(
previousPosition.x,
previousPosition.y,
(previousPosition.x + currentPosition.x) / 2,
(previousPosition.y + currentPosition.y) / 2
)
previousPosition = currentPosition
}
MotionEvent.Up -> {
erasePath.lineTo(currentPosition.x, currentPosition.y)
currentPosition = Offset.Unspecified
previousPosition = currentPosition
motionEvent = MotionEvent.Idle
}
else -> Unit
}
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
val space = 20.dp.roundToPx()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstBitmap,
dstOffset = IntOffset(
space / 2,
space / 2
),
dstSize = IntSize(
canvasWidth - space, canvasHeight - space
)
)
// Source
drawPath(
color = Color.Transparent,
path = erasePath,
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
}
}
Outcome

Wavy box in Jetpack compose

Is there a way to make a box with a wavy top with Canvas?
I would like to know if this effect can be achieved directly with a Canvas, it is not necessary to have a scrolling animation.
It's not quite clear why you're talking about Canvas. To crop a view like this, you can use a custom Shape and apply it to your view with Modifier.clip. Here's a shape you can use:
class WavyShape(
private val period: Dp,
private val amplitude: Dp,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(Path().apply {
val wavyPath = Path().apply {
val halfPeriod = with(density) { period.toPx() } / 2
val amplitude = with(density) { amplitude.toPx() }
moveTo(x = -halfPeriod / 2, y = amplitude)
repeat(ceil(size.width / halfPeriod + 1).toInt()) { i ->
relativeQuadraticBezierTo(
dx1 = halfPeriod / 2,
dy1 = 2 * amplitude * (if (i % 2 == 0) 1 else -1),
dx2 = halfPeriod,
dy2 = 0f,
)
}
lineTo(size.width, size.height)
lineTo(0f, size.height)
}
val boundsPath = Path().apply {
addRect(Rect(offset = Offset.Zero, size = size))
}
op(wavyPath, boundsPath, PathOperation.Intersect)
})
}
If you really need to use this inside Canvas for some reason, you can pass the same Path that I create inside WavyShape to DrawScope.clipPath, so that the contents of the clipPath block will be clipped.
Apply custom shape to your Image or any other view:
Image(
painter = painterResource(id = R.drawable.my_image_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clip(WavyShape(period = 100.dp, amplitude = 50.dp))
)
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()
)
}
}

Categories

Resources