Skip to content

Instantly share code, notes, and snippets.

@equationl
Last active January 28, 2024 04:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save equationl/adb91edf141ac63ec62925534c784428 to your computer and use it in GitHub Desktop.
Save equationl/adb91edf141ac63ec62925534c784428 to your computer and use it in GitHub Desktop.
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.consumePositionChange
import androidx.compose.ui.input.pointer.pointerInput
import com.equationl.blurdraw.gesture.MotionEvent
/**
* @author SmartToolFactory
*
* from: https://github.com/SmartToolFactory/Compose-Drawing-App
* */
suspend fun AwaitPointerEventScope.awaitDragMotionEvent(
onTouchEvent: (MotionEvent, PointerInputChange) -> Unit
) {
// Wait for at least one pointer to press down, and set first contact position
val down: PointerInputChange = awaitFirstDown()
onTouchEvent(MotionEvent.Down, 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 consumePositionChange() is not consumed drag does not
// function properly.
// Consuming position change causes change.positionChanged() to return false.
change.consumePositionChange()
}
if (change != null) {
// 🔥 Calls awaitDragOrCancellation(pointer) in a while loop
drag(change.id) { pointerInputChange: PointerInputChange ->
pointer = pointerInputChange
onTouchEvent(MotionEvent.Move, pointer)
}
// All of the pointers are up
onTouchEvent(MotionEvent.Up, pointer)
} else {
// Drag threshold is not passed and last pointer is up
onTouchEvent(MotionEvent.Up, pointer)
}
}
fun Modifier.dragMotionEvent(onTouchEvent: (MotionEvent, PointerInputChange) -> Unit) = this.then(
Modifier.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitDragMotionEvent(onTouchEvent)
}
}
}
)
suspend fun 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 consumePositionChange() is not consumed drag does not
// function properly.
// Consuming position change causes change.positionChanged() to return false.
change.consumePositionChange()
}
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 and last pointer is up
onDragEnd(pointer)
}
}
fun Modifier.dragMotionEvent(
onDragStart: (PointerInputChange) -> Unit = {},
onDrag: (PointerInputChange) -> Unit = {},
onDragEnd: (PointerInputChange) -> Unit = {}
) = this.then(
Modifier.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitDragMotionEvent(onDragStart, onDrag, onDragEnd)
}
}
}
)
enum class MotionEvent {
Idle, Down, Move, Up
}
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.consumePositionChange
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.equationl.blurdraw.gesture.MotionEvent
import com.equationl.blurdraw.gesture.dragMotionEvent
@Composable
fun BlurView(
paintWidth: Dp = 15.dp, blurRadius: Dp = 10.dp, content: @Composable () -> Unit,
) {
var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }
Box(
modifier = Modifier.dragMotionEvent(
onDragStart = { pointerInputChange ->
motionEvent = MotionEvent.Down
currentPosition = pointerInputChange.position
pointerInputChange.consumeDownChange()
},
onDrag = { pointerInputChange ->
motionEvent = MotionEvent.Move
currentPosition = pointerInputChange.position
pointerInputChange.consumePositionChange()
},
onDragEnd = { pointerInputChange ->
motionEvent = MotionEvent.Up
pointerInputChange.consumeDownChange()
},
),
) {
Column {
content()
}
Column(
modifier = Modifier
.fillMaxSize()
.blur(blurRadius)
.drawWithCache {
val currentPath = Path()
onDrawWithContent {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
when (motionEvent) {
MotionEvent.Down -> {
currentPath.moveTo(currentPosition.x, currentPosition.y)
previousPosition = currentPosition
}
MotionEvent.Move -> {
currentPath.quadraticBezierTo(
previousPosition.x,
previousPosition.y,
(previousPosition.x + currentPosition.x) / 2,
(previousPosition.y + currentPosition.y) / 2
)
previousPosition = currentPosition
}
MotionEvent.Up -> {
currentPath.lineTo(currentPosition.x, currentPosition.y)
currentPosition = Offset.Unspecified
previousPosition = currentPosition
motionEvent = MotionEvent.Idle
}
else -> Unit
}
drawContent()
drawPath(
currentPath, color = Color.White, alpha = 1f,
style = Stroke(
width = paintWidth.toPx(),
),
blendMode = BlendMode.DstOut,
)
restoreToCount(checkPoint)
}
}
},
) {
content()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment