You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确取消Pending Runnable,在用户交互时保持自定义视频控件可见?

Android自定义视频播放器控件自动隐藏Bug修复

问题原因

核心问题是每次调用自动隐藏方法时都会创建新的Runnable实例,导致后续无法正确取消之前的延迟任务:

  1. setViewToDisappear方法内部每次新建Runnable,你在触摸事件中尝试取消的hideViewRunnable是初始化时创建的,和实际post的不是同一个对象,Handler找不到要移除的回调。
  2. cancelSetViewToDisappear方法使用错误:handler.removeCallbacksAndMessages(runnable)传入Runnable实例时,只会移除该特定实例的回调,但这里实例不匹配;精准移除需要保证post和remove的是同一个Runnable对象。

修复方案

  1. 为每个需要自动隐藏的控件(播放按钮、选项栏)定义全局Runnable实例,避免重复创建。
  2. 统一管理自动隐藏逻辑,先取消已有任务再post新任务,避免重复计时。
  3. 在用户交互场景(触摸视频、拖动SeekBar、点击播放按钮)中,添加取消/重启自动隐藏的逻辑,确保交互时控件保持显示。

修改后的完整代码

package consumer.app.co.za.rain.clean.ui.view.custom.doityourselfvideoplayer

import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import android.widget.SeekBar
import consumer.app.co.za.rain.R
import consumer.app.co.za.rain.databinding.MiniScreenVideoPlayerLayoutBinding

@SuppressLint("ClickableViewAccessibility")
class VideoPlayerMiniScreen @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val binding = MiniScreenVideoPlayerLayoutBinding.inflate(
        LayoutInflater.from(context),
        this,
        true
    )

    private var seekBarTracking = false
    private val handler = Handler(Looper.getMainLooper())

    private val updateProgressRunnable = object : Runnable {
        override fun run() {
            if (!seekBarTracking && binding.vvDoItYourself.isPlaying) {
                val currentPosition = binding.vvDoItYourself.currentPosition
                binding.seekBarDoItYourself.progress = currentPosition
            }
            handler.postDelayed(this, 16)
        }
    }

    // 为每个需要自动隐藏的控件定义全局Runnable实例
    private val hidePlayPauseRunnable = Runnable { hideView(binding.ivPlayPause) }
    private val hideOptionsRunnable = Runnable { hideView(binding.videoOptionsDoItYourself) }

    init {
        with(binding) {
            videoOptionsDoItYourself.apply {
                visibility = VISIBLE
                z = 2F
            }
            ivPlayPause.apply {
                alpha = 1F
                visibility = GONE
                z = 2F
            }

            vvDoItYourself.run {
                z = 1F

                setOnTouchListener { _, event ->
                    when (event.action) {
                        MotionEvent.ACTION_DOWN -> {
                            // 取消所有待隐藏任务
                            cancelAllHideRunnables()

                            Log.d("DIY", "Touched Down")
                            if (ivPlayPause.visibility == VISIBLE) togglePlayPause()
                            else {
                                Log.d("DIY", "Show controls")
                                showView(ivPlayPause)
                                showView(videoOptionsDoItYourself)
                            }
                        }
                        MotionEvent.ACTION_UP -> {
                            // 重新启动自动隐藏
                            startAutoHideControls()
                        }
                    }
                    true
                }

                setOnPreparedListener { mediaPlayer ->
                    seekBarDoItYourself.max = mediaPlayer.duration
                    if (mediaPlayer.isPlaying) mediaPlayer.stop()

                    mediaPlayer.setOnSeekCompleteListener {
                        seekBarDoItYourself.progress = it.currentPosition
                    }
                    handler.post(updateProgressRunnable)
                }

                setOnCompletionListener { mediaPlayer ->
                    ivPlayPause.setImageResource(R.drawable.baseline_play_arrow_24)
                    showView(ivPlayPause)
                    seekBarDoItYourself.progress = 0
                    mediaPlayer.seekTo(0)
                    handler.removeCallbacks(updateProgressRunnable)
                    // 播放结束后取消自动隐藏,保持按钮可见
                    cancelAllHideRunnables()
                }
            }

            ivPlayPause.setOnClickListener {
                togglePlayPause()
                // 点击后重启自动隐藏
                startAutoHideControls()
            }

            seekBarDoItYourself.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
                override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                    if (!seekBarTracking && binding.vvDoItYourself.isPlaying) {
                        seekBarDoItYourself.progress = binding.vvDoItYourself.currentPosition
                    }
                    if (fromUser) {
                        binding.vvDoItYourself.seekTo(progress)
                        seekBarDoItYourself.progress = progress
                    }
                }

                override fun onStartTrackingTouch(seekBar: SeekBar?) {
                    seekBarTracking = true
                    // 拖动开始时取消自动隐藏
                    cancelAllHideRunnables()
                }

                override fun onStopTrackingTouch(seekBar: SeekBar?) {
                    seekBarTracking = false
                    seekBar?.let { binding.vvDoItYourself.seekTo(it.progress) }
                    // 拖动结束后重启自动隐藏
                    startAutoHideControls()
                }
            })
        }
    }

    fun setVideoURI(uri: Uri) {
        binding.vvDoItYourself.setVideoURI(uri)
    }

    private fun togglePlayPause() {
        with(binding) {
            if (!vvDoItYourself.isPlaying) {
                ivPlayPause.setImageResource(R.drawable.baseline_pause_24)
                vvDoItYourself.start()
            } else {
                ivPlayPause.setImageResource(R.drawable.baseline_play_arrow_24)
                vvDoItYourself.pause()
            }
        }
    }

    private fun showView(view: View) {
        view.run {
            visibility = VISIBLE
            alpha = 0F
            animate().alpha(1F).setDuration(500).start()
        }
    }

    private fun hideView(view: View) {
        view.run {
            alpha = 1F
            animate().alpha(0F).setDuration(500).withEndAction {
                visibility = GONE
            }.start()
        }
    }

    /** 统一启动控件自动隐藏 */
    private fun startAutoHideControls() {
        cancelAllHideRunnables()
        handler.postDelayed(hidePlayPauseRunnable, 3000)
        handler.postDelayed(hideOptionsRunnable, 3000)
    }

    /** 统一取消所有待隐藏任务 */
    private fun cancelAllHideRunnables() {
        handler.removeCallbacks(hidePlayPauseRunnable)
        handler.removeCallbacks(hideOptionsRunnable)
    }
}

关键修改说明

  • 新增全局hidePlayPauseRunnable和hideOptionsRunnable,保证post和remove的是同一个实例。
  • 新增startAutoHideControls方法,先取消已有任务再post新任务,避免重复计时。
  • 新增cancelAllHideRunnables方法,统一管理取消逻辑。
  • 在触摸事件、SeekBar拖动、播放按钮点击等交互场景中,添加取消/重启自动隐藏的逻辑,确保交互时控件不消失。
  • 播放结束时取消自动隐藏,保持播放按钮可见。

内容的提问来源于stack exchange,提问作者Klem Lloyd Mwenya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 21:07:06