Android(に限らないけど)で普通はViewヒエラルキーの上から順にたどってenableでfocusableなViewを順にチェックして最初に見つかったハンドリングしたいと主張したViewのみがタッチイベントやクリックイベントを受け取りことができます。別な言い方をすると、重なった上のViewがタッチイベント等を処理してしまうと通常その下層にあるViewにはイベントが発生しません(もちろん上のViewから下のViewへタッチイベントを移譲するするように上のViewに実装すれば可能ですが)。
通常のUIコンポーネント、例えばボタンを重ねて表示するなどであればまぁ普通はこの挙動を変えることはまずないです。でも大人の事情により重なったViewを同期してタッチ操作したいときもまれにはあります、たぶん。例えば動画を表示していてタッチ操作でその動画の移動拡大縮小しつつそれに合わせてオーバーレイ表示している要素も移動拡大縮小するとか。
そういうときはカスタムView作って必要な描画一括で行うってのが普通なのかなぁ…普通じゃないからよくわかりませんが?
でも同期して操作したいViewのソースがいつもあるわけじゃないし、中身が複雑なViewの場合余計な処理をさらに密結合でぶち込むのはためらわれる場合もあります。あるいは動画表示の場合TextureViewやSurfaceView、GLSurfaceViewで表示しているはずですが、それらのViewではCanvasを使った追加の画面描画ができません。一度オフスクリーン描画で映像とオーバーレイ表示を重ね合わせた上でそれらのViewに転送することになります。動画とオーバーレイ表示が密に関係している場合は仕方ないのですがいずれにしてもめんどくさいんじゃぁ〜
ということで独立した複数のViewを重ね合わせた時に重ね合わせの状態に関わらずすべてのViewに対してタッチイベントを起こせるのかどうかを確かめてみました。重ね合わせるということで可能性があるのは FrameLayout , RelativeLayout , ConstraintLayout あたりが対象になりますが今回はFrameLayoutを拡張してみます、名付けて MultiDispatchTouchFrameLayoutです。
子Viewへフォーカスがいかないように&ViewGroup自体がタッチイベント等を受け取れるようにコンストラクタで次のように設定しておきます(今回はKotlinにしてみた)。
1 2 3 4 |
init { descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS isFocusable = true } |
関係ないけどWordpressのcrayonプラグインにはKotlinの選択肢がないんだよね(。・_・。)
続いて
onTouchEvent でもいいのかもしれないけど更に呼び出し上位の
dispatchTouchEvent をoverrideします。
子Viewが有効&子ViewのhitRect内をタッチ&子Viewがイベントをハンドリングしているときに
子View#dispatchTouchEvent を呼び出します。
なお最初のACTION_DOWNイベントで
MultiDispatchTouchFrameLayout#dispatchTouchEvent がfalse(==ハンドリングしない)を返してしまうと以降のイベントが飛んでこなくなってしまうので、いずれかの子Viewの
dispatchTouchEvent がtrueを返したときには
MultiDispatchTouchFrameLayout#dispatchTouchEvent もtrueを返す必要があります。
なお、 MultiDispatchTouchFrameLayout#dispatchTouchEventから super#dispatchTouchEventを呼んでしまうと 子ViewのdispatchTouchEvent が1回余分に呼ばれることになりますので注意してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
private val mDispatched = SparseBooleanArray() private val mWorkRect = Rect() override fun dispatchTouchEvent(ev: MotionEvent): Boolean { return if (onFilterTouchEventForSecurity(ev)) { var result = false val children = childCount for (i in 0 until children) { val v = getChildAt(i) val id = v.hashCode() v.getHitRect(mWorkRect) if (v.isEnabled && isFocusable && mWorkRect.contains(ev.x.toInt(), ev.y.toInt()) && mDispatched[id, true]) { // 子Viewが有効&子ViewのhitRect内をタッチ&子Viewがイベントをハンドリングしているとき val dispatched = v.dispatchTouchEvent(ev) mDispatched.put(id, dispatched) result = result or dispatched } } val action = ev.action if ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) && ev.pointerCount == 1) { mDispatched.clear() } if (DEBUG) Log.v(TAG, "dispatchTouchEvent:result=$result") result } else { super.dispatchTouchEvent(ev) } } |
これをレイアウトxmlへ放り込んで適当なViewを子Viewに追加します。ここでは単にonTouchEventが呼ばれた時にlogCatへログを出力するようにしただけなTest1View(onTouchEventがtrueを返す==タッチイベントをハンドリングする)とTest2View(onTouchEventがfalseを返す==タッチイベントをハンドリングしない)を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <com.serenegiant.widget.MultiDispatchTouchFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.serenegiant.widget.Test1View android:id="@+id/test1" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.serenegiant.widget.Test2View android:id="@+id/test2" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.serenegiant.widget.MultiDispatchTouchFrameLayout> |
いざ動かしてみるとlogCatはこんな感じになります。Test2ViewはonTouchEventがfalseを返すので最初のACTION_DOWNのみ、Test1ViewはonTouchEventが常にtrueを返すので全部のイベント来ています。
1 2 3 4 5 6 7 8 |
V/Test1View: onTouchEvent(2131231051):0 V/Test2View: onTouchEvent(2131231052):0 V/Test1View: onTouchEvent(2131231051):2 V/Test1View: onTouchEvent(2131231051):2 ... V/Test1View: onTouchEvent(2131231051):2 V/Test1View: onTouchEvent(2131231051):2 V/Test1View: onTouchEvent(2131231051):1 |
でもまぁこれだけじゃ片方にしかイベント来てないから普通じゃんってことで先程のレイアウトのViewを2つともTest1Viewにしてみます。
通常のFrameLayoutであれば上にあるViewにしかタッチイベントが来ないはずですがどうなるでしょう?
1 2 3 4 5 6 7 8 9 |
V/Test1View: onTouchEvent(2131231051):0 V/Test1View: onTouchEvent(2131231052):0 V/Test1View: onTouchEvent(2131231051):2 V/Test1View: onTouchEvent(2131231052):2 ... V/Test1View: onTouchEvent(2131231051):2 V/Test1View: onTouchEvent(2131231052):2 V/Test1View: onTouchEvent(2131231051):1 V/Test1View: onTouchEvent(2131231052):1 |
重なっているにも関わらずちゃんと両方のViewへ onTouchEventが来ています\(^o^)/
おまけで通常のFrameLayoutの場合はこんな感じ。上側のView(id=2131231052)にしか onTouchEvent が来ません(通常の正常な挙動)
1 2 3 4 5 |
V/Test1View: onTouchEvent(2131231052):0 V/Test1View: onTouchEvent(2131231052):2 ... V/Test1View: onTouchEvent(2131231052):2 V/Test1View: onTouchEvent(2131231052):1 |
先程までは MultiDispatchTouchFrameLayout内に直接Viewを入れていましたが、 Fragment を入れた場合にはどうなるのか気になって仕方ないですよね?ということで、おりゃっっと
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="utf-8"?> <com.serenegiant.widget.MultiDispatchTouchFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <FrameLayout android:id="@+id/container1" android:layout_width="match_parent" android:layout_height="match_parent" /> <FrameLayout android:id="@+id/container2" android:layout_width="match_parent" android:layout_height="match_parent" /> <FrameLayout android:id="@+id/container3" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.serenegiant.widget.MultiDispatchTouchFrameLayout> |
id/container1にはFragment1, id/container2にはFragment2, id/container3にはFragment3をreplaceします。
それぞれのレイアウトはこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".Fragment1"> <com.serenegiant.widget.Test1View android:id="@+id/test1" android:layout_width="match_parent" android:layout_height="match_parent"/> <com.serenegiant.widget.Test1View android:id="@+id/test3" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".Fragment2"> <com.serenegiant.widget.Test2View android:id="@+id/test2" android:layout_width="match_parent" android:layout_height="match_parent"/> <com.serenegiant.widget.Test1View android:id="@+id/test4" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <com.serenegiant.widget.MultiDispatchTouchFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".Fragment3"> <com.serenegiant.widget.Test1View android:id="@+id/test5" android:layout_width="match_parent" android:layout_height="match_parent"/> <com.serenegiant.widget.Test1View android:id="@+id/test6" android:layout_width="match_parent" android:layout_height="match_parent"/> </com.serenegiant.widget.MultiDispatchTouchFrameLayout> |
想定どおりであれば、Fragment1とFragment2ではTest1Viewがそれぞれ1つだけ、Fragment3ではTest1Viewが2つともタッチイベントを受け取れるはずです(Test1Viewが合計4つ)。
いざ実行(^o^)/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
V/Test1View: onTouchEvent(2131231053):0 V/Test1View: onTouchEvent(2131231054):0 V/Test1View: onTouchEvent(2131231055):0 V/Test1View: onTouchEvent(2131231056):0 V/Test1View: onTouchEvent(2131231053):2 V/Test1View: onTouchEvent(2131231054):2 V/Test1View: onTouchEvent(2131231055):2 V/Test1View: onTouchEvent(2131231056):2 V/Test1View: onTouchEvent(2131231053):2 V/Test1View: onTouchEvent(2131231054):2 V/Test1View: onTouchEvent(2131231055):2 V/Test1View: onTouchEvent(2131231056):2 ... V/Test1View: onTouchEvent(2131231053):2 V/Test1View: onTouchEvent(2131231054):2 V/Test1View: onTouchEvent(2131231055):2 V/Test1View: onTouchEvent(2131231056):2 V/Test1View: onTouchEvent(2131231053):1 V/Test1View: onTouchEvent(2131231054):1 V/Test1View: onTouchEvent(2131231055):1 V/Test1View: onTouchEvent(2131231056):1 |
おぉ〜ちゃんとタッチイベントが来てますね、 MultiDispatchTouchFrameLayout を入れ子にしたFragment3も正常に動作しています。
ということで、普通のアプリじゃ使うことがなさそうな変態ViewGroupを作ってみました。お互いに動作がわからない任意のView同士を重ねて同じタッチイベントを処理できるようになります。役に立つかどうか分からなけど一応ソースコードはGitHubにあげたよ。
MultiDispatchTouchEventリポジトリon GitHub
ちゃんちゃん(^^)/~~~