掲題の、Youtube等に搭載されている長押しで早送りする機能を作った時に得た知見について記録します。
動画再生ページ仕様
BottomSheetBehaviorを用いて動画表示部分を下にスワイプすると、指の動きに画面ごと追従しながら閉じること(詳細はこちらの記事の「透明Activityの作り方と活用方法」を参照ください)
実装時に起きた問題点
-
対象となる動画表示画面に対してFragmentで
onLongPress
を早送りとして実装しただけでは、長押し中に少しでも指を上下左右に動かしてしまうとonLongPress
判定が外れてしまい、早送りしなくなってしまった -
上記問題を解消するために、対象の動画表示画面のviewの
setOnTouchListener
でイベントをハンドリングすることで回避できるかを検証した
具体的な簡易コードとしては以下
view.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// ここにログを貼って、全てのEventについて検証した
longPressListener.onLongPress(onLongPressing = false)
}
else -> {
// ここにログを貼って、全てのEventについて検証した
}
}
gestureDetector.onTouchEvent(event)
true
}
長押し状態が外れてしまう原因
長押し中に少しでも指を上下左右に動かしてしまうとonLongPress
判定が外れてしまう根本原因は上記の、下スワイプで画面を閉じる仕様が影響していた
上記のコード内で全てのMotionEventに対してログを貼って検証するとACTION_CANCEL
に入っていたことがわかりました
ACTION_CANCEL
が呼ばれるパターンとしてはざっくり以下となっています
- 親Viewによるモーションの奪取
- タッチイベントのキャンセル
- タッチ範囲外への移動
- onTouchメソッドでの処理
今回は対象画面の親Activityで定義した、下にスワイプすると指の動きに画面ごと追従しながら閉じるBottomSheetBehaviorの操作、つまり「親Viewによるモーションの奪取」によって起きていたことが根本原因でした
解決した実装
完全に仕様を同じのまま問題を解決した状態で動作させることは難しかったですが、以下のように実装しました。
ポイントとしては、長押し中のスワイプアクションを親Viewに奪われないようにrequestDisallowInterceptTouchEvent(true)
を追加します。
また、それだけだと上記仕様の下スワイプで画面を閉じる、が死んでしまうので動画表示画面で下スワイプしたときに外からdownSwipeListenerを与えることでコントロールできるようにしました(下スワイプ時、指の動きに画面ごと追従させる、は実現できていません)
val gestureDetector = GestureDetector(view.context, object : GestureDetector.SimpleOnGestureListener() {
private val swipeThreshold = 100
private val swipeVelocityThreshold = 100
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
singleTapListener.onSingleTap()
return true
}
override fun onLongPress(e: MotionEvent) {
longPressListener.onLongPress(onLongPressing = true)
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
val diffY = e2.y - e1!!.y
if (abs(diffY) > swipeThreshold && abs(velocityY) > swipeVelocityThreshold) {
if (diffY > 0) {
// 下スワイプ検知
downSwipeListener.onDownSwipe()
}
}
return super.onFling(e1, e2, velocityX, velocityY)
}
})
view.setOnTouchListener { _, event ->
// 親View側で処理を奪ってほしくないので以下を追加
view.parent.requestDisallowInterceptTouchEvent(true)
when (event.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// 指が画面から離れるか、異常を検知したらonLongPressingを終了
longPressListener.onLongPress(onLongPressing = false)
}
}
gestureDetector.onTouchEvent(event)
true
}