Android Jetpack Compose complex animate performance issue - android

i just started the compose animation and now I'm confused I Have a login screen and ı want to with animation but ı noticed to performance issues on my code. When ı click the box on first time coming lagyly and ı dont wanna that what can do for this is there any other solution or it just like that ? How can I solve this?
enum class ViewState { WELCOME, SIGNING, SIGNUP }
#ExperimentalAnimationApi
#Composable
fun OnBoardView(deviceHeight: Dp, deviceWidth: Dp,activity: Activity) {
val baseState = remember { mutableStateOf(ViewState.WELCOME) }
val signUpTransition = updateTransition(targetState = baseState, "1")
val signInTransition = updateTransition(targetState = baseState, label = "2")
val focusManager = LocalFocusManager.current
val visible by remember { baseState }
val density = LocalDensity.current
val isRunning = !signUpTransition.isRunning || !signInTransition.isRunning
//SIGN UP
val signUpSize = sizeAnimate(
targetState = ViewState.SIGNUP,
deviceWidth = deviceWidth,
label = "3",
transition = signUpTransition,
)
val signUpOffset = offsetAnimate(
targetState = ViewState.SIGNUP,
label = "4",
transition = signUpTransition,
deviceWidth = deviceWidth
)
//SIGN IN
val signInSize = sizeAnimate(
targetState = ViewState.SIGNING,
deviceWidth = deviceWidth,
label = "5",
transition = signInTransition,
)
val signInOffset = offsetAnimate(
targetState = ViewState.SIGNING,
label = "6",
transition = signInTransition,
deviceWidth = deviceWidth
)
#Composable
fun TextView(label: String, targetState: ViewState, icon: ImageVector, color: Color) {
AnimatedVisibility(
visible = visible == targetState,
enter = slideInHorizontally(
animationSpec = tween(900),
initialOffsetX = {
with(density) {
when (targetState) {
ViewState.SIGNUP -> -deviceWidth.roundToPx()
ViewState.SIGNING -> deviceWidth.roundToPx()
else -> {
0
}
}
}
}
),
exit = slideOutHorizontally(
animationSpec = tween(900),
targetOffsetX = {
with(density) {
when (targetState) {
ViewState.SIGNUP -> -deviceWidth.roundToPx()
ViewState.SIGNING -> deviceWidth.roundToPx()
else -> {
0
}
}
}
}
)
) {
Column(
modifier = Modifier
.fillMaxHeight(0.5F)
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(
enabled = isRunning && !this#AnimatedVisibility.transition.isRunning,
onClick = {
baseState.value = ViewState.WELCOME
},
modifier = Modifier
.size(50.dp)
.clip(shape = CircleShape)
.background(color)
) {
Icon(
icon,
contentDescription = null,
tint = Color.White
)
}
Text(text = label, fontSize = 25.sp)
}
}
}
#Composable
fun MainBoxView(
offset: Offset,
deviceWidth: Dp,
backgroundColor: Color,
selectedState: ViewState,
content: #Composable () -> Unit
) {
Box(
modifier = Modifier
.size(
width = deviceWidth,
height = if (selectedState == ViewState.SIGNUP) deviceHeight / 1.5F else deviceHeight / 2.3F
)
.offset(offset.x.dp, offset.y.dp)
.clip(shapes.medium)
.background(backgroundColor)
.noRippleClickable(isEnabled = isRunning) {
baseState.value = selectedState
focusManager.clearFocus()
},
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
content()
}
}
}
#Composable
fun BoxColumnView(
targetState: ViewState,
content: #Composable () -> Unit,
) {
AnimatedVisibility(
visible = visible == targetState,
enter = slideInHorizontally(
animationSpec = tween(800),
initialOffsetX = { with(density) { if (targetState == ViewState.SIGNUP) (-deviceWidth / 2).roundToPx() else (deviceWidth / 2).roundToPx() } }
),
exit = slideOutHorizontally(
animationSpec = tween(800),
targetOffsetX = { with(density) { if (targetState == ViewState.SIGNUP) (-deviceWidth / 2).roundToPx() else (deviceWidth / 2).roundToPx() } }
)
) {
content()
}
}
#Composable
fun BoxPreTextView(label: String, targetState: ViewState) {
AnimatedVisibility(
visible = visible == ViewState.WELCOME,
enter = when (visible) {
ViewState.SIGNUP -> {
slideInHorizontally(
animationSpec = tween(800),
initialOffsetX = { with(density) { deviceWidth.roundToPx() } }
)
}
ViewState.WELCOME -> {
slideInHorizontally(
animationSpec = tween(800),
initialOffsetX = { with(density) { if (targetState == ViewState.SIGNUP) deviceWidth.roundToPx() else -deviceWidth.roundToPx() } }
)
}
else -> {
slideInHorizontally(
animationSpec = tween(800),
initialOffsetX = { with(density) { deviceWidth.roundToPx() } }
)
}
},
exit = when (visible) {
ViewState.SIGNUP -> {
slideOutHorizontally(
animationSpec = tween(800),
targetOffsetX = { with(density) { deviceWidth.roundToPx() } }
)
}
ViewState.WELCOME -> {
slideOutHorizontally(
animationSpec = tween(800),
targetOffsetX = { with(density) { if (targetState == ViewState.SIGNUP) -deviceWidth.roundToPx() else deviceWidth.roundToPx() } }
)
}
else -> {
slideOutHorizontally(
animationSpec = tween(800),
targetOffsetX = { with(density) { -deviceWidth.roundToPx() } }
)
}
}
) {
Text(
text = label,
color = Color.White,
)
}
}
#Composable
fun MainTextView(label: String, targetState: ViewState) {
AnimatedVisibility(
visible = visible == targetState || visible == ViewState.WELCOME,
enter = slideInHorizontally(
animationSpec = tween(800),
initialOffsetX = {
with(density) {
if (targetState == ViewState.SIGNUP) {
(-deviceWidth / 3).roundToPx()
} else {
(deviceWidth / 3).roundToPx()
}
}
}
),
exit = slideOutHorizontally(
animationSpec = tween(1200),
targetOffsetX = {
with(density) {
if (targetState == ViewState.SIGNUP) {
(-deviceWidth / 3).roundToPx()
} else {
(deviceWidth / 3).roundToPx()
}
}
})
) {
Text(
text = label,
color = Color.White,
fontSize = 24.sp,
)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
focusManager.clearFocus()
},
) {
Box(
modifier = Modifier
.weight(1.5F)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
TextView(
icon = Icons.Default.ArrowForward,
label = "WELCOME!",
targetState = ViewState.SIGNUP,
color = DarkBlue
)
TextView(
icon = Icons.Default.ArrowBack,
label = "WELCOME BACK!",
targetState = ViewState.SIGNING,
color = LightBlue
)
}
Row(
modifier = Modifier
.weight(3F)
.fillMaxSize()
.noRippleClickable {
focusManager.clearFocus()
},
//horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
//SIGN UP
MainBoxView(
offset = signUpOffset,
deviceWidth = signUpSize,
backgroundColor = DarkBlue,
selectedState = ViewState.SIGNUP
) {
Box(
modifier = Modifier.weight(1F),
contentAlignment = Alignment.Center
) {
MainTextView(
label = "SIGN UP",
targetState = ViewState.SIGNUP,
)
}
Box(
modifier = Modifier.weight(6F),
contentAlignment = Alignment.TopCenter
) {
/*
BoxPreTextView(
label = "New here? come on, what are you waiting for, sign up and open up to new worlds",
targetState = ViewState.SIGNUP
)
*/
BoxColumnView(
targetState = ViewState.SIGNUP,
) {
}
}
}
//SIGN IN
MainBoxView(
offset = signInOffset,
deviceWidth = signInSize,
backgroundColor = LightBlue,
selectedState = ViewState.SIGNING
) {
Box(
modifier = Modifier.weight(1F),
contentAlignment = Alignment.Center
) {
MainTextView(
label = "SIGN IN",
targetState = ViewState.SIGNING,
)
}
Box(
modifier = Modifier.weight(4F),
contentAlignment = Alignment.TopCenter
) {
/*
BoxPreTextView(
label = "Returning? Just Sign in to resume what you were doing",
targetState = ViewState.SIGNING
)
*/
BoxColumnView(
targetState = ViewState.SIGNING,
) {
LoginView(activity)
}
}
}
}
Box(
modifier = Modifier
.weight(1F)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier.fillMaxSize(0.5F),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = { },
modifier = Modifier
.size(50.dp)
.clip(shape = CircleShape)
.background(Color.Blue)
) {
Icon(
painterResource(id = R.mipmap.facebook_foreground),
contentDescription = null,
tint = Color.White
)
}
IconButton(
onClick = { },
modifier = Modifier
.size(50.dp)
.clip(shape = CircleShape)
.background(GoogleRed)
) {
Icon(
painterResource(id = R.mipmap.google_foreground),
contentDescription = null,
tint = Color.White
)
}
IconButton(
onClick = { },
modifier = Modifier
.size(50.dp)
.clip(shape = CircleShape)
.background(LightBlue)
) {
Icon(
painterResource(id = R.mipmap.twitter_foreground),
contentDescription = null,
tint = Color.White
)
}
}
}
}
}
#Composable
fun offsetAnimate(
transition: Transition<MutableState<ViewState>>,
targetState: ViewState,
label: String,
deviceWidth: Dp
): Offset {
val offset by transition.animateOffset(
label = label,
transitionSpec = {
if (this.targetState.value == targetState) {
tween(900, 100)
} else {
tween(450)
}
},
) { animated ->
fun width(value: Double) = (deviceWidth.value * value).toFloat()
if (targetState == ViewState.SIGNUP) {
when (animated.value) {
ViewState.SIGNUP -> {
Offset(width(0.07), 0f)
}
ViewState.SIGNING -> {
Offset(-width(0.1), 0f)
}
else -> {
Offset(-width(0.06), 0f)
}
}
} else {
when (animated.value) {
ViewState.SIGNUP -> {
Offset(width(0.2), 0f)
}
ViewState.SIGNING -> {
Offset(width(0.00), 0f)
}
else -> {
Offset(width(0.04), 0f)
}
}
}
}
return offset
}
#Composable
fun sizeAnimate(
transition: Transition<MutableState<ViewState>>,
targetState: ViewState,
label: String,
deviceWidth: Dp
): Dp {
val sizeAnimate by transition.animateDp(
label = label,
transitionSpec = {
if (this.targetState.value == targetState)
tween(450) else tween(900)
},
) {
if (targetState == ViewState.SIGNUP) {
when (it.value) {
ViewState.WELCOME -> deviceWidth / 2F
ViewState.SIGNING -> deviceWidth / 16F
ViewState.SIGNUP -> deviceWidth / 1.15F
}
} else {
when (it.value) {
ViewState.WELCOME -> deviceWidth / 2F
ViewState.SIGNUP -> deviceWidth / 16F
ViewState.SIGNING -> deviceWidth / 1.15F
}
}
}
return sizeAnimate
}

I've had similar performance issue not so long ago and managed to fix it by switching the build variant from debug to release.
Firstly from the bottom left corner of your project click on 'Build Variants'
and select 'release' as the 'Active Build Variant'.
Now you need to set a signing config for the release variant.
Press Ctrl+Alt+Shift+S and this shortcut should open the Project Structure.
Then from 'Modules' select 'app' and click on 'Default Config'.
Find 'Signing Config' and select '$signingConfigs.debug'.
Click Apply and wait for Gradle to fetch the build models.
Then click OK and wait for Gradle to sync the project.
Rebuild the project and performance issue should be gone.

Related

Create a draggable element that will return to its original position when the user lifts their finger

I'm working on implementing a draggable element using Android Jetpack Compose that will smoothly return to its original position when the user lifts their finger. I came to some implementation by I do not really like it.
#Composable
fun dragAndBack2() {
var offset by remember { mutableStateOf(IntOffset.Zero) }
var isAnimating by remember { mutableStateOf(false) }
var animate = animateIntOffsetAsState(
targetValue = if (isAnimating) {
IntOffset.Zero
} else offset,
animationSpec = tween(
durationMillis = 1000,
easing = LinearEasing
),
finishedListener = {
offset = IntOffset.Zero
}
)
Box(
modifier = Modifier
.offset { if (isAnimating) animate.value else offset }
.pointerInput(Unit) {
detectDragGestures(
onDragCancel = { isAnimating = true },
onDragStart = { isAnimating = false },
onDragEnd = { isAnimating = true },
onDrag = { change, dragAmount ->
change.consumeAllChanges()
val offsetChange =
IntOffset(dragAmount.x.roundToInt(), dragAmount.y.roundToInt())
offset = offset.plus(offsetChange)
}
)
}
.background(Color.Blue)
.size(50.dp)
)
}
It works, but when the finger is lifted, the element jumps some distance away from the finger, as if it is jumping to the last saved position.
#Composable
fun AnimatedBox() {
var isAnimating by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = !isAnimating)
var offset by remember { mutableStateOf(IntOffset.Zero) }
val transitionOffset by transition.animateIntOffset(
transitionSpec = {
keyframes {
durationMillis = 1000
offset at 0 with LinearOutSlowInEasing // start value
IntOffset.Zero at 500
}
},
label = "boxSize"
) { b ->
if (b) {
offset
} else {
IntOffset.Zero
}
}
Box(
modifier = Modifier
.offset { transitionOffset }
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
offset = IntOffset.Zero
isAnimating = false
},
onDragEnd = {
isAnimating = true
},
onDrag = { change, dragAmount ->
change.consumeAllChanges()
val offsetChange =
IntOffset(dragAmount.x.roundToInt(), dragAmount.y.roundToInt())
offset = offset.plus(offsetChange)
}
)
}
.background(Color.Blue)
.size(50.dp)
)
}
It also works, but if the finger doesn't move, the element quickly moves back to its original position.
There is also some delay in both versions of the realization. Thanks in advance.
Finally i solved it. Here is some delay between finger and element. But it works like I want.
#Composable
fun onTouchWithTransition(position: IntOffset, sizeDp: Dp) {
val fingerPosition by remember { mutableStateOf(Position(position)) }
val placeToReturn by remember { mutableStateOf(Position(position)) }
var testTransition: Position by remember {
mutableStateOf(fingerPosition)
}
var transition: Transition<Position> = updateTransition(targetState = testTransition)
val transitionOffset by transition.animateIntOffset(
transitionSpec = {
keyframes {
durationMillis = 500
}
},
label = "label"
) {
it.offset
}
Box(modifier = Modifier
.offset { transitionOffset }
.clip(CircleShape)
.background(Color.Red)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
fingerPosition.offset = position
testTransition = fingerPosition
},
onDragEnd = { testTransition = placeToReturn },
onDrag = { change, dragAmount ->
change.consumeAllChanges()
val offsetChange =
IntOffset(dragAmount.x.roundToInt(), dragAmount.y.roundToInt())
fingerPosition.offset = fingerPosition.offset.plus(offsetChange)
}
)
}
.size(sizeDp, sizeDp)
)
}
class Position(private var off: IntOffset) {
var offset: IntOffset by mutableStateOf(off)
}

Composable LazyColumn/Pager Scroll while zoomed

I need to create the pdf reader, I am sending images bitmap array to lazy compose view, it works fine when there is no zoom but with zoom the issue I am facing is that while scrolling to bottom it became too slow to scroll.
Following is the code
#OptIn(ExperimentalPagerApi::class, ExperimentalFoundationApi::class)
#Composable
fun ViewPager(
list: List<Pair<ImageBitmap, List<Fields>>>,
minScale: Float = 1f,
maxScale: Float = 3f,
scrollEnabled: MutableState<Boolean>,
isRotation: Boolean = false
) {
var targetScale by remember { mutableStateOf(1f) }
val scale = animateFloatAsState(targetValue = maxOf(minScale, minOf(maxScale, targetScale)))
var rotationState by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(1f) }
var offsetY by remember { mutableStateOf(1f) }
val configuration = LocalConfiguration.current
val screenWidthPx = with(LocalDensity.current) { configuration.screenWidthDp.dp.toPx() }
val screenHeightPx = with(LocalDensity.current) { configuration.screenHeightDp.dp.toPx() }
val focusManager = LocalFocusManager.current
VerticalPager(
modifier= Modifier
.wrapContentSize()
// .verticalScroll(scrollState)
.fillMaxSize(1f)
.background(color = Color.Blue)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
focusManager.clearFocus()
},
onDoubleClick = {
if (targetScale >= 2f) {
targetScale = 1f
offsetX = 1f
offsetY = 1f
scrollEnabled.value = true
} else targetScale = 3f
}
)
.graphicsLayer {
this.scaleX = scale.value
this.scaleY = scale.value
if (isRotation) {
rotationZ = rotationState
}
this.translationX = offsetX
this.translationY = offsetY
Log.e("Ramesh lazycolumn size", "${this.size.width} ${this.size.height}")
}
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
do {
val event = awaitPointerEvent()
val zoom = event.calculateZoom()
targetScale *= zoom
val offset = event.calculatePan()
if (targetScale <= 1) {
offsetX = 1f
offsetY = 1f
targetScale = 1f
scrollEnabled.value = true
} else {
offsetX += offset.x
// todo change
if (offsetY + offset.y < screenHeightPx && offsetY + offset.y > -screenHeightPx) {
offsetY += offset.y
}
if (zoom > 1) {
scrollEnabled.value = false
rotationState += event.calculateRotation()
}
val imageWidth = screenWidthPx * scale.value
val borderReached =
imageWidth - screenWidthPx - 2 * abs(offsetX)
scrollEnabled.value = borderReached <= 0
if (borderReached < 0) {
offsetX =
((imageWidth - screenWidthPx) / 2f).withSign(offsetX)
if (offset.x != 0f) offsetY -= offset.y
}
}
Log.e("Ramesh lazycolumn xy ", "$offsetX $offsetY")
} while (event.changes.any { it.pressed })
}
}
},
count = list.size,
contentPadding = PaddingValues(0.dp),
flingBehavior = ScrollableDefaults.flingBehavior()
) { page ->
val it = list[page]
Page(
pageContent = {
PageContent(
modifier = Modifier,
imagePainter = BitmapPainter(it.first),
fields = it.second
)
},
fields = it.second,
color = if (page % 2 == 0) Color.Yellow else Color.Cyan
)
}
}
I have used the LazyColumn, Pager, Scrollable column but still same issue.
#Composable
fun Page(
modifier: Modifier = Modifier,
fields: List<Fields>,
pageContent: #Composable () -> Unit,
color: Color
) {
Box(
modifier = modifier
// Modifier.fillMaxHeight().background(color)
// .height(placeables[0].height)
) {
Layout(
content = pageContent
) { measurables, constraints ->
val placeables = measurables.map { measurable -> measurable.measure(constraints) }
Log.e(
"Ramesh",
"Placeable ${placeables[0].height} ${placeables[0].width} \nConstraints ${constraints.maxWidth}, ${constraints.maxHeight}"
)
val factor = placeables[0].height
layout(
width = constraints.maxWidth,
height = placeables[0].height
) {
placeables[0].place(0, 0, 0f)
for (index in 1..placeables.lastIndex) {
val fieldValue = fields[index - 1]
val offset = IntOffset(fieldValue.x, fieldValue.y - 70)
placeables[index].placeRelative(offset, 0f)
// placeables[index].placeRelative(fieldValue.x, fieldValue.y, 0f)
// placeables[index].place(fieldValue.x, fieldValue.y - 120, 0f)
}
}
}
}
}
#Composable
fun PageContent(
modifier: Modifier = Modifier,
fields: List<Fields>,
imagePainter: Painter
) {
Image(
painter = imagePainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = modifier
// .onGloballyPositioned {
// val position = it.positionInRoot()
// Log.e("ramesh image coordinates", "x = ${position.x} y = ${position.y}")
// Log.e("ramesh image size", "x = ${it.size.width} y = ${it.size.height}")
// }
)
// fields.forEach {
// InputField(it)
// }
}
Can anyone help me on this?
thanks

Change an AsyncImage into a bitmap [duplicate]

I'm trying to get two images from an url and then I have a Composable that needs two bitmaps to draw them in a Canvas, I've tried it but the canvas don't get painted am I missing something?
val overlayImage =
"https://st2.depositphotos.com/1400069/5999/i/600/depositphotos_59995765-stock-photo-abstract-galaxy-background.jpg"
val baseImage =
"https://www.vitrinesdocomercio.com/uploads/1/3/9/4/13943900/1278180_orig.jpg"
val overlayImageLoaded = rememberAsyncImagePainter(
model = overlayImage,
)
val baseImageLoaded = rememberAsyncImagePainter(
model = baseImage
)
var overlayBitmap = remember<Bitmap?> {
null
}
var baseBitmap = remember<Bitmap?> {
null
}
val overlayImageLoadedState = overlayImageLoaded.state
if (overlayImageLoadedState is AsyncImagePainter.State.Success) {
overlayBitmap = overlayImageLoadedState.result.drawable.toBitmap()
}
val baseImageLoadedState = baseImageLoaded.state
if (baseImageLoadedState is AsyncImagePainter.State.Success) {
baseBitmap = baseImageLoadedState.result.drawable.toBitmap()
}
MyCanvasComposable(baseBitmap, overlayBitmap)
You should assign size for painter to be created, otherwise it returns error
val overlayPainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(overlayImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
val basePainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(baseImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
Result
You can send both of the ImageBitmaps when both states are success with
#Composable
private fun MyComposable() {
val overlayImage =
"https://st2.depositphotos.com/1400069/5999/i/600/depositphotos_59995765-stock-photo-abstract-galaxy-background.jpg"
val baseImage =
"https://www.vitrinesdocomercio.com/uploads/1/3/9/4/13943900/1278180_orig.jpg"
val overlayPainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(overlayImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
val basePainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(baseImage)
.size(coil.size.Size.ORIGINAL) // Set the target size to load the image at.
.build()
)
val overlayImageLoadedState = overlayPainter.state
val baseImageLoadedState = basePainter.state
if (
baseImageLoadedState is AsyncImagePainter.State.Success &&
overlayImageLoadedState is AsyncImagePainter.State.Success
) {
SideEffect {
println("🔥 COMPOSING...")
}
val baseImageBitmap =
baseImageLoadedState.result.drawable.toBitmap()
.asImageBitmap()
val overlayImageBitmap =
overlayImageLoadedState.result.drawable
.toBitmap()
.asImageBitmap()
EraseBitmapSample(
baseImageBitmap = baseImageBitmap,
overlayImageBitmap = overlayImageBitmap,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f)
)
}
}
And what you wish to achieve
#Composable
fun EraseBitmapSample(
overlayImageBitmap: ImageBitmap,
baseImageBitmap: ImageBitmap,
modifier: Modifier
) {
var matchPercent by remember {
mutableStateOf(100f)
}
BoxWithConstraints(modifier) {
// 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 imageWidth = constraints.maxWidth
val imageHeight = constraints.maxHeight
val drawImageBitmap = remember {
Bitmap.createScaledBitmap(
overlayImageBitmap.asAndroidBitmap(),
imageWidth,
imageHeight,
false
)
.asImageBitmap()
}
// Pixels of scaled bitmap, we scale it to composable size because we will erase
// from Composable on screen
val originalPixels: IntArray = remember {
val buffer = IntArray(imageWidth * imageHeight)
drawImageBitmap
.readPixels(
buffer = buffer,
startX = 0,
startY = 0,
width = imageWidth,
height = imageHeight
)
buffer
}
val erasedBitmap: ImageBitmap = remember {
Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888).asImageBitmap()
}
val canvas: Canvas = remember {
Canvas(erasedBitmap)
}
val paint = remember {
Paint()
}
val erasePaint = remember {
Paint().apply {
blendMode = BlendMode.Clear
this.style = PaintingStyle.Stroke
strokeWidth = 30f
}
}
canvas.apply {
val nativeCanvas = this.nativeCanvas
val canvasWidth = nativeCanvas.width.toFloat()
val canvasHeight = nativeCanvas.height.toFloat()
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
matchPercent = compareBitmaps(
originalPixels,
erasedBitmap,
imageWidth,
imageHeight
)
}
else -> Unit
}
with(canvas.nativeCanvas) {
drawColor(android.graphics.Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawImageRect(
image = drawImageBitmap,
dstSize = IntSize(canvasWidth.toInt(), canvasHeight.toInt()),
paint = paint
)
drawPath(
path = erasePath,
paint = erasePaint
)
}
}
val canvasModifier = 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()
},
delayAfterDownInMillis = 20
)
Image(
bitmap = baseImageBitmap,
contentDescription = null
)
Image(
modifier = canvasModifier
.clipToBounds()
.matchParentSize()
.border(2.dp, Color.Green),
bitmap = erasedBitmap,
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
Text(
text = "Bitmap match ${matchPercent}%",
color = Color.Red,
fontSize = 22.sp,
)
}

Prevent dragging box out of the screen with Jetpack Compose

I have such code from here: https://developer.android.com/jetpack/compose/gestures
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Red)
.size(120.dp) // makes it rectangle. wrap_content without it
.align(Alignment.BottomEnd)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
) {
// todo
}
}
So for end side of x I create something like this:
val newOffsetX = if ((offsetX + dragAmount.x) < 0) { offsetX + dragAmount.x } else { 0 }
offsetX = newOffsetX
But how can I found start of x and prevent my draggable box go out of screen?
Is there a way to do it for both X and Y?
If you align your draggable box with Alignment.TopStart you can coerce min and max width and height between 0 and parent size - box size
#Composable
private fun DragSample() {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val parentWidth = constraints.maxWidth
val parentHeight = constraints.maxHeight
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Red)
.size(120.dp) // makes it rectangle. wrap_content without it
.align(Alignment.TopStart)
.pointerInput(Unit) {
val boxSize = this.size
detectDragGestures { _, dragAmount ->
offsetX = (offsetX + dragAmount.x).coerceIn(
0f,
parentWidth - boxSize.width.toFloat()
)
offsetY = (offsetY + dragAmount.y).coerceIn(
0f,
parentHeight - boxSize.height.toFloat()
)
}
}
) {
// todo
}
}
}
If you wish to start from Alignemnt.BottomEnd you should do it as
#Composable
private fun DragSample() {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val parentWidth = constraints.maxWidth
val parentHeight = constraints.maxHeight
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Red)
.size(120.dp) // makes it rectangle. wrap_content without it
.align(Alignment.BottomEnd)
.pointerInput(Unit) {
val boxSize = this.size
detectDragGestures { _, dragAmount ->
offsetX = (offsetX + dragAmount.x).coerceIn(
boxSize.width.toFloat() -parentWidth,
0f
)
offsetY = (offsetY + dragAmount.y).coerceIn(
boxSize.height.toFloat() -parentHeight,
0f
)
}
}
) {
// todo
}
}
}

Elliptical list in Compose

was wondering if anyone knows how to produce elliptical/arched list in Compose?
Something along these lines:
Not sure If I'm overlooking an 'easy' way of doing it in Compose. Cheers!
I have an article here that shows how to do this. It is not a LazyList in that it computes all the items (but only renders the visible ones); you can use this as a starting point to build upon.
The full code is below as well:
data class CircularListConfig(
val contentHeight: Float = 0f,
val numItems: Int = 0,
val visibleItems: Int = 0,
val circularFraction: Float = 1f,
val overshootItems: Int = 0,
)
#Stable
interface CircularListState {
val verticalOffset: Float
val firstVisibleItem: Int
val lastVisibleItem: Int
suspend fun snapTo(value: Float)
suspend fun decayTo(velocity: Float, value: Float)
suspend fun stop()
fun offsetFor(index: Int): IntOffset
fun setup(config: CircularListConfig)
}
class CircularListStateImpl(
currentOffset: Float = 0f,
) : CircularListState {
private val animatable = Animatable(currentOffset)
private var itemHeight = 0f
private var config = CircularListConfig()
private var initialOffset = 0f
private val decayAnimationSpec = FloatSpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
private val minOffset: Float
get() = -(config.numItems - 1) * itemHeight
override val verticalOffset: Float
get() = animatable.value
override val firstVisibleItem: Int
get() = ((-verticalOffset - initialOffset) / itemHeight).toInt().coerceAtLeast(0)
override val lastVisibleItem: Int
get() = (((-verticalOffset - initialOffset) / itemHeight).toInt() + config.visibleItems)
.coerceAtMost(config.numItems - 1)
override suspend fun snapTo(value: Float) {
val minOvershoot = -(config.numItems - 1 + config.overshootItems) * itemHeight
val maxOvershoot = config.overshootItems * itemHeight
animatable.snapTo(value.coerceIn(minOvershoot, maxOvershoot))
}
override suspend fun decayTo(velocity: Float, value: Float) {
val constrainedValue = value.coerceIn(minOffset, 0f).absoluteValue
val remainder = (constrainedValue / itemHeight) - (constrainedValue / itemHeight).toInt()
val extra = if (remainder <= 0.5f) 0 else 1
val target =((constrainedValue / itemHeight).toInt() + extra) * itemHeight
animatable.animateTo(
targetValue = -target,
initialVelocity = velocity,
animationSpec = decayAnimationSpec,
)
}
override suspend fun stop() {
animatable.stop()
}
override fun setup(config: CircularListConfig) {
this.config = config
itemHeight = config.contentHeight / config.visibleItems
initialOffset = (config.contentHeight - itemHeight) / 2f
}
override fun offsetFor(index: Int): IntOffset {
val maxOffset = config.contentHeight / 2f + itemHeight / 2f
val y = (verticalOffset + initialOffset + index * itemHeight)
val deltaFromCenter = (y - initialOffset)
val radius = config.contentHeight / 2f
val scaledY = deltaFromCenter.absoluteValue * (config.contentHeight / 2f / maxOffset)
val x = if (scaledY < radius) {
sqrt((radius * radius - scaledY * scaledY))
} else {
0f
}
return IntOffset(
x = (x * config.circularFraction).roundToInt(),
y = y.roundToInt()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CircularListStateImpl
if (animatable.value != other.animatable.value) return false
if (itemHeight != other.itemHeight) return false
if (config != other.config) return false
if (initialOffset != other.initialOffset) return false
if (decayAnimationSpec != other.decayAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = animatable.value.hashCode()
result = 31 * result + itemHeight.hashCode()
result = 31 * result + config.hashCode()
result = 31 * result + initialOffset.hashCode()
result = 31 * result + decayAnimationSpec.hashCode()
return result
}
companion object {
val Saver = Saver<CircularListStateImpl, List<Any>>(
save = { listOf(it.verticalOffset) },
restore = {
CircularListStateImpl(it[0] as Float)
}
)
}
}
#Composable
fun rememberCircularListState(): CircularListState {
val state = rememberSaveable(saver = CircularListStateImpl.Saver) {
CircularListStateImpl()
}
return state
}
#Composable
fun CircularList(
visibleItems: Int,
modifier: Modifier = Modifier,
state: CircularListState = rememberCircularListState(),
circularFraction: Float = 1f,
overshootItems: Int = 3,
content: #Composable () -> Unit,
) {
check(visibleItems > 0) { "Visible items must be positive" }
check(circularFraction > 0f) { "Circular fraction must be positive" }
Layout(
modifier = modifier.clipToBounds().drag(state),
content = content,
) { measurables, constraints ->
val itemHeight = constraints.maxHeight / visibleItems
val itemConstraints = Constraints.fixed(width = constraints.maxWidth, height = itemHeight)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
state.setup(
CircularListConfig(
contentHeight = constraints.maxHeight.toFloat(),
numItems = placeables.size,
visibleItems = visibleItems,
circularFraction = circularFraction,
overshootItems = overshootItems,
)
)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
for (i in state.firstVisibleItem..state.lastVisibleItem) {
placeables[i].placeRelative(state.offsetFor(i))
}
}
}
}
private fun Modifier.drag(
state: CircularListState,
) = pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
state.stop()
val tracker = VelocityTracker()
awaitPointerEventScope {
verticalDrag(pointerId) { change ->
val verticalDragOffset = state.verticalOffset + change.positionChange().y
launch {
state.snapTo(verticalDragOffset)
}
tracker.addPosition(change.uptimeMillis, change.position)
change.consumePositionChange()
}
}
val velocity = tracker.calculateVelocity().y
val targetValue = decay.calculateTargetValue(state.verticalOffset, velocity)
launch {
state.decayTo(velocity, targetValue)
}
}
}
}

Categories

Resources