feat: graffiti

This commit is contained in:
cgspine
2022-03-03 13:15:12 +08:00
parent 94359efe3f
commit 40522fff17
3 changed files with 334 additions and 21 deletions
@@ -69,7 +69,20 @@ class QMUIPhotoPickerConfig(
val bucketChooserBgColor: Color = topBarBgColor,
val bucketChooserIndicationColor: Color = Color.White.copy(alpha = 0.2f),
val bucketChooserMainTextColor: Color = Color.White,
val bucketChooserCountTextColor: Color = Color.White.copy(alpha = 0.64f)
val bucketChooserCountTextColor: Color = Color.White.copy(alpha = 0.64f),
val paintEditOptions: List<PaintEdit> = listOf(
PaintMosaic(2),
PaintMosaic(1),
PaintGraffiti(Color.White),
PaintGraffiti(Color.Black),
PaintGraffiti(Color.Red),
PaintGraffiti(Color.Yellow),
PaintGraffiti(Color.Green),
PaintGraffiti(Color.Blue),
PaintGraffiti(Color.Magenta)
),
val paintEditStrokeWidth: Dp = 5.dp
)
val qmuiPhotoPickerDefaultConfig by lazy { QMUIPhotoPickerConfig() }
@@ -1,40 +1,105 @@
package com.qmuiteam.photo.compose.picker
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.*
import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.consumePositionChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ChainStyle
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.Visibility
import androidx.core.view.WindowInsetsCompat
import com.qmuiteam.compose.core.R
import com.qmuiteam.compose.core.provider.QMUILocalWindowInsets
import com.qmuiteam.compose.core.provider.dp
import com.qmuiteam.photo.compose.QMUIGesturePhoto
import com.qmuiteam.photo.data.QMUIMediaPhotoVO
import kotlinx.coroutines.coroutineScope
private enum class QMUIPhotoPickerEditScene {
normal, paint, text, clip
}
private class QMUIPhotoPickerPhotoInfo(
var scale: Float,
var rect: Rect?,
var drawable: Drawable?,
private class MutablePickerPhotoInfo(
var drawable: Drawable?
)
private data class PickerPhotoLayoutInfo(var scale: Float, var rect: Rect)
sealed class PaintEdit {
@Composable
abstract fun Compose(size: Dp, selected: Boolean, onClick: () -> Unit)
}
class PaintMosaic(val level: Int) : PaintEdit() {
@Composable
override fun Compose(size: Dp, selected: Boolean, onClick: () -> Unit) {
val ringWidth = with(LocalDensity.current) {
2.dp.toPx()
}
Canvas(modifier = Modifier.size(size)) {
drawCircle(
Color.White,
radius = this.size.minDimension / 2 - if (selected) 0f else ringWidth
)
drawCircle(
Color.Black,
radius = this.size.minDimension / 2 - ringWidth * 2
)
}
}
}
class PaintGraffiti(val color: Color) : PaintEdit() {
@Composable
override fun Compose(size: Dp, selected: Boolean, onClick: () -> Unit) {
val ringWidth = with(LocalDensity.current) {
2.dp.toPx()
}
Canvas(modifier = Modifier
.size(size)
.clickable(
interactionSource = remember {
MutableInteractionSource()
},
indication = null
) {
onClick()
}) {
drawCircle(
Color.White,
radius = this.size.minDimension / 2 - if (selected) 0f else ringWidth
)
drawCircle(
color,
radius = this.size.minDimension / 2 - ringWidth * 2
)
}
}
}
@Composable
fun QMUIPhotoPickerEdit(
onBackPressedDispatcher: OnBackPressedDispatcher,
@@ -45,9 +110,18 @@ fun QMUIPhotoPickerEdit(
mutableStateOf(QMUIPhotoPickerEditScene.normal)
}
val photoInfo = remember(data) {
QMUIPhotoPickerPhotoInfo(1f, null, null)
MutablePickerPhotoInfo(null)
}
var photoLayoutInfo by remember(data) {
mutableStateOf(PickerPhotoLayoutInfo(1f, Rect.Zero))
}
val editLayers = remember(data) {
mutableStateListOf<EditLayer>()
}
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
QMUIGesturePhoto(
containerWidth = maxWidth,
@@ -63,13 +137,15 @@ fun QMUIPhotoPickerEdit(
}
) { _, scale, rect, onImageRatioEnsured ->
photoInfo.scale = scale
photoInfo.rect = rect
photoLayoutInfo = PickerPhotoLayoutInfo(scale, rect)
QMUIPhotoPickerEditPhotoContent(data) {
photoInfo.drawable = it
onImageRatioEnsured(it.intrinsicWidth.toFloat() / it.intrinsicHeight)
}
}
QMUIPhotoEditHistoryList(photoLayoutInfo, editLayers)
AnimatedVisibility(
visible = scene == QMUIPhotoPickerEditScene.normal || scene == QMUIPhotoPickerEditScene.paint,
enter = fadeIn(),
@@ -77,10 +153,11 @@ fun QMUIPhotoPickerEdit(
) {
QMUIPhotoPickerEditPaint(
paintState = scene == QMUIPhotoPickerEditScene.paint,
photoInfo = photoInfo,
editLayers = editLayers,
layoutInfo = photoLayoutInfo,
onBack = onBack,
onPaintClick = {
scene = if(it) QMUIPhotoPickerEditScene.paint else QMUIPhotoPickerEditScene.normal
scene = if (it) QMUIPhotoPickerEditScene.paint else QMUIPhotoPickerEditScene.normal
},
onTextClick = {
scene = QMUIPhotoPickerEditScene.text
@@ -88,8 +165,11 @@ fun QMUIPhotoPickerEdit(
onClipClick = {
scene = QMUIPhotoPickerEditScene.clip
},
onFinishPaintLayer = {
editLayers.add(it)
},
onEnsureClick = {
// TODO
}
)
}
@@ -99,11 +179,13 @@ fun QMUIPhotoPickerEdit(
@Composable
private fun QMUIPhotoPickerEditPaint(
paintState: Boolean,
photoInfo: QMUIPhotoPickerPhotoInfo,
editLayers: List<EditLayer>,
layoutInfo: PickerPhotoLayoutInfo,
onBack: () -> Unit,
onPaintClick: (toPaint: Boolean) -> Unit,
onTextClick: () -> Unit,
onClipClick: () -> Unit,
onFinishPaintLayer: (PaintEditLayer) -> Unit,
onEnsureClick: () -> Unit
) {
val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility(
@@ -112,11 +194,46 @@ private fun QMUIPhotoPickerEditPaint(
WindowInsetsCompat.Type.displayCutout()
).dp()
val paintEditOptions = QMUILocalPickerConfig.current.paintEditOptions
var paintEditCurrentIndex by remember {
mutableStateOf(4)
}
if (paintEditCurrentIndex >= paintEditOptions.size) {
paintEditCurrentIndex = paintEditOptions.size - 1
}
var showTools by remember {
mutableStateOf(true)
}
ConstraintLayout(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier
.fillMaxSize()
.constrainAs(createRef()) {
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
visibility = if (paintState) Visibility.Visible else Visibility.Gone
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}) {
QMUIPhotoPaintCanvas(
paintEditOptions[paintEditCurrentIndex],
layoutInfo,
editLayers,
onTouchBegin = {
showTools = false
},
onTouchEnd = {
showTools = true
onFinishPaintLayer(it)
}
)
}
AnimatedVisibility(
visible = showTools,
modifier = Modifier.constrainAs(createRef()) {
@@ -207,7 +324,13 @@ private fun QMUIPhotoPickerEditPaint(
enter = fadeIn(),
exit = fadeOut()
) {
Text("TODO", color = Color.White)
QMUIPhotoPickerEditPaintOptions(
paintEditOptions,
24.dp,
paintEditCurrentIndex
) {
paintEditCurrentIndex = it
}
}
}
}
@@ -236,12 +359,141 @@ fun QMUIPhotoPickerEditPhotoContent(
}
@Composable
internal fun QMUIPhotoPickerEditPaintChooser(){
private fun QMUIPhotoEditHistoryList(
layoutInfo: PickerPhotoLayoutInfo,
editLayers: List<EditLayer>
) {
if (layoutInfo.rect == Rect.Zero) {
return
}
val (w, h) = with(LocalDensity.current) {
arrayOf(
layoutInfo.rect.width.toDp(),
layoutInfo.rect.height.toDp()
)
}
Canvas(modifier = Modifier
.width(w / layoutInfo.scale)
.height(h / layoutInfo.scale)
.graphicsLayer {
this.transformOrigin = TransformOrigin(0f, 0f)
this.translationX = layoutInfo.rect.left
this.translationY = layoutInfo.rect.top
this.scaleX = layoutInfo.scale
this.scaleY = layoutInfo.scale
this.clip = true
}) {
editLayers.forEach {
with(it) {
draw()
}
}
}
}
@Composable
internal fun QMUIPhotoPickerEditToolBar(
private fun QMUIPhotoPaintCanvas(
paintEdit: PaintEdit,
layoutInfo: PickerPhotoLayoutInfo,
editLayers: List<EditLayer>,
onTouchBegin: () -> Unit,
onTouchEnd: (PaintEditLayer) -> Unit
) {
val (w, h) = with(LocalDensity.current) {
arrayOf(
layoutInfo.rect.width.toDp(),
layoutInfo.rect.height.toDp()
)
}
val strokeWidth = with(LocalDensity.current) {
QMUILocalPickerConfig.current.paintEditStrokeWidth.toPx()
}
val currentLayerState = remember(editLayers, paintEdit, layoutInfo) {
val layer = if (paintEdit is PaintGraffiti) {
GraffitiEditLayer(Path(), paintEdit.color, strokeWidth / layoutInfo.scale)
} else {
MosaicEditLayer(Path())
}
mutableStateOf(layer, neverEqualPolicy())
}
val currentLayer = currentLayerState.value
Canvas(modifier = Modifier
.width(w / layoutInfo.scale)
.height(h / layoutInfo.scale)
.graphicsLayer {
this.transformOrigin = TransformOrigin(0f, 0f)
this.translationX = layoutInfo.rect.left
this.translationY = layoutInfo.rect.top
this.scaleX = layoutInfo.scale
this.scaleY = layoutInfo.scale
this.clip = true
}) {
with(currentLayer) {
draw()
}
}
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(editLayers, paintEdit, layoutInfo) {
coroutineScope {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = true)
down.consumeDownChange()
currentLayer.path.moveTo(
(down.position.x - layoutInfo.rect.left) / layoutInfo.scale,
(down.position.y - layoutInfo.rect.top) / layoutInfo.scale
)
onTouchBegin()
do {
val event = awaitPointerEvent()
val change = event.changes.find { it.id.value == down.id.value }
if (change != null) {
change.consumePositionChange()
currentLayer.path.lineTo(
(change.position.x - layoutInfo.rect.left) / layoutInfo.scale,
(change.position.y - layoutInfo.rect.top) / layoutInfo.scale
)
currentLayerState.value = currentLayer
}
} while (change == null || change.pressed)
onTouchEnd(currentLayer)
}
}
}
}
)
}
@Composable
private fun QMUIPhotoPickerEditPaintOptions(
paintEdit: List<PaintEdit>,
size: Dp,
selectedIndex: Int,
onSelect: (Int) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceAround
) {
paintEdit.forEachIndexed { index, paintEdit ->
paintEdit.Compose(size = size, selected = index == selectedIndex) {
onSelect(index)
}
}
}
}
@Composable
private fun QMUIPhotoPickerEditToolBar(
modifier: Modifier,
isPaintState: Boolean,
onPaintClick: (toPaint: Boolean) -> Unit,
@@ -256,7 +508,7 @@ internal fun QMUIPhotoPickerEditToolBar(
) {
val (paint, text, clip, ensure) = createRefs()
val horChain = createHorizontalChain(paint, text, clip, chainStyle = ChainStyle.Packed(0f))
constrain(horChain){
constrain(horChain) {
start.linkTo(parent.start, 16.dp)
end.linkTo(ensure.start)
}
@@ -298,7 +550,7 @@ internal fun QMUIPhotoPickerEditToolBar(
enabled = true,
text = "确定",
onClick = onEnsureClick,
modifier = Modifier.constrainAs(ensure){
modifier = Modifier.constrainAs(ensure) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end, 16.dp)
@@ -0,0 +1,48 @@
package com.qmuiteam.photo.compose.picker
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
interface EditLayer {
fun DrawScope.draw()
fun drawToBitmap()
}
sealed class PaintEditLayer(val path: Path) : EditLayer
class GraffitiEditLayer(
path: Path,
val color: Color,
val strokeWidth: Float
) : PaintEditLayer(path) {
override fun DrawScope.draw() {
drawPath(
path,
color = color,
style = Stroke(
width = strokeWidth,
cap = StrokeCap.Round,
join = StrokeJoin.Round
)
)
}
override fun drawToBitmap() {
}
}
class MosaicEditLayer(path: Path) : PaintEditLayer(path) {
override fun DrawScope.draw() {
}
override fun drawToBitmap() {
}
}