如何禁用 Jetpack Compose 列表/列/行中多个项目的同时点击(开箱即用的反跳?)

Sea*_*ici 28 android multi-touch multi-select kotlin android-jetpack-compose

我在 Jetpack Compose 中实现了一列按钮。我们意识到可以一次单击多个项目(例如用多个手指),并且我们想禁用此功能。

是否有一种开箱即用的方法可以通过使用父列修饰符来禁用对子可组合项的多个同时单击?

这是我的 UI 当前状态的示例,请注意有两个选定的项目和两个未选定的项目。

在此输入图像描述

这是一些如何实现的代码(精简)

Column(
    modifier = modifier
            .fillMaxSize()
            .verticalScroll(nestedScrollParams.childScrollState),
    ) {
        viewDataList.forEachIndexed { index, viewData ->
            Row(modifier = modifier.fillMaxWidth()
                        .height(dimensionResource(id = 48.dp)
                        .background(colorResource(id = R.color.large_button_background))
                        .clickable { onClick(viewData) },
                              verticalAlignment = Alignment.CenterVertically
    ) {
        //Internal composables, etc
    }
}
Run Code Online (Sandbox Code Playgroud)

Bor*_*huk 11

检查这个解决方案。它与 splitMotionEvents="false" 标志具有类似的行为。将此扩展与您的列修改器一起使用

import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.coroutineScope


fun Modifier.disableSplitMotionEvents() =
    pointerInput(Unit) {
        coroutineScope {    
            var currentId: Long = -1L    
            awaitPointerEventScope {    
                while (true) {
                awaitPointerEvent(PointerEventPass.Initial).changes.forEach { pointerInfo ->
                        when {
                            pointerInfo.pressed && currentId == -1L -> currentId = pointerInfo.id.value
                            pointerInfo.pressed.not() && currentId == pointerInfo.id.value -> currentId = -1
                            pointerInfo.id.value != currentId && currentId != -1L -> pointerInfo.consume()
                            else -> Unit
                        }
                    }
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)


And*_*Dev 7

这里有四种解决方案:

单击去抖动 (ViewModel)r

为此,您需要使用视图模型。视图模型处理点击事件。您应该传入一些标识被单击项目的 ID(或数据)。在您的示例中,您可以传递分配给每个项目的 id(例如按钮 id):

// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect

class MyViewModel : ViewModel() {

    val debounceState = MutableStateFlow<String?>(null)

    init {
        viewModelScope.launch {
            debounceState
                .debounce(300)
                .collect { buttonId ->
                    if (buttonId != null) {
                        when (buttonId) {
                            ButtonIds.Support -> displaySupport()
                            ButtonIds.About -> displayAbout()
                            ButtonIds.TermsAndService -> displayTermsAndService()
                            ButtonIds.Privacy -> displayPrivacy()
                        }
                    }
                }
        }
    }

    fun onItemClick(buttonId: String) {
        debounceState.value = buttonId
    }
}

object ButtonIds {
    const val Support = "support"
    const val About = "about"
    const val TermsAndService = "termsAndService"
    const val Privacy = "privacy"
}
Run Code Online (Sandbox Code Playgroud)

去抖器会忽略上次收到的点击后 500 毫秒内出现的任何点击。我已经测试过这个并且它有效。您永远无法一次单击多个项目。尽管您可以一次触摸两个并且两个都会突出显示,但只有您触摸的第一个才会生成单击处理程序。

单击去抖动器(修改器)

这是点击去抖动器的另一种形式,但旨在用作修改器。这可能是您最想使用的一个。大多数应用程序都会使用滚动列表,让您点击列表项。如果您多次快速点击某个项目,clickable修改器中的代码将执行多次。这可能会很麻烦。虽然用户通常不会点击多次,但我发现即使是意外的双击也会触发可点击两次。由于您希望在整个应用程序中不仅在列表上而且在按钮上避免这种情况,因此您可能应该使用自定义修饰符来解决此问题,而无需诉诸上面显示的视图模型方法。

创建自定义修改器。我把它命名为onClick

// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect

class MyViewModel : ViewModel() {

    val debounceState = MutableStateFlow<String?>(null)

    init {
        viewModelScope.launch {
            debounceState
                .debounce(300)
                .collect { buttonId ->
                    if (buttonId != null) {
                        when (buttonId) {
                            ButtonIds.Support -> displaySupport()
                            ButtonIds.About -> displayAbout()
                            ButtonIds.TermsAndService -> displayTermsAndService()
                            ButtonIds.Privacy -> displayPrivacy()
                        }
                    }
                }
        }
    }

    fun onItemClick(buttonId: String) {
        debounceState.value = buttonId
    }
}

object ButtonIds {
    const val Support = "support"
    const val About = "about"
    const val TermsAndService = "termsAndService"
    const val Privacy = "privacy"
}
Run Code Online (Sandbox Code Playgroud)

您会注意到,在上面的代码中,我使用的是App.debounceClicks. 这当然不存在于您的应用程序中。您需要在应用程序中可全局访问的某个位置创建此函数。这可能是一个单例对象。在我的代码中,我使用一个继承自 Application 的类,因为这是应用程序启动时实例化的类:

fun Modifier.onClick(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onClick: () -> Unit
) = composed(
    inspectorInfo = debugInspectorInfo {
        name = "clickable"
        properties["enabled"] = enabled
        properties["onClickLabel"] = onClickLabel
        properties["role"] = role
        properties["onClick"] = onClick
    }
) {

    Modifier.clickable(
        enabled = enabled,
        onClickLabel = onClickLabel,
        onClick = {
            App.debounceClicks {
                onClick.invoke()
            }
        },
        role = role,
        indication = LocalIndication.current,
        interactionSource = remember { MutableInteractionSource() }
    )
}
Run Code Online (Sandbox Code Playgroud)

不要忘记在 AndroidManifest 中包含类的名称:

<application
    android:name=".App"
Run Code Online (Sandbox Code Playgroud)

现在不要使用clickable,而是使用onClick

class App : Application() {

    override fun onCreate() {
        super.onCreate()
    }

    companion object {
        private val debounceState = MutableStateFlow { }

        init {
            GlobalScope.launch(Dispatchers.Main) {
                // IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
                debounceState
                    .debounce(300)
                    .collect { onClick ->
                        onClick.invoke()
                    }
            }
        }

        fun debounceClicks(onClick: () -> Unit) {
            debounceState.value = onClick
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

全局禁用多点触控

在您的主要活动中,覆盖dispatchTouchEvent:

class MainActivity : AppCompatActivity() {
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        return ev?.getPointerCount() == 1 && super.dispatchTouchEvent(ev)
    }
}
Run Code Online (Sandbox Code Playgroud)

这会全局禁用多点触控。如果您的应用程序有 Google 地图,您将需要向dispatchTouchEvent 添加一些代码,以确保在显示地图的屏幕可见时它保持启用状态。用户将使用两根手指缩放地图,这需要启用多点触控。

状态管理的点击处理程序

使用单击事件处理程序来存储单击的项目的状态。当第一个项目调用单击时,它设置状态以指示单击处理程序处于“使用中”。如果第二个项目尝试调用单击处理程序并且“正在使用”设置为 true,则它只会返回而不执行处理程序的代码。这本质上相当于同步处理程序,但不会阻塞,任何进一步的调用都会被忽略。


Sab*_*aba 6

fun singleClick(onClick: () -> Unit): () -> Unit {
    var latest: Long = 0
    return {
        val now = System.currentTimeMillis()
        if (now - latest >= 300) {
            onClick()
            latest = now
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以使用

Button(onClick = singleClick {
    // TODO
})
Run Code Online (Sandbox Code Playgroud)