Show touch point using Android Jetpack Compose? - android

How show touch point using Android Jetpack Compose?
Example:

Here is my solution.
#Composable
fun TouchableFeedback() {
// Some constants here
val sizeAnimationDuration = 200
val colorAnimationDuration = 200
val boxSize = 100.dp
val startColor = Color.Red.copy(alpha = .05f)
val endColor = Color.Red.copy(alpha = .8f)
// These states are changed to update the animation
var touchedPoint by remember { mutableStateOf(Offset.Zero) }
var visible by remember { mutableStateOf(false) }
// circle color and size in according to the visible state
val colorAnimation by animateColorAsState(
if (visible) startColor else endColor,
animationSpec = tween(
durationMillis = colorAnimationDuration,
easing = LinearEasing
),
finishedListener = {
visible = false
}
)
val sizeAnimation by animateDpAsState(
if (visible) boxSize else 0.dp,
tween(
durationMillis = sizeAnimationDuration,
easing = LinearEasing
)
)
// Box for the whole screen
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures {
// changing the state to set the point touched on the screen
// and make it visible
touchedPoint = it
visible = true
}
}
) {
// The touch offset is px and we need to convert to Dp
val density = LocalDensity.current
val (xDp, yDp) = with(density) {
(touchedPoint.x.toDp() - boxSize / 2) to (touchedPoint.y.toDp() - boxSize / 2)
}
// This box serves as container. It has a fixed size.
Box(
Modifier
.offset(xDp, yDp)
.size(boxSize),
) {
// And this box is animating the background and the size
Box(
Modifier
.align(Alignment.Center)
.background(colorAnimation, CircleShape)
.height(if (visible) sizeAnimation else 0.dp)
.width(if (visible) sizeAnimation else 0.dp),
)
}
}
}
Here is the result:

Related

Shadow clipping in LazyColumn/LazyRow

The shadow is clipping in a very odd way when it's overlapping other items in a LazyRow and I can't figure out why.
I'm running this code on TV emulator but I can't imagine that would make any difference.
Attempt 1: Modifier.shadow()
val colors = listOf(
Color.Red,
Color.Blue,
Color.Green,
Color.Yellow
)
#Composable
fun ListTest() {
LazyColumn {
items(30) {
Column {
Text("This is row $it")
LazyRow {
items(colors) {
var isFocused by remember { mutableStateOf(false) }
val alpha = if (isFocused) 1f else 0.25f
val elevation = if (isFocused) 40.dp else 0.dp
Surface(
shape = RoundedCornerShape(8.dp),
color = it.copy(alpha = alpha),
modifier = Modifier
.width(240.dp)
.height(150.dp)
.padding(start = 16.dp)
// 🔴 Look here
.shadow(elevation)
.onFocusChanged { state ->
isFocused = state.isFocused
}
.focusable(),
) {
// Content here
}
}
}
}
}
}
}
Attempt 2: Modifier.drawBehind {}
I was referred to these lines in the Android code that limits elevation to 30.dp.
val colors = listOf(
Color.Red,
Color.Blue,
Color.Green,
Color.Yellow
)
#Composable
fun ListTest() {
LazyColumn {
items(30) {
Column {
Text("This is row $it")
LazyRow {
items(colors) {
var isFocused by remember { mutableStateOf(false) }
val alpha = if (isFocused) 1f else 0.25f
val shadowColor = if (isFocused) Color.Black else Color.Transparent
Surface(
shape = RoundedCornerShape(8.dp),
color = it.copy(alpha = alpha),
modifier = Modifier
.width(240.dp)
.height(150.dp)
.padding(start = 16.dp)
// 🔴 Look here
.coloredShadow(shadowColor)
.onFocusChanged { state ->
isFocused = state.isFocused
}
.focusable(),
) {
// Content here
}
}
}
}
}
}
}
fun Modifier.coloredShadow(color: Color) = drawBehind {
val shadowColor = color.toArgb()
val transparentColor = color.copy(alpha = 0f).toArgb()
val offsetX = 0.dp
val offsetY = 8.dp
val cornerRadius = 4.dp
drawIntoCanvas {
val paint = Paint()
val frameworkPaint = paint.asFrameworkPaint()
frameworkPaint.color = transparentColor
frameworkPaint.setShadowLayer(
// 🔴 Set to 400.dp as radius
400.dp.toPx(),
offsetX.toPx(),
offsetY.toPx(),
shadowColor
)
it.drawRoundRect(
0f,
0f,
this.size.width,
this.size.height,
cornerRadius.toPx(),
cornerRadius.toPx(),
paint
)
}
}
How can I get rid of this clipping issue?
I don't think it's a clipping issue. You just have the elevation set too high, so the surface's shadow has to reach across another row/column, and display on top of another Surface, but because they're not children of the same view, it's not processing the shadow blurring properly.
Maybe you should try setting the elevation to 30dp or 24dp?

How to detect if two views(composables) are overlapping in jetpack compose?

I'm creating a 2D game with simple animations in jetpack compose. The game is simple, a balloon will float on top, a cannon will shoot arrow towards balloon. If the arrow hits balloon, player gets a point. I'm able to animate all of the above things but one. How would I detect when the arrow and balloon areas intersect?
The arrow is moving upwards with animation, also the balloon is animating side-ways. I want to capture the point at which the arrow touches the balloon. How would I proceed with this?
CODE:
Balloon Composable
#Composable
fun Balloon(modifier: Modifier, gameState: GameState) {
val localConfig = LocalConfiguration.current
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 80.dp),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenHeightDp.dp / 3),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = modifier
.offset(offsetX, offsetY)
.size(80.dp)
.background(Color.Red, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Text(text = "D!!")
}
}
Cannon Composable:
#Composable
fun ShootBalloon(modifier: Modifier) {
val localConfig = LocalConfiguration.current
var gameState by remember { mutableStateOf(GameState.START) }
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 50.dp),
infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (-(localConfig.screenHeightDp.dp - 50.dp)),
infiniteRepeatable(
animation = tween(500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.Start
) {
if (gameState == GameState.START) {
Button(onClick = { gameState = GameState.PLAYING }) { Text(text = "Play") }
}
Box(
modifier = Modifier
.size(50.dp)
.offset(x = offsetX)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_baseline_arrow_upward_24),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.offset(y = offsetY)
.onGloballyPositioned { layoutCoordinates ->
val offset = layoutCoordinates.positionInRoot()
}
)
}
}
}
I'm animating following things,
The cannon box moves side ways, the arrow image moves upwards.
The balloon has random movement.
Add a ticker and on each tick check the global coordinate of arrow and offsets of balloon. If values are same, it means they hit on another

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:

Reset offset animation on draggable item in Jetpack Compose

I have a green square that I can drag vertically. But whenever I stop dragging it, I want it to reset the offset to the start with an animation. I tried it like this, but I can't figure it out. Does someone know how to do it?
#Composable
fun DraggableSquare() {
var currentOffset by remember { mutableStateOf(0F) }
val resetAnimation by animateIntOffsetAsState(targetValue = IntOffset(0, currentOffset.roundToInt()))
var shouldReset = false
Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize()) {
Surface(
color = Color(0xFF34AB52),
modifier = Modifier
.size(100.dp)
.offset {
when {
shouldReset -> resetAnimation
else -> IntOffset(0, currentOffset.roundToInt())
}
}
.draggable(
state = rememberDraggableState { delta -> currentOffset += delta },
orientation = Orientation.Vertical,
onDragStopped = {
shouldReset = true
currentOffset = 0F
}
)
) {}
}
}
You can define the offset as an Animatable.
While dragging use the method snapTo to update the current value as the initial value and the onDragStopped to start the animation.
val coroutineScope = rememberCoroutineScope()
val offsetY = remember { Animatable(0f) }
Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize()) {
Surface(
color = Color(0xFF34AB52),
modifier = Modifier
.size(100.dp)
.offset {
IntOffset(0, offsetY.value.roundToInt())
}
.draggable(
state = rememberDraggableState { delta ->
coroutineScope.launch {
offsetY.snapTo(offsetY.value + delta)
}
},
orientation = Orientation.Vertical,
onDragStopped = {
coroutineScope.launch {
offsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 3000,
delayMillis = 0
)
)
}
}
)
) {
}
}

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