Compose Modifier.swipeable() サイドスワイプ コンポーネントを作成する


序文

QQ を使用するときは、見たことがあるはずです:ここに画像の説明を挿入今回は、Compose を使用して同様のものを書きたいと思います. 順を追って説明するか、完全なコードを直接読むことができます.


1. ツールの選択

In Compose, there are two types of SwipeToDismiss and Modifier.swipeable() for us to use. SwipeToDismiss の最下層は、swipeable を使用して実装されます. これを使用すると、横スワイプは次のように行全体を占有します:

ここに画像の説明を挿入

それは私たちがやりたいことと一致していませんし、私たちが慣れ親しんでいることとも異なります。Modifier.swipeable() を使用して達成してください。


2.実現

1. メソッド定義

最初に、最後のメソッドとそのパラメーターの定義を見てください。

/**
 * @Description: 侧拉滑动组件
 * @Param:
 * @param modifier 没啥好说的
 * @param swipeItemWidth 侧拉组件的宽度
 * @param isShowSwipe 判断是否显示
 * @param swipeDirection 判断侧拉方向
 * @param swipeContent 侧拉组件的内容
 * @param content 主题内容
 * @return:
 */
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@Composable
fun SwipeItem(
    modifier: Modifier = Modifier,
    swipeItemWidth: Float,
    isShowSwipe: Boolean = true,
    swipeDirection: SwipeDirection = SwipeDirection.ENDTOSTART,
    swipeContent: @Composable () -> Unit,
    content: @Composable () -> Unit
) {}
复制代码

方向の定義は次のとおりです。列挙型クラスを設定します。

enum class SwipeDirection {
    STARTTOEND,
    ENDTOSTART
}
复制代码

2 つの異なる方向を示します。その他はコメントを見て理解する必要があります。

2.変数の準備

// 记录一下滑动方向, 便于下面的判断
val isEndToStart = swipeDirection == SwipeDirection.ENDTOSTART
val swipeState = rememberSwipeableState(initialValue = false)
// 滑动偏移量, 偏移量指 content 的左边的偏移
val dx: Float = if (isEndToStart) {
    -swipeItemWidth
} else {
    swipeItemWidth
}
// 偏移 dx 时显示, 两个方向不同, 所以上面对 dx 做判断
val anchors = mapOf(dx to true, 0f to false)
Row(modifier = modifier) {
	...主体内容和侧拉内容...
}
复制代码

ここでは、以下のスライド コンポーネントとサイド ドロー コンポーネントの表示を準備するために、いくつかの量を定義します。左と右のオフセットが swipeState の true に対応する必要があるため、dx の値に注意してください.オフセット距離が swipeItemWidth の場合、swipeState の値は true になり、swipeState の値はサイドプルに使用されるかどうかコンポーネントが表示されるかどうか。ここのアンカーは、後で swipeable() で使用されます。次に、本文のコンテンツを入力し、コンテンツを横にプルします。

3. 主な内容

コードは以下のように表示されます:

Box(
    modifier = Modifier
//          .fillMaxWidth()
        // 这里要用 weight 才会有挤压的效果
        // 而且用 fillMaxWidth() 滑动组件会被遮挡
        .weight(1f)
        .offset {
            IntOffset(
                swipeState.offset.value.toInt(), 0
            )
        }
        // swipeable() 是滑动组件的核心所在
        .swipeable(
            state = swipeState,
            anchors = anchors,
            thresholds = { _, _ -> FractionalThreshold(1f) },
            orientation = Orientation.Horizontal
        )
) {
    // 主体内容
    content()
}

复制代码

後で説明するように、コード内のウェイトは、サイド描画コンポーネントが表示されたり非表示になったりするときにスクイーズ効果を生み出します。offset は、メイン コンテンツのスライドでオフセット効果を生成します。swipeable() の最初の 2 つのパラメーターを定義しました。しきい値は、異なるアンカー ポイント間の吸着効果の臨界しきい値をカスタマイズするためによく使用されます。一般的に使用される 2 つの FixedThreshold(Dp) と FractionalThreshold(Float) があります。向きには何もありません。ここは横向きに違いない(興味があれば縦向きも可)。この時点で、メイン コンテンツ パーツが完成し、オフセットされます。

4.サイドプル内容

最後に、主人公のサイド プル コンポーネントです。サイド プル コンポーネントを後で 2 回使用するため (理由はすぐにわかります)、彼を引き出します。

private fun RowScope.SwipeChild(
    isShowSwipe: Boolean,
    swipeState: SwipeableState<Boolean>,
    swipeContent: @Composable () -> Unit
) {
    // 这里用动画进行侧拉组件显示和隐藏
    AnimatedVisibility(visible = isShowSwipe && swipeState.currentValue) {
         Box(
            modifier = Modifier
                .align(alignment = Alignment.CenterVertically)
        ) {
            swipeContent()
        }
    }
}
复制代码

ここでのコンテンツは比較的単純で、表示が許可され、swipeState の値が true の場合、つまり、メイン コンテンツのスライド オフセットが設定した値に達すると、サイド スワイプ コンテンツが表示されます。ただし、ここには実際には 1 つ欠けているものがあります。つまり、サイド プル コンテンツをクリックすると非表示になるはずなので、追加するだけです。

scope.launch {
    swipeState.animateTo(false)
}
复制代码

スワイプされたコンテンツが非表示になるように、swipeState の値を false に変更します。最後に、コンテンツを横にプルするためのコードは次のとおりです (上記のコード ブロックに数行を追加するだけです)。

/**
 * @Description: 侧拉组件显示与隐藏
 * @Param:
 * @param isShowSwipe
 * @param swipeState
 * @param swipeContent
 * @return:
 */
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@Composable
private fun RowScope.SwipeChild(
    isShowSwipe: Boolean,
    swipeState: SwipeableState<Boolean>,
    swipeContent: @Composable () -> Unit
) {
    val scope = rememberCoroutineScope()
    // 这里用动画进行侧拉组件显示和隐藏
    AnimatedVisibility(visible = isShowSwipe && swipeState.currentValue) {
        Box(modifier = Modifier
            .align(alignment = Alignment.CenterVertically)
            .clickable {
                scope.launch {
                    swipeState.animateTo(false)
                }
            }) {
            swipeContent()
        }
    }
}
复制代码

5. 組み合わせ組立

各部分を記述したので、それらをまとめることができます。

    Row(modifier = modifier) {
        // 由于 Row 的缘故, 这里和下面进行了判断
        // 因为两个方向要显示的 swipeItem 位置不同
        if (!isEndToStart) {
            SwipeChild(isShowSwipe, swipeState, swipeContent)
        }
		...主体内容...
        if (isEndToStart) {
            SwipeChild(isShowSwipe, swipeState, swipeContent)
        }
    }
复制代码

ここでは、Row で記述した順序で左から右に配置され、サイド プル コンポーネントが両側に表示されているため、今言ったことを 2 回使用していることがわかります。いい方法があれば教えてください。)

6.結果クイズ

前にすべてをカバーしました (完全なコードはここにあります)。最後に、いくつかをテストして、コードをテストしましょう (呼び出すだけです)。

@Composable
fun Main() {
    SwipeItem(
        modifier = Modifier
            .fillMaxWidth()
            .height(50.dp),
        swipeItemWidth = 20f,
        isShowSwipe = true,
        swipeDirection = SwipeDirection.STARTTOEND,
//        swipeDirection = SwipeDirection.ENDTOSTART,
        swipeContent = {
            Icon(
                imageVector = Icons.Default.Face,
                contentDescription = null
            )
        }) {
        Row {
            Text(
                text = "哈哈哈哈哈哈哈哈哈哈哈哈",
                modifier = Modifier.background(Color.Red),
                fontSize = 30.sp
            )
        }
    }
}
复制代码

効果表示: 右から左に引く:

ここに画像の説明を挿入

左から右に引っ張る:

ここに画像の説明を挿入

非表示にするサイド プル コンポーネントをクリックします。

ここに画像の説明を挿入


3.完全なコード

説明するコメント付きのコアコードは次のとおりです。

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import kotlinx.coroutines.launch

/**
 * @Description: 侧拉滑动组件
 * @Param:
 * @param modifier 没啥好说的
 * @param swipeItemWidth 侧拉组件的宽度
 * @param isShowSwipe 判断是否显示
 * @param swipeDirection 判断侧拉方向
 * @param swipeContent 侧拉组件的内容
 * @param content 主题内容
 * @return:
 */
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@Composable
fun SwipeItem(
    modifier: Modifier = Modifier,
    swipeItemWidth: Float,
    isShowSwipe: Boolean = true,
    swipeDirection: SwipeDirection = SwipeDirection.ENDTOSTART,
    swipeContent: @Composable () -> Unit,
    content: @Composable () -> Unit
) {
    // 记录一下滑动方向, 便于下面的判断
    val isEndToStart = swipeDirection == SwipeDirection.ENDTOSTART
    val swipeState = rememberSwipeableState(initialValue = false)
    // 滑动偏移量, 偏移量指 content 的左边的偏移
    val dx: Float = if (isEndToStart) {
        -swipeItemWidth
    } else {
        swipeItemWidth
    }
    // 偏移 dx 时显示, 两个方向不同, 所以上面对 dx 做判断
    val anchors = mapOf(dx to true, 0f to false)
    Row(modifier = modifier) {
        // 由于 Row 的缘故, 这里和下面进行了判断
        // 因为两个方向要显示的 swipeItem 位置不同
        if (!isEndToStart) {
            SwipeChild(isShowSwipe, swipeState, swipeContent)
        }
        Box(
            modifier = Modifier
//                .fillMaxWidth()
                // 这里要用 weight 才会有挤压的效果
                // 而且用 fillMaxWidth() 滑动组件会被遮挡
                .weight(1f)
                .offset {
                    IntOffset(
                        swipeState.offset.value.toInt(), 0
                    )
                }
                // swipeable() 是滑动组件的核心所在
                .swipeable(
                    state = swipeState,
                    anchors = anchors,
                    thresholds = { _, _ -> FractionalThreshold(1f) },
                    orientation = Orientation.Horizontal
                )
        ) {
            // 主体内容
            content()
        }
        if (isEndToStart) {
            SwipeChild(isShowSwipe, swipeState, swipeContent)
        }
    }
}

/**
 * @Description: 侧拉组件显示与隐藏
 * @Param:
 * @param isShowSwipe
 * @param swipeState
 * @param swipeContent
 * @return:
 */
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@Composable
private fun RowScope.SwipeChild(
    isShowSwipe: Boolean,
    swipeState: SwipeableState<Boolean>,
    swipeContent: @Composable () -> Unit
) {
    val scope = rememberCoroutineScope()
    // 这里用动画进行侧拉组件显示和隐藏
    AnimatedVisibility(visible = isShowSwipe && swipeState.currentValue) {
        Box(modifier = Modifier
            .align(alignment = Alignment.CenterVertically)
            .clickable {
                scope.launch {
                    swipeState.animateTo(false)
                }
            }) {
            swipeContent()
        }
    }
}

enum class SwipeDirection {
    STARTTOEND,
    ENDTOSTART
}

复制代码

やっと

これで記事は終わりです。お役に立てれば幸いです。コメントは大歓迎です。さようなら!

おすすめ

転載: juejin.im/post/7084058959039954974