如何正确取消Pending Runnable,在用户交互时保持自定义视频控件可见?
Android自定义视频播放器控件自动隐藏Bug修复
问题原因
核心问题是每次调用自动隐藏方法时都会创建新的Runnable实例,导致后续无法正确取消之前的延迟任务:
setViewToDisappear方法内部每次新建Runnable,你在触摸事件中尝试取消的hideViewRunnable是初始化时创建的,和实际post的不是同一个对象,Handler找不到要移除的回调。cancelSetViewToDisappear方法使用错误:handler.removeCallbacksAndMessages(runnable)传入Runnable实例时,只会移除该特定实例的回调,但这里实例不匹配;精准移除需要保证post和remove的是同一个Runnable对象。
修复方案
- 为每个需要自动隐藏的控件(播放按钮、选项栏)定义全局Runnable实例,避免重复创建。
- 统一管理自动隐藏逻辑,先取消已有任务再post新任务,避免重复计时。
- 在用户交互场景(触摸视频、拖动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
相关产品推荐
相关产品推荐

