Compose Modifier.swipeable() Escribe un componente de deslizamiento lateral


prefacio

Cuando usas QQ, debes haberlo visto: inserte la descripción de la imagen aquíesta vez, quiero usar Compose para escribir uno similar. Puedes seguirme paso a paso, o puedes leer directamente el código completo .


1. Selección de herramientas

En Compose, hay dos tipos de SwipeToDismiss y Modifier.swipeable() para que los usemos. La capa inferior de SwipeToDismiss se implementa usando swipeable. Al usarlo, el deslizamiento lateral ocupará una línea completa, así:

inserte la descripción de la imagen aquí

No es coherente con lo que queremos hacer, y además es diferente a lo que estamos acostumbrados. Así que no lo use, use Modifier.swipeable() para lograrlo.


2. Realización

1. Definición del método

Primero mire la definición del último método y sus parámetros:

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

Aquí está la definición de dirección, configuramos una clase de enumeración:

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

Indica dos direcciones diferentes. Otros deben entenderse mirando los comentarios.

2. Preparación variable

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

Aquí definimos algunas cantidades para preparar la visualización de los componentes deslizantes y de dibujo lateral a continuación. Preste atención al valor de dx, porque el desplazamiento de izquierda y derecha debe corresponder al verdadero de swipeState. Cuando la distancia de desplazamiento es swipeItemWidth, el valor de swipeState se volverá verdadero, y el valor de swipeState se usará para tirar lateralmente. el componente se muestra o no. Los anclajes aquí se usarán en swipeable() más adelante. A continuación, completamos el contenido del cuerpo y tiramos del contenido hacia los lados.

3. Contenido principal

el código se muestra a continuación:

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

复制代码

El peso en el código producirá un efecto de compresión cuando el componente de dibujo lateral se muestre y se oculte, como veremos más adelante. offset hará que el deslizamiento del contenido principal produzca un efecto de compensación. Acabamos de definir los dos primeros parámetros de swipeable(); los umbrales se usan a menudo para personalizar el umbral crítico del efecto de adsorción entre diferentes puntos de anclaje, y hay dos de uso común FixedThreshold(Dp) y FractionalThreshold(Float); la orientación no tiene nada decir, ¿verdad?, debe ser horizontal aquí (también puedes probar vertical si estás interesado). En este punto, nuestra parte de contenido principal está completa y se compensará.

4. Contenido de extracción lateral

Finalmente es el componente de tracción lateral de nuestro protagonista. Porque usaremos el componente de extracción lateral dos veces más tarde (¿por qué? Lo sabré en un momento), así que lo sacamos:

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

El contenido aquí es relativamente simple. Cuando la visualización está permitida y el valor de swipeState es verdadero, es decir, cuando el desplazamiento deslizante del contenido principal alcanza el valor que configuramos, se mostrará el contenido de deslizamiento lateral. Sin embargo, en realidad falta una cosa aquí, es decir, cuando hacemos clic en el contenido de extracción lateral, debe estar oculto, entonces, ¿qué debemos hacer? Simplemente agréguelo:

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

Cambiará el valor de swipeState a falso para que el contenido deslizado esté oculto. Finalmente, el código para extraer contenido lateralmente es el siguiente (simplemente agregue unas pocas líneas al bloque de código de arriba):

/**
 * @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. Montaje combinado

Hemos escrito cada parte, y luego podemos juntarlas:

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

Aquí puede ver que lo que acabo de decir se usa dos veces, porque está organizado de izquierda a derecha en el orden que escribimos en Fila, y nuestros componentes de extracción lateral se muestran en ambos lados, por lo que esta es la única forma (no No sé si hay alguna buena manera, puedes enseñarme que no).

6. Cuestionario de resultados

Hemos cubierto todo antes (el código completo está aquí), y finalmente, probemos un poco, probemos el código (simplemente llámelo):

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

Visualización de efectos: tire de derecha a izquierda:

inserte la descripción de la imagen aquí

Tira de izquierda a derecha:

inserte la descripción de la imagen aquí

Haga clic en el componente de extracción lateral para ocultar:

inserte la descripción de la imagen aquí


3. Código completo

Aquí está el código central, con comentarios para explicar:

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
}

复制代码

Al final

Este es el final del artículo, espero que les sea útil, los comentarios son bienvenidos, ¡chao!

Supongo que te gusta

Origin juejin.im/post/7084058959039954974
Recomendado
Clasificación