Skip to content

Instantly share code, notes, and snippets.

@ardakazanci
Created January 16, 2024 19:04
Show Gist options
  • Save ardakazanci/35de8e1ec6b26030e5ef6fe0ccc1eb2e to your computer and use it in GitHub Desktop.
Save ardakazanci/35de8e1ec6b26030e5ef6fe0ccc1eb2e to your computer and use it in GitHub Desktop.
Sticky Draggable Button for Jetpack Compose
@Composable
fun DraggableStickyButton() {
val context = LocalContext.current
val density = LocalDensity.current
val imageBitmap = remember {
val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.ball)
bitmap.asImageBitmap()
}
val screenWidthPx = with(density) { LocalConfiguration.current.screenWidthDp.dp.toPx() }
val screenHeightPx = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
val imageSize = 50.dp
val imageRadius = with(density) { imageSize.toPx() } / 2f
val stickinessThreshold = 100f
var targetPosition by remember {
mutableStateOf(
Offset(
screenWidthPx / 2, screenHeightPx / 2
)
)
}
var dragPosition by remember { mutableStateOf(targetPosition) }
var positionsList by remember { mutableStateOf(listOf<Offset>()) }
val animatedPosition by animateOffsetAsState(
targetValue = targetPosition, animationSpec = TweenSpec(durationMillis = 300), label = "Sample offset state"
)
var isRotating by remember { mutableStateOf(false) }
val rotationAngle by animateFloatAsState(
targetValue = if (isRotating) 360f else 0f, animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
), label = "Sample rotate animation state"
)
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(onDragStart = {
dragPosition = animatedPosition
positionsList = listOf(animatedPosition)
}, onDrag = { _, dragAmount ->
dragPosition = Offset(
x = (dragPosition.x + dragAmount.x).coerceIn(
imageRadius, screenWidthPx - imageRadius
), y = (dragPosition.y + dragAmount.y).coerceIn(
imageRadius, screenHeightPx - imageRadius
)
)
positionsList = positionsList + dragPosition
}, onDragEnd = {
targetPosition = dragPosition.calculateStickiness(
stickinessThreshold, screenWidthPx, screenHeightPx, imageRadius
)
})
}
.clickable {
isRotating = !isRotating
}
) {
positionsList.zipWithNext { a, b ->
val gradientColor = lerp(
Color.Green, Color.Red, (positionsList.indexOf(a).toFloat() / positionsList.size)
)
drawLine(gradientColor, a, b, strokeWidth = 4f)
}
withTransform({
rotate(rotationAngle, pivot = animatedPosition)
}) {
drawImage(
image = imageBitmap,
topLeft = animatedPosition - Offset(imageRadius, imageRadius),
alpha = 1.0f
)
}
}
}
fun Offset.calculateStickiness(
stickinessThreshold: Float, screenWidthPx: Float, screenHeightPx: Float, radius: Float
): Offset {
val xSticky = when {
abs(x - radius) < stickinessThreshold -> radius
abs(x - (screenWidthPx - radius)) < stickinessThreshold -> screenWidthPx - radius
else -> x
}
val ySticky = when {
abs(y - radius) < stickinessThreshold -> radius
abs(y - (screenHeightPx - radius)) < stickinessThreshold -> screenHeightPx - radius
else -> y
}
return Offset(xSticky, ySticky)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment