From bd377bef00b8d0b60865d9da43d190b1de020138 Mon Sep 17 00:00:00 2001 From: jayChou <549226148@qq.com> Date: Mon, 9 May 2022 09:22:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9ESwipe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 + app/src/main/AndroidManifest.xml | 11 ++ .../cui/testclient/swipe/SwipeActivity.kt | 38 ++++ .../idesign/cui/testclient/swipe/SwipeTest.kt | 182 ++++++++++++++++++ .../main/java/cn/idesign/cui/swipe/Swipe.kt | 169 ++++++++++++++++ .../java/cn/idesign/cui/swipe/SwipeCopy.kt | 112 +++++++++++ 6 files changed, 519 insertions(+) create mode 100644 app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeActivity.kt create mode 100644 app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeTest.kt create mode 100644 cui/src/main/java/cn/idesign/cui/swipe/Swipe.kt create mode 100644 cui/src/main/java/cn/idesign/cui/swipe/SwipeCopy.kt diff --git a/README.md b/README.md index 9d613fd..13b3a1a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ - [StatefulLayout](#StatefulLayout) - [Stepper](#Stepper) - [Steps](#Steps) +- [Swipe](#Swipe) - [TimeSelect](#TimeSelect) - [VerifyCode](#VerifyCode) @@ -172,6 +173,12 @@ Name | 截图 --- | --- Steps| +Swipe +====================== +Name | 截图 +--- | --- +Swipe| + TimeSelect ====================== Name | 截图 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 69d73e6..626f712 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -296,6 +296,17 @@ + + + + + + + diff --git a/app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeActivity.kt b/app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeActivity.kt new file mode 100644 index 0000000..b6551fe --- /dev/null +++ b/app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeActivity.kt @@ -0,0 +1,38 @@ +package cn.idesign.cui.testclient.swipe + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.ui.Modifier +import cn.idesign.cui.testclient.ui.theme.CUITestTheme + +class SwipeActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + CUITestTheme { + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "Swipe示例", + color = MaterialTheme.colors.onPrimary + ) + }, + backgroundColor = MaterialTheme.colors.primary, + ) + }, + modifier = Modifier.fillMaxSize() + ) { + SwipeTest() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeTest.kt b/app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeTest.kt new file mode 100644 index 0000000..92e61af --- /dev/null +++ b/app/src/main/java/cn/idesign/cui/testclient/swipe/SwipeTest.kt @@ -0,0 +1,182 @@ +package cn.idesign.cui.testclient.swipe + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Switch +import androidx.compose.material.Text +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import cn.idesign.cui.cell.Cell +import cn.idesign.cui.swipe.Swipe +import cn.idesign.cui.swipe.SwipeDirection +import cn.idesign.cui.swipe.rememberSwipeState + +@Composable +fun SwipeTest() { + var checked by remember { + mutableStateOf(false) + } + val state = rememberSwipeState() + LazyColumn( + Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + item { + Text( + text = "基本用法", + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + color = MaterialTheme.colors.onSurface.copy(ContentAlpha.high), + style = MaterialTheme.typography.subtitle1.copy(fontWeight = FontWeight.Medium) + ) + Swipe(background = { + Box( + modifier = Modifier + .width(66.dp) + .fillMaxHeight() + .background(Color.Red), + contentAlignment = Alignment.Center + ) { + Text( + text = "删除", + color = Color.White, + + ) + } + }) { + Cell(text = "左滑删除") + } + } + item { + Text( + text = "右滑删除", + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + color = MaterialTheme.colors.onSurface.copy(ContentAlpha.high), + style = MaterialTheme.typography.subtitle1.copy(fontWeight = FontWeight.Medium) + ) + Swipe( + direction = SwipeDirection.LeftToRight, + background = { + Box( + modifier = Modifier + .width(66.dp) + .fillMaxHeight() + .background(MaterialTheme.colors.primary), + contentAlignment = Alignment.Center + ) { + Text( + text = "收藏", + color = Color.White, + + ) + } + }) { + Cell(text = "右滑删除") + } + } + item { + Text( + text = "多个操作", + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + color = MaterialTheme.colors.onSurface.copy(ContentAlpha.high), + style = MaterialTheme.typography.subtitle1.copy(fontWeight = FontWeight.Medium) + ) + Swipe(background = { + Row { + Box( + modifier = Modifier + .width(66.dp) + .fillMaxHeight() + .background(Color.Red), + contentAlignment = Alignment.Center + ) { + Text( + text = "删除", + color = Color.White, + + ) + } + Box( + modifier = Modifier + .width(66.dp) + .fillMaxHeight() + .background(MaterialTheme.colors.primary), + contentAlignment = Alignment.Center + ) { + Text( + text = "收藏", + color = Color.White, + + ) + } + } + }) { + Cell(text = "多个操作") + } + } + + item { + Text( + text = "异步控制", + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + color = MaterialTheme.colors.onSurface.copy(ContentAlpha.high), + style = MaterialTheme.typography.subtitle1.copy(fontWeight = FontWeight.Medium) + ) + Swipe( + state = state, + background = { + Box( + modifier = Modifier + .width(66.dp) + .fillMaxHeight() + .background(Color.Red), + contentAlignment = Alignment.Center + ) { + Text( + text = "删除", + color = Color.White, + + ) + } + + }) { + Cell( + text = "多个操作", + rightComponent = { + Switch(checked = checked, onCheckedChange = { + checked = it + if (it) { + state.open() + } else { + state.close() + } + }) + }, + ) + } + } + } +} \ No newline at end of file diff --git a/cui/src/main/java/cn/idesign/cui/swipe/Swipe.kt b/cui/src/main/java/cn/idesign/cui/swipe/Swipe.kt new file mode 100644 index 0000000..7a7332c --- /dev/null +++ b/cui/src/main/java/cn/idesign/cui/swipe/Swipe.kt @@ -0,0 +1,169 @@ +package cn.idesign.cui.swipe + +import android.annotation.SuppressLint +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FractionalThreshold +import androidx.compose.material.rememberSwipeableState +import androidx.compose.material.swipeable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +@SuppressLint("RememberReturnType") +@OptIn(ExperimentalComposeUiApi::class, androidx.compose.material.ExperimentalMaterialApi::class) +@Composable +fun Swipe( + state: SwipeState = rememberSwipeState(), + threshold: Float = 0.3f, + direction: SwipeDirection = SwipeDirection.RightToLeft, + background: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + var boxWidthPx by remember { + mutableStateOf(0) + } + var boxHeightPx by remember { + mutableStateOf(0) + } + var backgroundWidthPx by remember { + mutableStateOf(0) + } + val scope = rememberCoroutineScope() + val swipeableState = rememberSwipeableState(0) + val anchors by remember(backgroundWidthPx, direction) { + if (direction == SwipeDirection.RightToLeft) { + mutableStateOf(mapOf(0f to 0, -backgroundWidthPx.toFloat() to 1)) + } else { + mutableStateOf(mapOf(0f to 0, backgroundWidthPx.toFloat() to 1)) + } + } + + remember(state.currentValue) { + when (state.currentValue) { + SwipeValue.Hidden -> scope.launch { + swipeableState.animateTo(0) + } + SwipeValue.Open -> scope.launch { swipeableState.animateTo(1) } + } + + } + + + val swipeModifier = if (backgroundWidthPx > 0) Modifier.swipeable( + state = swipeableState, + anchors = anchors, + thresholds = { _, _ -> FractionalThreshold(threshold) }, + orientation = Orientation.Horizontal, + ) else Modifier + Box(modifier = Modifier + .onSizeChanged { + boxWidthPx = it.width + boxHeightPx = it.height + } + .clipToBounds() + .then(swipeModifier) + ) { + + val backgroundOffsetX = when (direction) { + SwipeDirection.LeftToRight -> -backgroundWidthPx + swipeableState.offset.value.roundToInt() + SwipeDirection.RightToLeft -> boxWidthPx + swipeableState.offset.value.roundToInt() + } + Box(modifier = Modifier + .fillMaxWidth() + .offset { + IntOffset( + x = swipeableState.offset.value.roundToInt(), + y = 0 + ) + }) { + content() + } + Box(modifier = Modifier + .height(with(LocalDensity.current) { boxHeightPx.toDp() }) + .onSizeChanged { + backgroundWidthPx = it.width + } + .offset { + IntOffset( + x = backgroundOffsetX, + y = 0 + ) + } + ) { + background() + } + + } +} + +sealed class SwipeDirection { + object LeftToRight : SwipeDirection() + object RightToLeft : SwipeDirection() +} + +enum class SwipeValue { + Hidden, + Open, +} + + +@Composable +fun rememberSwipeState( + initialValue: SwipeValue = SwipeValue.Hidden +): SwipeState = rememberSaveable(saver = SwipeState.SAVER) { + SwipeState( + initialValue = initialValue, + ) +} + +@OptIn(ExperimentalMaterialApi::class) +class SwipeState( + val initialValue: SwipeValue, +) { + + private var _currentValue: SwipeValue by mutableStateOf(initialValue) + + val currentValue: SwipeValue + get() = _currentValue + + fun open() { + if (_currentValue != SwipeValue.Open) { + _currentValue = SwipeValue.Open + } + } + + fun close() { + if (_currentValue != SwipeValue.Hidden) { + _currentValue = SwipeValue.Hidden + } + } + + companion object { + val SAVER: Saver = Saver( + save = { + it.initialValue + }, + restore = { + SwipeState(it) + } + ) + } +} \ No newline at end of file diff --git a/cui/src/main/java/cn/idesign/cui/swipe/SwipeCopy.kt b/cui/src/main/java/cn/idesign/cui/swipe/SwipeCopy.kt new file mode 100644 index 0000000..ffc0837 --- /dev/null +++ b/cui/src/main/java/cn/idesign/cui/swipe/SwipeCopy.kt @@ -0,0 +1,112 @@ +package cn.idesign.cui.swipe + +import android.util.Log +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import kotlin.math.abs +import kotlin.math.roundToInt + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun SwipeCopy( + threshold: Float = 0.5f, + background: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + var boxWidthPx by remember { + mutableStateOf(0) + } + var boxHeightPx by remember { + mutableStateOf(0) + } + var backgroundWidthPx by remember { + mutableStateOf(0) + } + var needConsume by remember { + mutableStateOf(0f) + } + val animateOffset by animateFloatAsState(needConsume) + + var offsetX by remember() { mutableStateOf(0f) } + Box(modifier = Modifier + .onSizeChanged { + boxWidthPx = it.width + boxHeightPx = it.height + Log.d("Swipe", "boxWidthPx:${boxWidthPx}") + } + .clipToBounds() + + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragEnd = { + //计算当前offsetX 是否在阀值 + val offsetXPercent = abs(offsetX) / backgroundWidthPx + if (offsetXPercent == 1f) { + //已经打开 + needConsume = 0f + } else if (offsetXPercent == 0f) { + //已经关闭 + needConsume = 0f + } else if (offsetXPercent >= threshold) { + //需要打开 + needConsume = (-(backgroundWidthPx + offsetX)) + } else { + //需要关闭 + needConsume = -offsetX + } + Log.d("Swipe", "offsetXPercent:${offsetXPercent}") + }, + onDragCancel = {}, + onDragStart = {}, + onHorizontalDrag = { change, dragAmount -> + offsetX = (offsetX + dragAmount).coerceIn(-backgroundWidthPx.toFloat(), 0f) + val offset = change.position.x.coerceIn(0f, boxWidthPx.toFloat()) + Log.d("Swipe", "offset:${offset},offsetX:${offsetX}") + }, + ) + } + ) { + + + Box(modifier = Modifier + .fillMaxWidth() + .offset { + IntOffset( + x = offsetX.roundToInt() + animateOffset.roundToInt(), + y = 0 + ) + }) { + content() + } + Box(modifier = Modifier + .height(with(LocalDensity.current) { boxHeightPx.toDp() }) + .onSizeChanged { + backgroundWidthPx = it.width + Log.d("Swipe", "backgroundWidthPx:${backgroundWidthPx},height:${it.height}") + } + .offset { + IntOffset( + x = boxWidthPx + offsetX.roundToInt() + animateOffset.roundToInt(), + y = 0 + ) + }) { + background() + } + } +} \ No newline at end of file