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?
Related
I would like to create a watermark effect in my app using text as shown in the picture below.
I achieved this by using canvas and bitmap, is there any other reliable way to do this?
Here is my composable function
#Composable
fun WaterMark(
modifier: Modifier = Modifier,
content: (#Composable BoxScope.() -> Unit)? = null,
) {
val watermarkText: String = "some mutable text"
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.textSize = LocalContext.current.dpToPx(24).toFloat()
paint.color = PSCoreColours.psCoreColours.onSurface.hashCode()
paint.textAlign = Paint.Align.LEFT
paint.alpha = (255 * 0.25).toInt()
val baseline: Float = -paint.ascent()
val image: Bitmap = Bitmap.createBitmap(paint.measureText(watermarkText).toInt(),
(baseline + paint.descent()).toInt(),
Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(image)
canvas.drawText(watermarkText, 0f, baseline, paint)
val rotationMatrix: Matrix = Matrix().apply { postRotate(-45f) }
val rotatedImage: Bitmap = Bitmap.createBitmap(image, 0, 0, image.width, image.height, rotationMatrix, true)
val pattern: ImageBitmap = rotatedImage.asImageBitmap()
Box {
content?.let { it() }
Canvas(
modifier = modifier
) {
val totalWidth = size.width / pattern.width
val totalHeight = size.height / pattern.height
var x = 0f
var y = 0f
for (i in 0..totalHeight.toInt()) {
y = (i * pattern.height).toFloat()
for (j in 0..totalWidth.toInt()) {
x = (j * pattern.width).toFloat()
drawImage(
pattern,
colorFilter = null,
topLeft = Offset(x, y),
)
}
}
}
}
}
You can do custom layouts in compose for this
private const val SPACING = 100
#Composable
fun Watermark(
content: #Composable BoxScope.() -> Unit,
) {
Box {
content()
Layout(
content = {
// Repeating the placeables, 6 should do for now but we should be able to calculate this too
repeat(6) {
Text(
text = watermarkText,
..
)
}
}
) { measurables, constraints ->
// Measuring all the placables
val placeables: List<Placeable> = measurables
.map { measurable -> measurable.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
// Calculating the max width of a placable
val maxWidth: Double = placeables.maxOf { it.width }.toDouble()
// Calculating the max width of a tile given the text is rotated
val tileSize: Int = (constraints.maxWidth / atan(maxWidth)).toInt()
placeables
.chunked(2) // Placing 2 columns
.forEachIndexed { index, (first, second) ->
val indexedTileSize: Int = index * tileSize
first.placeRelativeWithLayer(-SPACING, indexedTileSize + SPACING) { rotationZ = -45f }
second.placeRelativeWithLayer(tileSize, indexedTileSize) { rotationZ = -45f }
}
}
}
}
}
Watermark function creates instance of Paint and Bitmap on each recomposition. You should wrap them with remember as in this answer.
However you might, i think, do what you do fully Compose way without Paint and Bitmap either using Modifier.drawWithContent{} and drawText function of DrawScope and using translate or rotate inside DrawScope.
This is a drawText sample to understand how you can create and store TextLayoutResult remember.
And another sample using Modifier.drawWithContent
You can also try using Modifier.drawWithCache to cache TextLayoutResult in layout phase instead of composition phase which is suggested by Google Compose developer works on Text here
Is there a way to have an Icon (with ImageVector) component with a shadow/elevation in Jetpack Compose?
I want to make an IconButton with an elevated Icon but there seems to be no solution available for this problem. Things like Modifier.shadow() will only draw a shadow box around my icon and the Icon component itself has no elevation parameter.
This ticket seems like a duplicate of How to add a shadow / border / elevation to an icon in Jetpack Compose at first glance, but that ticket is not referring to the Icon component in combination with an ImageVector. Also, the proposed solution does not work and it wasn't updated in 6 months.
To further clarify, I want my Icon to look like this:
What you require is a library that converts imageVectors or xml files into Path. As i know of there is no built-in library for this. There are probably few out there that converts into Path or Shape.
When you have a shape or path what you need to do is draw with this shape as Modifier or into Canvas
fun Modifier.vectorShadow(
path: Path,
x: Dp,
y: Dp,
radius: Dp
) = composed(
inspectorInfo = {
name = "vectorShadow"
value = path
value = x
value = y
value = radius
},
factory = {
val paint = remember {
Paint()
}
val frameworkPaint = remember {
paint.asFrameworkPaint()
}
val color = Color.DarkGray
val dx: Float
val dy: Float
val radiusInPx: Float
with(LocalDensity.current) {
dx = x.toPx()
dy = y.toPx()
radiusInPx = radius.toPx()
}
drawBehind {
this.drawIntoCanvas {
val transparent = color
.copy(alpha = 0f)
.toArgb()
frameworkPaint.color = transparent
frameworkPaint.setShadowLayer(
radiusInPx,
dx,
dy,
color
.copy(alpha = .7f)
.toArgb()
)
it.drawPath(path, paint)
}
}
}
)
Usage
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
val center = with(LocalDensity.current) {
150.dp.toPx()
}
val path1 = createPolygonPath(center, center, 6, center)
val path2 = createPolygonPath(center, center, 5, center)
Canvas(
modifier = Modifier
.size(300.dp)
.vectorShadow(path1, 0.dp, 0.dp, 6.dp)
.border(3.dp, Color.Green)
) {
drawPath(path1, Color.White)
}
Spacer(modifier = Modifier.height(10.dp))
Canvas(
modifier = Modifier
.size(300.dp)
.vectorShadow(path2, 3.dp, 3.dp, 10.dp)
.border(3.dp, Color.Green)
) {
drawPath(path2, Color.White)
}
}
Result
createPolygonPath is a sample function to create Path. If you manage to convert your vector to Path rest is simple.
fun createPolygonPath(cx: Float, cy: Float, sides: Int, radius: Float): Path {
val angle = 2.0 * Math.PI / sides
return Path().apply {
moveTo(
cx + (radius * cos(0.0)).toFloat(),
cy + (radius * sin(0.0)).toFloat()
)
for (i in 1 until sides) {
lineTo(
cx + (radius * cos(angle * i)).toFloat(),
cy + (radius * sin(angle * i)).toFloat()
)
}
close()
}
}
It's not exactly what you want but for elevating an icon you can simply do this:
Icon(
Icons.Outlined.Refresh, contentDescription = "back",
modifier = Modifier
.size(300.dp)
.offset(10.dp, 10.dp), tint = Color(0, 0, 0, 40)
)
Icon(
Icons.Outlined.Refresh, contentDescription = "front",
modifier = Modifier.size(300.dp), tint = Color(0xFFb6d7a8)
)
The problem is that it is lacking the blurring effect.
I am trying to get what part of the image has been clicked and get the Color of that point
I'm able to get the tap coordinates through a Modifier but I don't know how to relate it with the drawable of the Image:
#Composable
fun InteractableImage(
modifier = Modifier
...
) {
val sectionClicked by rememberSaveable { mutableStateOf<Offset>(Offset.Unspecified) }
Image(
modifier = Modifier.fillMaxSize().pointerInput(Unit) {
detectTapGestures (
onTap = { offset -> sectionClicked = offset },
painter = painterResource(id = R.drawable.ic_square),
contentDescription = "square"
)
....
}
With the classic view system, I could access the matrix of the imageView, and with the scale property and the intrinsic drawable dimensions, I could find the part of the image that was clicked. How could I achieve this using Android Compose?
It's a little bit long but working solution except when there are spaces at edges of the Bitmap inside Image because of contentScale param of Image. Need to do another operation for calculating space around but couldn't find it at the moment. And if anyone finds the space just set it to startX, startY and use linear interpolation.
1- Instead of painter get an ImageBitmap
val imageBitmap: ImageBitmap = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.landscape7
)
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
2- Create variables for touch position on x, y axes and size of the Image that we put bitmap into
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var imageSize by remember { mutableStateOf(Size.Zero) }
Get Image width from Modifier.onSizeChanged{imageSize = it.toSize()}
3- Get touch position scale it to bitmap position, convert to pixel and return r, g, b values from pixel
.pointerInput(Unit) {
detectTapGestures { offset: Offset ->
// Touch coordinates on image
offsetX = offset.x
offsetY = offset.y
// Scale from Image touch coordinates to range in Bitmap
val scaledX = (bitmapWidth/imageSize.width)*offsetX
val scaledY = (bitmapHeight/imageSize.height)*offsetY
try {
val pixel: Int =
imageBitmap
.asAndroidBitmap()
.getPixel(scaledX.toInt(), scaledY.toInt())
// Don't know if there is a Compose counterpart for this
val red = android.graphics.Color.red(pixel)
val green = android.graphics.Color.green(pixel)
val blue = android.graphics.Color.blue(pixel)
colorInTouchPosition = Color(red,green,blue)
}catch (e:Exception){
println("Exception e: ${e.message}")
}
}
}
4- Instead of scaling and lerping you can just use
val scaledX = (bitmapWidth/endImageX)*offsetX but when your bitmap is not fit into image you need to use linear interpolation after calculating space on left, right, top or bottom of the image, or you should make sure that Image has the same width/height ratio as Bitmap.
i used contentScale = ContentScale.FillBounds for simplicity
// Scale from Image touch coordinates to range in Bitmap
val scaledX = (bitmapWidth/imageSize.width)*offsetX
val scaledY = (bitmapHeight/imageSize.height)*offsetY
Full Implementation
#Composable
private fun TouchOnImageExample() {
val imageBitmap: ImageBitmap = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.landscape6
)
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var imageSize by remember { mutableStateOf(Size.Zero) }
// These are for debugging
var text by remember { mutableStateOf("") }
var colorInTouchPosition by remember { mutableStateOf(Color.Unspecified) }
val imageModifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
// This is for displaying different ratio, optional
.aspectRatio(4f / 3)
.pointerInput(Unit) {
detectTapGestures { offset: Offset ->
// Touch coordinates on image
offsetX = offset.x
offsetY = offset.y
// Scale from Image touch coordinates to range in Bitmap
val scaledX = (bitmapWidth/imageSize.width)*offsetX
val scaledY = (bitmapHeight/imageSize.height)*offsetY
// TODO This section needed when Bitmap does not fill Image completely
// However i couldn't find a solution to find spaces correctly
// // Need to calculate spaces at edges of the bitmap inside Image Composable if
// // not exactly filling the bounds of Image
// val startImageX = 0f
// val startImageY = 0f
//
// // End positions, this might be less than Image dimensions if bitmap doesn't fit Image
// val endImageX = imageSize.width - startImageX
// val endImageY = imageSize.height - startImageY
// val scaledX =
// scale(
// start1 = startImageX,
// end1 = endImageX,
// pos = offsetX,
// start2 = 0f,
// end2 = bitmapWidth.toFloat()
// ).coerceAtMost(bitmapWidth.toFloat())
// val scaledY =
// scale(
// start1 = startImageY,
// end1 = endImageY,
// pos = offsetY,
// start2 = 0f,
// end2 = bitmapHeight.toFloat()
// ).coerceAtMost(bitmapHeight.toFloat())
try {
val pixel: Int =
imageBitmap
.asAndroidBitmap()
.getPixel(scaledX.toInt(), scaledY.toInt())
// Don't know if there is a Compose counterpart for this
val red = android.graphics.Color.red(pixel)
val green = android.graphics.Color.green(pixel)
val blue = android.graphics.Color.blue(pixel)
text = "Image Touch: $offsetX, offsetY: $offsetY\n" +
"size: $imageSize\n" +
"bitmap width: ${bitmapWidth}, height: $bitmapHeight\n" +
"scaledX: $scaledX, scaledY: $scaledY\n" +
"red: $red, green: $green, blue: $blue\n"
colorInTouchPosition = Color(red,green,blue)
}catch (e:Exception){
println("Exception e: ${e.message}")
}
}
}
.onSizeChanged { imageSize = it.toSize() }
Image(
bitmap = imageBitmap,
contentDescription = null,
modifier = imageModifier,
contentScale = ContentScale.FillBounds
)
Text(text = text)
Box(
modifier = Modifier
.then(
if (colorInTouchPosition == Color.Unspecified) {
Modifier
} else {
Modifier.background(colorInTouchPosition)
}
)
.size(100.dp)
)
}
/**
* Interpolate position x linearly between start and end
*/
fun lerp(start: Float, end: Float, amount: Float): Float {
return start + amount * (end - start)
}
/**
* Scale x1 from start1..end1 range to start2..end2 range
*/
fun scale(start1: Float, end1: Float, pos: Float, start2: Float, end2: Float) =
lerp(start2, end2, calculateFraction(start1, end1, pos))
/**
* Calculate fraction for value between a range [end] and [start] coerced into 0f-1f range
*/
fun calculateFraction(start: Float, end: Float, pos: Float) =
(if (end - start == 0f) 0f else (pos - start) / (end - start)).coerceIn(0f, 1f)
Result
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:
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()
)
}
}