|
@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) |
|
} |