Compose Modifier.swipeable() Write a side-swipe component


foreword

When you use QQ, you must have seen it: insert image description herethis time, I want to use Compose to write a similar one. You can follow me step by step, or you can directly read the complete code .


1. Tool selection

In Compose, there are two types of SwipeToDismiss and Modifier.swipeable() for us to use. The bottom layer of SwipeToDismiss is implemented using swipeable. When using it, the side-swipe will occupy a whole line, like this:

insert image description here

It is not consistent with what we want to do, and it is also different from what we are used to. So don't use it, use Modifier.swipeable() to achieve.


2. Realization

1. Method definition

First look at the definition of the last method and its parameters:

/**
 * @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
) {}
复制代码

Here's the direction definition, we set up an enumeration class:

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

Indicates two different directions. Others should be understood by looking at the comments.

2. Variable preparation

// 记录一下滑动方向, 便于下面的判断
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) {
	...主体内容和侧拉内容...
}
复制代码

Here we define some quantities to prepare for the display of sliding and side-drawing components below. Pay attention to the value of dx, because the offset of left and right must correspond to true of swipeState. When the offset distance is swipeItemWidth, the value of swipeState will become true, and the value of swipeState will be used for side pull Whether the component is displayed or not. The anchors here will be used in swipeable() later. Next, we fill in the body content and pull the content sideways.

3. Main content

code show as below:

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()
}

复制代码

The weight in the code will produce a squeeze effect when the side-drawing component is displayed and hidden, as we will see later. offset will cause the sliding of the main content to produce an offset effect. We have just defined the first two parameters of swipeable(); thresholds are often used to customize the critical threshold of the adsorption effect between different anchor points, and there are two commonly used FixedThreshold(Dp) and FractionalThreshold(Float); orientation has nothing to say, right? It must be horizontal here (you can also try vertical if you are interested). At this point, our main content part is complete, and it will be offset.

4. Side pull content

Finally it's our protagonist's side pull component. Because we will use the side pull component twice later (why? I will know in a moment), so we pull him out:

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()
        }
    }
}
复制代码

The content here is relatively simple. When the display is allowed and the value of swipeState is true, that is, when the sliding offset of the main content reaches the value we set, the side-swipe content will be displayed. However, there is actually one thing missing here, that is, when we click on the side pull content, it should be hidden, so what should we do, just add it:

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

It will change the value of swipeState to false so that the swiped content is hidden. Finally, the code for pulling content sideways is as follows (just add a few lines to the code block above):

/**
 * @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. Combination assembly

We have written each part, and then we can put them together:

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

Here you can see that what I just said is used twice, because it is arranged from left to right in the order we wrote in Row, and our side pull components are displayed on both sides, so this is the only way (I don't know if there is any good way, you can teach me not).

6. Results Quiz

We've covered everything before (the complete code is here), and finally, let's test some, test the code (just call it):

@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
            )
        }
    }
}
复制代码

Effect display: Pull from right to left:

insert image description here

Pull from left to right:

insert image description here

Click the side pull component to hide:

insert image description here


3. Complete code

Here is the core code, with comments to explain:

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
}

复制代码

at last

This is the end of the article, I hope it will be helpful to you, comments are welcome, bye!

Guess you like

Origin juejin.im/post/7084058959039954974