首页 > 解决方案 > 是否可以在侦听器中对 onClick 回调进行去抖动?

问题描述

我有一个用于放大和缩小地图视图的侦听器:

class ZoomMapListener(
    mapView: MapView,
    private val zoom: Zoom,
) : View.OnClickListener {

    private val localMapView = WeakReference(mapView)
    private var clickCount = 0

    override fun onClick(view: View?) {
        clickCount++
    }

    fun moveCamera() {
        val mapView = localMapView.get()

        mapView?.let {
            var cameraPosition = it.map.cameraPosition

            val zoom = if (zoom == IN) {
                cameraPosition.zoom + (1.0f * clickCount)
            } else {
                cameraPosition.zoom - (1.0f * clickCount)
            }

            cameraPosition = CameraPosition(
                cameraPosition.target,
                zoom,
                cameraPosition.azimuth,
                cameraPosition.tilt,
            )

            clickCount = 0

            it.map.move(cameraPosition, Animation(Animation.Type.SMOOTH, 0.5f), null)
        }
    }
}

enum class Zoom {
    IN,
    OUT
}

为了如果用户多次点击按钮,我决定使用另一个答案的去抖动运算符(https://stackoverflow.com/a/60234167/13236614),所以如果有五次点击,例如,相机会增加五倍在一次操作中。

扩展功能:

@FlowPreview
@ExperimentalCoroutinesApi
fun View.setDebouncedListener(
    listener: ZoomMapListener,
    lifecycleCoroutineScope: LifecycleCoroutineScope,
) {
    callbackFlow {
        setOnClickListener {
            listener.onClick(this@setDebouncedListener)
            offer(Unit)
        }
        awaitClose {
            setOnClickListener(null)
        }
    }
        .debounce(500L)
        .onEach { listener.moveCamera() }
        .launchIn(lifecycleCoroutineScope)
}

以及我如何在我的片段中使用它:

zoomInMapButton.setDebouncedListener(ZoomMapListener(mapView, Zoom.IN), lifecycleScope)

我认为这一切看起来有点糟糕,我怀疑是因为@FlowPreview注释,所以有没有办法至少在自定义侦听器类中使它正确?

标签: androidkotlinkotlin-coroutines

解决方案


使用带有@FlowPreviewor@ExperimentalCoroutinesApi的东西有点像使用不推荐使用的函数,因为它可能会停止按预期工作或在库的未来版本中被删除。它们相对稳定,但每次更新核心 Kotlin 库时都需要检查它们。

我对其他问题的无协程回答更像是throttleFirstdebounce因为它不会延迟第一次点击。

我认为您只需更改一行代码即可直接在 ZoomListener 类中处理去抖动!替换clickCount++if (++clickCount == 1) v.postDelayed(::moveCamera, interval)

免责声明:我没有对此进行测试。

这里的策略是在第一次点击时立即发布延迟调用moveCamera()。如果在该延迟时间内有任何点击,他们不会发布新的延迟呼叫,因为他们的贡献已计入延迟结束时将使用的clickCount那个。moveCamera()

我也做了一些清理moveCamera(),但它在功能上是一样的。在我看来,?.let不应该用于局部变量,因为您可以利用局部变量的智能转换(或提前返回),这样您就可以使您的代码更具可读性和更少的嵌套。

class ZoomMapListener(
    mapView: MapView,
    private val zoom: Zoom,
    private val interval: Long
) : View.OnClickListener {

    private val localMapView = WeakReference(mapView)
    private var clickCount = 0

    override fun onClick(v: View) {
        if (++clickCount == 1) v.postDelayed(::moveCamera, interval)
    }

    fun moveCamera() {
        val map = localMapView.get()?.map ?: return
        val multiplier = if (zoom == IN) 1f else -1f
        val newCameraPosition = CameraPosition.builder(map.cameraPosition)
            .zoom(map.cameraPosition.zoom + multiplier * clickCount)
            .build()

        clickCount = 0

        map.move(newCameraPosition, Animation(Animation.Type.SMOOTH, 0.5f), null)
    }
}

推荐阅读