drawCircle( brush = Brush.sweepGradient( colors = listOf( Color.Green, Color.Cyan, Color.Red, Color.Blue, Color.Yellow, Color.Magenta, ), // Offset for this gradient is not at center, a little bit left of center center = Offset(2 * space + 2.7f * radius, canvasHeight / 2), ), radius = radius, center = Offset(2 * space + 3 * radius, canvasHeight / 2), )
drawCircle( brush = Brush.sweepGradient( colors = gradientColors, center = Offset(canvasWidth - space - radius, canvasHeight / 2), ), radius = radius, center = Offset(canvasWidth - space - radius, canvasHeight / 2) ) } }
@Composable funDrawNegativeArc() { var startAngle by remember { mutableStateOf(0f) } var sweepAngle by remember { mutableStateOf(60f) } var useCenter by remember { mutableStateOf(true) }
Canvas(modifier = canvasModifier) { val canvasWidth = size.width val canvasHeight = size.height
@Composable privatefunDrawMultipleArcs() { var startAngleBlue by remember { mutableStateOf(0f) } var sweepAngleBlue by remember { mutableStateOf(120f) }
var startAngleRed by remember { mutableStateOf(120f) } var sweepAngleRed by remember { mutableStateOf(120f) }
var startAngleGreen by remember { mutableStateOf(240f) } var sweepAngleGreen by remember { mutableStateOf(120f) }
var isFill by remember { mutableStateOf(true) }
Canvas(modifier = canvasModifier) { val canvasWidth = size.width val canvasHeight = size.height val arcHeight = canvasHeight - 20.dp.toPx() val arcStrokeWidth = 10.dp.toPx() val style = if (isFill) Fill else Stroke(arcStrokeWidth)
@Composable funDrawPath() { val path1 = remember { Path() } val path2 = remember { Path() }
Canvas(modifier = canvasModifier) { // Since we remember paths from each recomposition we reset them to have fresh ones // You can create paths here if you want to have new path instances path1.reset() path2.reset()
path1.moveTo(100f, 100f) // Draw a line from top right corner (100, 100) to (100,300) path1.lineTo(100f, 300f) // Draw a line from (100, 300) to (300,300) path1.lineTo(300f, 300f) // Draw a line from (300, 300) to (300,100) path1.lineTo(300f, 100f) // Draw a line from (300, 100) to (100,100) path1.lineTo(100f, 100f)
// Using relatives to draw blue path, relative is based on previous position of path path2.relativeMoveTo(100f, 100f) // Draw a line from (100,100) from (100, 300) path2.relativeLineTo(0f, 200f) // Draw a line from (100, 300) to (300,300) path2.relativeLineTo(200f, 0f) // Draw a line from (300, 300) to (300,100) path2.relativeLineTo(0f, -200f) // Draw a line from (300, 100) to (100,100) path2.relativeLineTo(-200f, 0f)
// Add rounded rectangle to path1 path1.addRoundRect( RoundRect( left = 400f, top = 200f, right = 600f, bottom = 400f, topLeftCornerRadius = CornerRadius(10f, 10f), topRightCornerRadius = CornerRadius(30f, 30f), bottomLeftCornerRadius = CornerRadius(50f, 20f), bottomRightCornerRadius = CornerRadius(0f, 0f) ) )
// Add rounded rectangle to path2 path2.addRoundRect( RoundRect( left = 700f, top = 200f, right = 900f, bottom = 400f, radiusX = 20f, radiusY = 20f ) )
path1.addOval(Rect(left = 400f, top = 50f, right = 500f, bottom = 150f)) path2.addArc( Rect(400f, top = 50f, right = 500f, bottom = 150f), startAngleDegrees = 0f, sweepAngleDegrees = 180f )
@Composable funDrawArcToPath() { val path1 = remember { Path() } val path2 = remember { Path() }
var startAngle by remember { mutableStateOf(0f) } var sweepAngle by remember { mutableStateOf(90f) }
Canvas(modifier = canvasModifier) { // Since we remember paths from each recomposition we reset them to have fresh ones // You can create paths here if you want to have new path instances path1.reset() path2.reset()
val rect = Rect(0f, 0f, size.width, size.height) path1.addRect(rect) path2.arcTo( rect, startAngleDegrees = startAngle, sweepAngleDegrees = sweepAngle, forceMoveTo = false )
var sides by remember { mutableStateOf(3f) } var cornerRadius by remember { mutableStateOf(1f) } val pathMeasure by remember { mutableStateOf(PathMeasure()) } var progress by remember { mutableStateOf(50f) }
val pathWithProgress by remember { mutableStateOf(Path()) }
Canvas(modifier = canvasModifier) { val canvasWidth = size.width val canvasHeight = size.height val cx = canvasWidth / 2 val cy = canvasHeight / 2 val radius = (canvasHeight - 20.dp.toPx()) / 2
@Composable funDrawQuad() { val density = LocalDensity.current.density
val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp
val screenWidthInPx = screenWidth.value * density
// (x0, y0) is initial coordinate where path is moved with path.moveTo(x0,y0) var x0 by remember { mutableStateOf(0f) } var y0 by remember { mutableStateOf(0f) }
/* Adds a quadratic bezier segment that curves from the current point(x0,y0) to the given point (x2, y2), using the control point (x1, y1). */ var x1 by remember { mutableStateOf(0f) } var y1 by remember { mutableStateOf(screenWidthInPx) } var x2 by remember { mutableStateOf(screenWidthInPx) } var y2 by remember { mutableStateOf(screenWidthInPx) }
// Draw Control Point on screen drawPoints( listOf(Offset(x1, y1)), color = Color.Green, pointMode = PointMode.Points, cap = StrokeCap.Round, strokeWidth = 40f ) }
@Composable funDrawCubic() { val density = LocalDensity.current.density
val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp
val screenWidthInPx = screenWidth.value * density
// (x0, y0) is initial coordinate where path is moved with path.moveTo(x0,y0) var x0 by remember { mutableStateOf(0f) } var y0 by remember { mutableStateOf(0f) }
/* Adds a cubic bezier segment that curves from the current point(x0,y0) to the given point (x3, y3), using the control points (x1, y1) and (x2, y2). */ var x1 by remember { mutableStateOf(0f) } var y1 by remember { mutableStateOf(screenWidthInPx) } var x2 by remember { mutableStateOf(screenWidthInPx) } var y2 by remember { mutableStateOf(0f) }
var x3 by remember { mutableStateOf(screenWidthInPx) } var y3 by remember { mutableStateOf(screenWidthInPx) }
// We apply operation to path1 and path2 and setting this new path to our newPath /* Set this path to the result of applying the Op to the two specified paths. The resulting path will be constructed from non-overlapping contours. The curve order is reduced where possible so that cubics may be turned into quadratics, and quadratics maybe turned into lines. */ newPath.op(path1, path2, operation = operation)
var sides1 by remember { mutableStateOf(5f) } var radius1 by remember { mutableStateOf(400f) }
var sides2 by remember { mutableStateOf(7f) } var radius2 by remember { mutableStateOf(300f) }
var clipOp by remember { mutableStateOf(ClipOp.Difference) }
Canvas(modifier = canvasModifier) { val canvasWidth = size.width val canvasHeight = size.height
val cx1 = canvasWidth / 3 val cx2 = canvasWidth * 2 / 3 val cy = canvasHeight / 2
val path1 = createPolygonPath(cx1, cy, sides1.roundToInt(), radius1) val path2 = createPolygonPath(cx2, cy, sides2.roundToInt(), radius2)
// Draw path1 to display it as reference, it's for demonstration drawPath( color = Color.Red, path = path1, style = Stroke( width = 2.dp.toPx(), pathEffect = PathEffect.dashPathEffect(floatArrayOf(40f, 20f)) ) )
// We apply clipPath operation to pah1 and draw after this operation /* Reduces the clip region to the intersection of the current clip and the given path. This method provides a callback to issue drawing commands within the region defined by the clipped path. After this method is invoked, this clip is no longer applied */ clipPath(path = path1, clipOp = clipOp) {
// Draw path1 to display it as reference, it's for demonstration drawPath( color = Color.Green, path = path1, style = Stroke( width = 2.dp.toPx(), pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 40f)) ) )
// Anything inside this scope will be clipped according to path1 shape drawRect( color = Color.Yellow, topLeft = Offset(100f, 100f), size = Size(canvasWidth - 300f, canvasHeight - 300f) )
/* Reduces the clip region to the intersection of the current clip and the given rectangle indicated by the given left, top, right and bottom bounds. This provides a callback to issue drawing commands within the clipped region. After this method is invoked, this clip is no longer applied. */ clipRect(left = 100f, top = 80f, right = 700f, bottom = 400f, clipOp = clipOp) {
@Composable funDrawPath() { val path = remember { Path() }
var displaySegmentStart by remember { mutableStateOf(true) } var displaySegmentEnd by remember { mutableStateOf(true) }
Canvas(modifier = canvasModifier) { // Since we remember paths from each recomposition we reset them to have fresh ones // You can create paths here if you want to have new path instances path.reset()
// Draw Rectangle path.moveTo(100f, 100f) // Draw a line from top right corner (100, 100) to (100,300) path.lineTo(100f, 300f) // Draw a line from (100, 300) to (300,300) path.lineTo(300f, 300f) // Draw a line from (300, 300) to (300,100) path.lineTo(300f, 100f) // Draw a line from (300, 100) to (100,100) path.lineTo(100f, 100f)
// Add rounded rectangle to path path.addRoundRect( RoundRect( left = 400f, top = 200f, right = 600f, bottom = 400f, topLeftCornerRadius = CornerRadius(10f, 10f), topRightCornerRadius = CornerRadius(30f, 30f), bottomLeftCornerRadius = CornerRadius(50f, 20f), bottomRightCornerRadius = CornerRadius(0f, 0f) ) )
// Add rounded rectangle to path path.addRoundRect( RoundRect( left = 700f, top = 200f, right = 900f, bottom = 400f, radiusX = 20f, radiusY = 20f ) )
path.addOval(Rect(left = 400f, top = 50f, right = 500f, bottom = 150f))
var cornerRadius by remember { mutableStateOf(20f) }
val pathEffect = PathEffect.cornerPathEffect(cornerRadius) DrawRect(pathEffect)
Text(text = "cornerRadius ${cornerRadius.roundToInt()}") Slider( value = cornerRadius, onValueChange = { cornerRadius = it }, valueRange = 0f..100f, ) }
@Composable privatefunChainPathEffectExample() {
var onInterval1 by remember { mutableStateOf(20f) } var offInterval1 by remember { mutableStateOf(20f) } var phase1 by remember { mutableStateOf(10f) }
var cornerRadius by remember { mutableStateOf(20f) }
@Composable funDrawImageExample() { val bitmap = ImageBitmap.imageResource(id = R.drawable.landscape1) var srcOffsetX by remember { mutableStateOf(0) } var srcOffsetY by remember { mutableStateOf(0) } var srcWidth by remember { mutableStateOf(1080) } var srcHeight by remember { mutableStateOf(1080) }
var dstOffsetX by remember { mutableStateOf(0) } var dstOffsetY by remember { mutableStateOf(0) } var dstWidth by remember { mutableStateOf(1080) } var dstHeight by remember { mutableStateOf(1080) }
@Composable privatefunDrawShapeBlendMode() { var selectedIndex by remember { mutableStateOf(3) } var blendMode: BlendMode by remember { mutableStateOf(BlendMode.SrcOver) } var showDstColorDialog by remember { mutableStateOf(false) } var showSrcColorDialog by remember { mutableStateOf(false) }
Canvas(modifier = canvasModifier) { val canvasWidth = size.width val canvasHeight = size.height val radius = canvasHeight / 2 - 100
val horizontalOffset = 70f val verticalOffset = 50f
val cx = canvasWidth / 2 - horizontalOffset val cy = canvasHeight / 2 + verticalOffset val srcPath = createPolygonPath(cx, cy, 5, radius)
with(drawContext.canvas.nativeCanvas) { val checkPoint = saveLayer(null, null)
/** * Draw into a [Canvas] behind the modified content. */ fun Modifier.drawBehind( onDraw: DrawScope.() -> Unit ) = this.then( DrawBackgroundModifier( onDraw = onDraw, inspectorInfo = debugInspectorInfo { name = "drawBehind" properties["onDraw"] = onDraw } ) )
privateclassDrawBackgroundModifier( val onDraw: DrawScope.() -> Unit, inspectorInfo: InspectorInfo.() -> Unit ) : DrawModifier, InspectorValueInfo(inspectorInfo) {
@Composable funRotateLabelExample() { val url1 = "https://www.techtoyreviews.com/wp-content/uploads/2020/09/5152094_Cover_PS5.jpg" val url2 = "https://i02.appmifile.com/images/2019/06/03/03ab1861-42fe-4137-b7df-2840d9d3a7f5.png" val context = LocalContext.current
Column(Modifier.background(Color(0xffECEFF1)).fillMaxSize().padding(20.dp)) { val painter1 = rememberAsyncImagePainter( ImageRequest.Builder(context).data(url1).size(coil.size.Size.ORIGINAL).build() )
val modifier1 = if (painter1.state is AsyncImagePainter.State.Success) { Modifier.drawDiagonalLabel( text = "50% OFF", color = Color.Red, labelTextRatio = 5f, showShimmer = false ) } else Modifier
val painter2 = rememberAsyncImagePainter( ImageRequest.Builder(context).data(url2).size(coil.size.Size.ORIGINAL).build() )
val modifier2 = if (painter2.state is AsyncImagePainter.State.Success) { Modifier.drawDiagonalLabel( text = "40% OFF", color = Color(0xff4CAF50), labelTextRatio = 5f ) } else Modifier
@Composable funDragCanvasMotionEventsExample() { var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
val canvasText = remember { StringBuilder() } val gestureText = remember { StringBuilder().apply { append("Touch Canvas above to display motion events") } }
// This is our motion event we get from touch motion var currentPosition by remember { mutableStateOf(Offset.Zero) } val sdf = remember { SimpleDateFormat("mm:ss.SSS", Locale.ROOT) }
Text( modifier = gestureTextModifier.verticalScroll(rememberScrollState()), text = gestureText.toString(), color = Color.White, ) }
privatefun DrawScope.drawText(text: String, x: Float, y: Float, paint: Paint) { val lines = text.split("\n") // 🔥🔥 There is not a built-in function as of 1.0.0 // for drawing text so we get the native canvas to draw text and use a Paint object val nativeCanvas = drawContext.canvas.nativeCanvas lines.indices.withIndex().forEach { (posY, i) -> nativeCanvas.drawText(lines[i], x, posY * 40 + y, paint) } }
@Composable funPointerInterOpFilterCanvasMotionEventsExample() { var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
val canvasText = remember { StringBuilder() } val gestureText = remember { StringBuilder().apply { append("Touch Canvas above to display motion events") } }
// This is our motion event we get from touch motion var currentPosition by remember { mutableStateOf(Offset.Zero) } val sdf = remember { SimpleDateFormat("mm:ss.SSS", Locale.ROOT) }
val requestDisallowInterceptTouchEvent = RequestDisallowInterceptTouchEvent() // 🔥 Requests other touch events like scrolling to not intercept this event // If this is not set to true scrolling stops pointerInteropFilter getting move events requestDisallowInterceptTouchEvent(true)
@Composable funAwaitPointerEventCanvasStateExample() { var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
val canvasText = remember { StringBuilder() } val gestureText = remember { StringBuilder().apply { append("Touch Canvas above to display motion events") } }
// This is our motion event we get from touch motion var currentPosition by remember { mutableStateOf(Offset.Zero) } val sdf = remember { SimpleDateFormat("mm:ss.SSS", Locale.ROOT) }
val drawModifier = canvasModifier .background(Color.White) .pointerInput(Unit) { awaitEachGesture { // Wait for at least one pointer to press down, and set first contact position val down: PointerInputChange = awaitFirstDown() currentPosition = down.position motionEvent = MotionEvent.Down gestureText.clear() gestureText.append("🔥 MotionEvent.Down time: ${sdf.format(System.currentTimeMillis())}\n") // Main pointer is the one that is down initially var pointerId = down.id while (true) { val event: PointerEvent = awaitPointerEvent() val anyPressed = event.changes.any { it.pressed } if (anyPressed) { // Get pointer that is down, if first pointer is up // get another and use it if other pointers are also down // event.changes.first() doesn't return same order val pointerInputChange = event.changes.firstOrNull { it.id == pointerId } ?: event.changes.first()
// Next time will check same pointer with this id pointerId = pointerInputChange.id
// This necessary to prevent other gestures or scrolling // when at least one pointer is down on canvas to draw pointerInputChange.consume() } else { // All of the pointers are up motionEvent = MotionEvent.Up gestureText.append("🔥🔥🔥 MotionEvent.Up time: ${sdf.format(System.currentTimeMillis())}\n") break } } } }
@Composable funAwaitPointerEventWithDelayCanvasStateExample() { var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
val canvasText = remember { StringBuilder() } val gestureText = remember { StringBuilder().apply { append("Touch Canvas above to display motion events") } }
// This is our motion event we get from touch motion var currentPosition by remember { mutableStateOf(Offset.Zero) } val sdf = remember { SimpleDateFormat("mm:ss.SSS", Locale.ROOT) } // 🔥 This coroutineScope is used for adding delay after first down event val scope = rememberCoroutineScope()
val drawModifier = canvasModifier .background(Color.White) .pointerInput(Unit) { awaitEachGesture { var waitedAfterDown = false
// Wait for at least one pointer to press down, and set first contact position val down: PointerInputChange = awaitFirstDown()
// 🔥 Without this delay Canvas misses down event scope.launch { delay(20) waitedAfterDown = true } // Main pointer is the one that is down initially var pointerId = down.id while (true) { val event: PointerEvent = awaitPointerEvent() val anyPressed = event.changes.any { it.pressed } if (anyPressed) { // Get pointer that is down, if first pointer is up // get another and use it if other pointers are also down // event.changes.first() doesn't return same order val pointerInputChange = event.changes.firstOrNull { it.id == pointerId } ?: event.changes.first() // Next time will check same pointer with this id pointerId = pointerInputChange.id if (waitedAfterDown) { currentPosition = pointerInputChange.position motionEvent = MotionEvent.Move } gestureText.append("🔥🔥 MotionEvent.Move time: ${sdf.format(System.currentTimeMillis())}\n") // This necessary to prevent other gestures or scrolling // when at least one pointer is down on canvas to draw pointerInputChange.consume() } else { // All of the pointers are up motionEvent = MotionEvent.Up gestureText.append("🔥🔥🔥 MotionEvent.Up time: ${sdf.format(System.currentTimeMillis())}\n") break } } } }
fun Modifier.pointerMotionEvents( key1: Any? = Unit, onDown: (PointerInputChange) -> Unit = {}, onMove: (PointerInputChange) -> Unit = {}, onUp: (PointerInputChange) -> Unit = {}, delayAfterDownInMillis: Long = 0L ) = this.then( Modifier.pointerInput(key1) { detectMotionEvents(onDown, onMove, onUp, delayAfterDownInMillis) } )
suspendfun PointerInputScope.detectMotionEvents( onDown: (PointerInputChange) -> Unit = {}, onMove: (PointerInputChange) -> Unit = {}, onUp: (PointerInputChange) -> Unit = {}, delayAfterDownInMillis: Long = 0L ) { coroutineScope { awaitEachGesture { // Wait for at least one pointer to press down, and set first contact position val down: PointerInputChange = awaitFirstDown() onDown(down) var pointer = down // Main pointer is the one that is down initially var pointerId = down.id // If a move event is followed fast enough down is skipped, especially by Canvas // to prevent it we add delay after first touch var waitedAfterDown = false launch { delay(delayAfterDownInMillis) waitedAfterDown = true } while (true) { val event: PointerEvent = awaitPointerEvent() val anyPressed = event.changes.any { it.pressed } // There are at least one pointer pressed if (anyPressed) { // Get pointer that is down, if first pointer is up // get another and use it if other pointers are also down // event.changes.first() doesn't return same order val pointerInputChange = event.changes.firstOrNull { it.id == pointerId } ?: event.changes.first() // Next time will check same pointer with this id pointerId = pointerInputChange.id pointer = pointerInputChange if (waitedAfterDown) { onMove(pointer) } } else { // All of the pointers are up onUp(pointer) break } } } } }
@Composable funTouchDrawWithCustomGestureModifierExample() { 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) } // Path is what is used for drawing line on Canvas val path = remember { Path() } // color and text are for debugging and observing state changes and position var gestureColor by remember { mutableStateOf(Color.White) } // Draw state on canvas as text when set to true val debug = false // This text is drawn to Canvas val canvasText = remember { StringBuilder() } val paint = remember { Paint().apply { textSize = 40f color = Color.Black.toArgb() } }
drawPath( color = Color.Red, path = path, style = Stroke(width = 4.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round) )
if (debug) { drawText(text = canvasText.toString(), x = 0f, y = 60f, paint) } } } privateval canvasModifier = Modifier .padding(8.dp) .shadow(1.dp) .fillMaxWidth() .height(300.dp) .clipToBounds()
privatefun DrawScope.drawText(text: String, x: Float, y: Float, paint: Paint) { val lines = text.split("\n") // 🔥🔥 There is not a built-in function as of 1.0.0 // for drawing text so we get the native canvas to draw text and use a Paint object val nativeCanvas = drawContext.canvas.nativeCanvas lines.indices.withIndex().forEach { (posY, i) -> nativeCanvas.drawText(lines[i], x, posY * 40 + y, paint) } }
@Composable privatefunTouchDrawWithDragGesture() { val path = remember { Path() } var motionEvent by remember { mutableStateOf(MotionEvent.Idle) } var currentPosition by remember { mutableStateOf(Offset.Unspecified) } // color and text are for debugging and observing state changes and position var gestureColor by remember { mutableStateOf(Color.White) } // Draw state on canvas as text when set to true val debug = false // This text is drawn to Canvas val canvasText = remember { StringBuilder() } val paint = remember { Paint().apply { textSize = 40f color = Color.Black.toArgb() } } val drawModifier = canvasModifier .background(gestureColor) .pointerInput(Unit) { awaitEachGesture { val down: PointerInputChange = awaitFirstDown().also { motionEvent = MotionEvent.Down currentPosition = it.position gestureColor = Blue400 } // 🔥 Waits for drag threshold to be passed by pointer // or it returns null if up event is triggered val change: PointerInputChange? = awaitTouchSlopOrCancellation(down.id) { change: PointerInputChange, over: Offset -> change.consume() gestureColor = Brown400 } if (change != null) { // ✏️ Alternative 1 // 🔥 Calls awaitDragOrCancellation(pointer) in a while loop drag(change.id) { pointerInputChange: PointerInputChange -> gestureColor = Green400 motionEvent = MotionEvent.Move currentPosition = pointerInputChange.position pointerInputChange.consume() }
// ✏️ Alternative 2 // while (change != null && change.pressed) { // // // 🔥 Calls awaitPointerEvent() in a while loop and checks drag change // change = awaitDragOrCancellation(change.id) // // if (change != null && !change.changedToUpIgnoreConsumed()) { // gestureColor = Green400 // motionEvent = MotionEvent.Move // currentPosition = change.position // change.consume() // } // } // All of the pointers are up motionEvent = MotionEvent.Up gestureColor = Color.White } else { // Drag threshold is not passed and last pointer is up gestureColor = Yellow400 motionEvent = MotionEvent.Up } } }
fun Modifier.dragMotionEvent( onDragStart: (PointerInputChange) -> Unit = {}, onDrag: (PointerInputChange) -> Unit = {}, onDragEnd: (PointerInputChange) -> Unit = {} ) = this.then( Modifier.pointerInput(Unit) { awaitEachGesture { awaitDragMotionEvent(onDragStart, onDrag, onDragEnd) } } )
suspendfun AwaitPointerEventScope.awaitDragMotionEvent( onDragStart: (PointerInputChange) -> Unit = {}, onDrag: (PointerInputChange) -> Unit = {}, onDragEnd: (PointerInputChange) -> Unit = {} ) { // Wait for at least one pointer to press down, and set first contact position val down: PointerInputChange = awaitFirstDown() onDragStart(down)
var pointer = down
// 🔥 Waits for drag threshold to be passed by pointer // or it returns null if up event is triggered val change: PointerInputChange? = awaitTouchSlopOrCancellation(down.id) { change: PointerInputChange, over: Offset -> // 🔥🔥 If consume() is not called drag does not // function properly. // Consuming position change causes change.positionChanged() to return false. change.consume() }
if (change != null) { // 🔥 Calls awaitDragOrCancellation(pointer) in a while loop drag(change.id) { pointerInputChange: PointerInputChange -> pointer = pointerInputChange onDrag(pointer) }
// All of the pointers are up onDragEnd(pointer) } else { // Drag threshold is not passed(awaitTouchSlopOrCancellation is NULL) and last pointer is up onDragEnd(pointer) } }
@Composable privatefunTouchDrawWithPropertiesAndEraseExample() { val context = LocalContext.current // Path used for drawing val drawPath = remember { Path() } // 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) }
var eraseMode by remember { mutableStateOf(false) } val pathOption = rememberPathOption()
@Composable privatefunTouchDrawPathSegmentsExample() { val path = remember { Path() } var motionEvent by remember { mutableStateOf(MotionEvent.Idle) } var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
var displaySegmentStart by remember { mutableStateOf(true) } var displaySegmentEnd by remember { mutableStateOf(true) }
@Composable privatefunTouchDrawWithMovablePathExample() { val context = LocalContext.current // Path used for drawing val drawPath = remember { Path() } // Path used for erasing. In this example erasing is faked by drawing with canvas color // above draw path. val erasePath = remember { Path() } // Canvas touch state. Idle by default, Down at first contact, Move while dragging and UP // when first pointer is up 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) } var drawMode by remember { mutableStateOf(DrawMode.Draw) } val pathOption = rememberPathOption() // Check if path is touched in Touch Mode var isPathTouched by remember { mutableStateOf(false) }
if (drawMode == DrawMode.Touch) { val rect = Rect(currentPosition, 25f) val segments: Iterable<PathSegment> = drawPath.asAndroidPath().flatten() segments.forEach { pathSegment: PathSegment -> val start = pathSegment.start val end = pathSegment.end if (!isPathTouched && (rect.contains(Offset(start.x, start.y)) || rect.contains(Offset(end.x, end.y))) ) { isPathTouched = true return@forEach } } } }, onDrag = { pointerInputChange -> motionEvent = MotionEvent.Move currentPosition = pointerInputChange.position if (drawMode == DrawMode.Touch && isPathTouched) { // Move draw and erase paths as much as the distance that // the pointer has moved on the screen minus any distance // that has been consumed. drawPath.translate(pointerInputChange.positionChange()) erasePath.translate(pointerInputChange.positionChange()) } pointerInputChange.consume() }, onDragEnd = { pointerInputChange -> motionEvent = MotionEvent.Up isPathTouched = false pointerInputChange.consume() } )
Canvas(modifier = drawModifier) { // Draw or erase depending on erase mode is active or not val currentPath = if (drawMode == DrawMode.Erase) erasePath else drawPath when (motionEvent) { MotionEvent.Down -> { if (drawMode != DrawMode.Touch) { currentPath.moveTo(currentPosition.x, currentPosition.y) } previousPosition = currentPosition } MotionEvent.Move -> { if (drawMode != DrawMode.Touch) { currentPath.quadraticBezierTo( previousPosition.x, previousPosition.y, (previousPosition.x + currentPosition.x) / 2, (previousPosition.y + currentPosition.y) / 2
@Composable funEraseBitmapSample() { val imageBitmap = ImageBitmap.imageResource(R.drawable.landscape5) .asAndroidBitmap().copy(Bitmap.Config.ARGB_8888, true).asImageBitmap() val aspectRatio = imageBitmap.width / imageBitmap.height.toFloat() val modifier = Modifier.fillMaxWidth().aspectRatio(aspectRatio) 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
// 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() }
@Composable privatefunOffsetAndTranslationExample() { var offset by remember { mutableStateOf(0f) } var tips by remember { mutableStateOf("") } var showTips by remember { mutableStateOf(false) }
@Composable privatefunGraphicsLayerExample() { val context = LocalContext.current var offsetX by remember { mutableStateOf(0f) } var scale by remember { mutableStateOf(1f) } var imageSize by remember { mutableStateOf("") } var globallyPosition by remember { mutableStateOf("") }
@Composable privatefunTransformOriginExample() { val context = LocalContext.current var angleX by remember { mutableStateOf(0f) } var angleY by remember { mutableStateOf(0f) } var angleZ by remember { mutableStateOf(0f) }
var pivotFractionX by remember { mutableStateOf(0.5f) } var pivotFractionY by remember { mutableStateOf(0.5f) }