AndroidのViewにおいて、タッチイベントは子の方が優先的に処理できますが、親Viewは子ViewがonTouchEvent
を処理している中、onInterceptTouchEvent
が呼び出されており、trueを返すことで処理を奪うことができます。
例えば横スクロールのViewAの上に、縦スクロールのViewBを配置するという画面構成の場合、ViewAはonInterceptTouchEvent
で操作を監視し、操作方向が横方向であると判定すると、ViewBから操作の優先権を奪うことで、縦横両方の操作が両立できるという仕組みになっています。
概ねこれでうまくいくのですが、場合によっては親View側で処理を奪ってほしくない場合があります。その場合に使うのが、requestDisallowInterceptTouchEvent(true)
です。
名前の通り、このメソッドがコールされたView(およびその親のView)はInterceptしなくなり、子はタッチイベントの優先権を奪われなくなります。
例えば、チョット複雑なレイアウトを作るとします
- 横スクロールのViewPager2
- その上に縦スクロールのRecyclerView
- RecyclerViewのセルの一つが横スクロールのViewを持っている
みたいな構成の場合(そんな構成作るなというのは置いておいて)
このセルの横方向操作をViewPager2に奪われてしまうとカルーセルの操作ができなくなってしまうので、その操作をしている間はViewPager2のIntercept動作を停止させたいですよね。
一方で、このセルは横方向の操作は受け取りたいけど、縦方向の場合は自身ではなく親のRecyclerViewに奪ってもらって、縦スクロールさせたいところです。
なので、単純に親に対してrequestDisallowInterceptTouchEvent(true)
を実行する訳にはいきません。
失敗例
ということで、そのセルのViewを拡張し、以下のように、親のViewPager2を探して、requestDisallowInterceptTouchEvent(true)
を実行して……ということをやっても、ViewPager2の動作を抑制させることができません。
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
findViewPager2(this)?.requestDisallowInterceptTouchEvent(true)
}
return super.dispatchTouchEvent(ev)
}
private fun findViewPager2(view: View): View? {
val parent = view.parent
if (parent is ViewPager2) {
return parent
}
if (parent is ViewGroup) {
return findViewPager2(parent)
}
return null
}
うまくいかない理由
Viewの階層を見れば分かるのですが、ViewPager2は直接PageのViewを持っているわけではありません。
ViewPager2直下にViewPager2.RecyclerViewImpl
というViewがあります。
ViewPager2の中にあるprivate classで、RecyclerViewを拡張したクラスです。
ViewPager2自身はページスクロールなどの機能は持っておらず、それらを行っているのが、このRecyclerViewImpl
なのですね。ということは、タッチイベントを奪ったりしているのは、RecyclerViewImpl
の方、ということです。
requestDisallowInterceptTouchEvent(true)
は、そのViewGroupおよびそれより親に対する命令なので、ViewPager2のrequestDisallowInterceptTouchEvent(true)
をコールしても、肝心のRecyclerViewImpl
に伝わらず、意図した動作にならなかった訳です。
解決策
ということで、先のコードはViewPager2まで遡ってしまったのが問題でした、その一つ下にあるRecyclerViewImpl
を探すように変更します。ただし、RecyclerViewImpl
はprivate classなので、直接調べるのが難しいです。
ということで、親を遡っていき、親がViewPager2であるViewを返すように変更。
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
findViewPager2(this)?.requestDisallowInterceptTouchEvent(true)
}
return super.dispatchTouchEvent(ev)
}
private fun findRecyclerViewImpl(view: View): View? {
val parent = view.parent
if (parent is ViewPager2) {
return view
}
if (parent is ViewGroup) {
return findViewPager2(parent)
}
return null
}
これでViewPager2に操作を奪われないようにすることができます。
以上です。