イベントの伝達#
イベントは、Activity から View に段階的に伝達されます。
イベント分配の概念#
主に 3 つのメソッドで実現されます。
分配の重点メソッド | 機能 | 戻り値の意味 |
---|---|---|
タッチイベントのディスパッチ:ブール値 | 上層の View によってトリガーされ、ターゲット View に伝達される | 戻り値は、子 View の影響を受ける(true:処理された)onTouchEvent``dispatchTouchEvent |
onInterceptTouchEvent:boolean | 現在の View 内で、特定のイベントをインターセプトするかどうかを判断 | 戻り値は、そのイベントがインターセプトされたかどうかを示す(true:インターセプトされた) |
タッチイベント:ブール値 | 現在の View がインターセプトし、処理を開始することを示す | 戻り値は、そのイベントが消費されたかどうかを示す(true:処理された) |
public boolean dispatchTouchEvent(MotionEvent e) {
bool isEventConsume = false;
if(onInterceptTouchEvent(e)) {
isEventConsume = onTouchEvent(e);
} else {
isEventConsume = child.dispatchTouchEvent(e);
}
return isEventConsume;
}
分配の順序は dispatchTouchEvent -> onInterceptTouchEvent であり、消費されたかどうかに基づいてその後の進行を決定します。dispatchTouchEvent の再帰呼び出しは、消費イベントの View が見つかるまで続きます。View は木構造であり、もしその View がクリックイベントをインターセプトした場合、onTouch がトリガーされます。onTouch がそのイベントを消費しなければ、onTouchEvent メソッドに渡されます。onTouchEvent 内で onClick イベントが発生します。
Activity がイベントを受信#
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// ここでのgetWindowはPhoneWindowクラスです
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//PhoneWindow.java
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java
//DecorViewはFrameLayoutを継承しており、FrameLayoutはdispatchTouchEventメソッドをオーバーライドしていないため、
//親クラスを探す必要があります。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
// ViewGroup.java
// ViewGroupはViewを継承しています。
// ViewGroupはdispatchTouchEventをオーバーライドしているため、Viewに進む必要はありません。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
}
イベント処理#
上記の分析を通じて、クリックイベントがどのようにDecorView#ViewGroup
に伝達されるかがわかります。ここでは、ViewGroup のクリックイベントをさらに分析します。
まず、いくつかの MotionEvent を理解します。
MotionEvent イベント | アクション | その他 |
---|---|---|
ACTION_DOWN | 指が下に押される | インターセプト、非インターセプトに分かれる |
ACTION_UP | 指が上に上がる | イベントが終了 |
ACTION_MOVE | 画面上をスライド | 複数回トリガーされる |
ACTION_CANCEL | イベントがキャンセルされる | 上層によってインターセプトされたときにトリガーされる |
ViewGroup 処理 - ChildView がACTION_DOWN
をインターセプト#
Down イベントは一度だけトリガーされます(単一タッチ、マルチタッチでは複数回トリガーされます)。インターセプトとは、その ViewGroup が自分でイベントを処理することであり、ChildView には分配されません。
各クリックイベントは Action Down から始まります。詳細は以下の注釈に注意してください。主に行うことは(ここでは主な処理項目を列挙します)。
以前のイベントをクリアします:resetTouchState メソッドに注意してください。これは ViewGroup#dispatchTouchEvent イベントが ACTION_DOWN のときに実行されます。
Note
以前のイベントをクリアします:resetTouchState メソッドに注意してください。これは ViewGroup#dispatchTouchEvent イベントが ACTION_DOWN のときに実行されます。
// ViewGroup.java
// このViewGroupから始まり、クリックのターゲットに接続します。
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 現在のアクションを取得
final int action = ev.getAction();
// マスクとAND操作を行い、実際のアクションを取得
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Action Downのときにのみresetを実行
// @ cancelAndClearTouchTargetsメソッドを追跡
cancelAndClearTouchTargets(ev);
// @ resetTouchStateメソッドを追跡
resetTouchState();
}
}
...
return handled;
}
private void resetTouchState() {
// @ clearTouchTargetsメソッドを確認
clearTouchTargets();
resetCancelNextUpFlag(this);
// FLAG_DISALLOW_INTERCEPTをクリアし、ViewGroupがインターセプトできるようにする
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
// 最初のクリックViewのすべてのイベントをクリアします
private void clearTouchTargets() {
// TouchTargetは単方向リストです
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
// TouchTargetを再利用できるように回収します
target.recycle();
target = next;
} while (target != null);
// メンバーmFirstTouchTargetをnullに設定します
mFirstTouchTarget = null;
}
}
ViewGroup#onInterceptTouchEventメソッドが呼び出されるかどうか:2 つの条件があり、さらに 1 つの FLAG の判断があります:
- 現在は ACTION_DOWN イベントです
- すでに ChildView がこのクリックイベントを処理しています(子 View がイベントを処理すると、mFirstTouchTarget に値が設定されます)
- 現在の ViewGroup がインターセプトを禁止されているかどうか(一般的に ViewGroup が受信したとき、禁止されていなければ、onInterceptTouchEvent メソッドが実行されます)
ACTION_DOWN
// ViewGroup.java
// このViewGroupから始まり、クリックのターゲットに接続します。
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 現在のアクションを取得
final int action = ev.getAction();
// マスクとAND操作を行い、実際のアクションを取得
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Action downのとき、FLAG_DISALLOW_INTERCEPTを初期化します
final boolean intercepted;
// ここには2つの判断があり、ViewGroup自身のonInterceptTouchEventメソッドを呼び出すかどうかを決定します
// 1. 現在はACTION_DOWNです
// 2. すでにChildViewがこのクリックイベントを処理しています
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// インターセプトが禁止されているかどうかを判断します
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 自身のonInterceptTouchEventメソッドを呼び出します
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 変更された場合に備えてアクションを復元します
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
...
return handled;
}
もし ViewGroup 自身がインターセプトしなければ、ChildView に再帰的に分配されます:それぞれの ChildView に対してViewGroup#dispatchTransformedTouchEvent
を実行し、ChildView がイベントを消費すれば true を返し、ループを抜けます。そうでなければ、次の ChildView に問い合わせます。
// ViewGroup
// このViewGroupから始まり、クリックのターゲットに接続します。
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
// マスクとAND操作を行い、実際のアクションを取得
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Action downのとき、FLAG_DISALLOW_INTERCEPTを初期化します
final boolean intercepted;
// 自身のViewGroupがイベントをインターセプトできるかどうかを判断します
// 現在、ViewGroupはインターセプトしないと仮定します
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
// このViewGroup内のChildViewの順序を再配置します
final ArrayList<View> preorderedList =
buildTouchDispatchChildList();
...
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// アニメーション中かどうか canReceivePointerEvents
// クリックがchild要素内にあるかどうか isTransformedTouchPointInView
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// クリック中のViewを取得します
// 新しいViewイベントは非nullで返されます
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// それが処理しているものに加えて新しいポインタを与えます。
// ChildViewがクリックを受け取った
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// 重点はdispatchTransformedTouchEventメソッドです
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ChildViewがイベントを処理しました
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// addTouchTargetメソッド
// 内部でmFirstTouchTargetに値を設定します
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// ループを抜けます
break;
}
}
}
}
}
}
...
return handled;
}
ViewGroup#dispatchTransformedTouchEvent
メソッド:ViewGroup はこのメソッドを通じてイベントを ChildView に渡します。もし渡された child が null であれば、ViewGroup の親の dispatchTouchEvent を呼び出し、null でなければ指定された View の dispatchTouchEvent を呼び出します(現在の状況では View オブジェクトが渡されるため、指定されたView#dispatchTouchEvent
メソッドにジャンプします)。
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event,
boolean cancel,
View child,
int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
// Child viewがnullであればViewGroupのParentに戻ります
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
ViewGroup#addTouchTarget
:メンバーmFirstTouchTarget
に値を設定します。
ViewGroup#addTouchTarget
:mFirstTouchTarget
メンバーを装備します。
// ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
// 再利用可能なTouchTargetオブジェクトを取得します
final TouchTarget target = TouchTarget
.obtain(child, pointerIdBits);
// 前のイベントのViewに接続します
target.next = mFirstTouchTarget;
// クリックイベントを処理する最初のView
mFirstTouchTarget = target;
return target;
}
ChildView がイベントを受信し、インターセプトした後(メソッドを通じて)、ViewGroup#mFirstTouchTarget
に値が設定され、ループを抜けます。dispatchTouchEvent
ChildView がイベントを処理した後、ViewGroup に戻って処理を続けます。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
if (mFirstTouchTarget == null) {
...
} else {
...
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// イベントを分配します
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
...
return handled;
}
ViewGroup 処理 - ChildView がACTION_DOWN
をインターセプトしない#
ViewGroup ChildView がイベントをインターセプトする:処理方法は同じです(前のセクション)。ここでは異なる部分を見ていきます。このイベントは ViewGroup 自身が処理します(dispatchTransformedTouchEvent メソッド)。
// ViewGroup.java
// ViewGroupから始まります
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Action Downのときにのみresetを実行
cancelAndClearTouchTargets(ev);
resetTouchState();
}
}
...
return handled;
}
以前のイベントをクリアします:resetTouchState メソッドに注意してください。これはViewGroup#dispatchTouchEvent
、イベントがACTION_DOWN
のときに実行されます。 ViewGroup#onInterceptTouchEvent
メソッドが呼び出されるかどうか:2 つの条件があり、さらに 1 つの FLAG の判断があります。現在はイベントACTION_DOWN
、すでに ChildView がこのクリックイベントを処理しています(子 View がイベントを処理すると、mFirstTouchTarget に値が設定されます)。現在の ViewGroup がインターセプトを禁止されているかどうか(一般的に ViewGroup が受信したとき、禁止されていなければ、onInterceptTouchEvent メソッドが実行されます)ACTION_DOWN
。もし ViewGroup 自身がインターセプトしなければ、ChildView に再帰的に分配されます:それぞれの ChildView に対してViewGroup#dispatchTransformedTouchEvent
を実行し、ChildView がイベントを消費すれば true を返し、ループを抜けます。
// ViewGroup
// ViewGroupから始まります
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
if (!canceled && !intercepted) {
// 現在、ChildViewがこのイベントを処理する必要はありません
}
}
...
}
ChildView が受信しますが、すべてのイベントをインターセプトしない場合、ViewGroup#mFirstTouchTarget は null になります。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
// 現在の状況では、ChildViewがイベントを処理する必要はありません
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
...
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
// ViewGroupのParent dispatchTouchEventに戻ります
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
}
...
return handled;
}
View がイベントを受信 - ACTION_UP
を処理#
View は最初に onTouch に分配され、処理されていなければ onTouchEvent に分配されます。順序は次のとおりです。
- 最初に onTouch インターフェースが実行され、すでに処理されていれば、onTouchEvent はイベントを受信しません
onTouch
- onTouch がこのイベントを処理しなければ、その View の onTouchEvent がイベントを処理します
onTouchEvent
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
// イベントがonTouchに分配されます
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
// このViewが有効かどうかを判断します
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// onTouchEventを分析します
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
View#onTouchEvent
メソッドでは、onClick、onLongClick インターフェースの呼び出しが見られます。
// View.java
// PerformClickはこのViewのクリックイベントを表します
private PerformClick mPerformClick;
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLEのいずれかを設定すれば、
// このViewはクリック可能です
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// クリックイベントの代理TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
// 長押しタスクmHasPerformedLongPress
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 長押しコールバックを削除します
removeLongPressCallback();
// 押された状態であった場合のみクリックアクションを実行します
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// performClickInternalを分析します
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
}
}
}
ここでは、View のクリックイベントが Handler を通じて Click クリックタスク(Runnable)を送信することが見られます。これにより、クリックイベントのブロッキングが防止されます。
// View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// ClickタスクをHandlerに入れます
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
// performClickを分析します
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// OnClickを実行します
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
ViewGroup - ACTION_DOWN#
- イベントが ViewGroup によってインターセプトされるかどうかを判断します:
ViewGroup がイベントをインターセプトする -> 直接 3 に進みます
ViewGroup がインターセプトしない -> まず 2 に進み、次に 3 に進みます - ViewGroup がインターセプトせず、いずれかの ChildView がイベントをインターセプトして処理します
ViewGroup#newTouchTarget
に値が設定されます
自身の View に分配します(親クラスを呼び出します)super.dispatchTouchEvent
// ViewGroup.java
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
- イベントの分配、処理:2 つの状況があります。
すべての ChildView がこのイベントをインターセプトしないため、mFirstTouchTarget は null であり、最後の View に相当します。
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
すべての ChildView がインターセプトするため、mFirstTouchTarget は null ではなく、while ループは 1 回だけ実行されます。なぜなら、ChildView が処理したからです(分配時に処理されます)。
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
ACTION_MOVE - 受信した MOVE を確認#
Move イベントは、最初に DecorView という ViewGroup を通過し、イベントを実行する際にいくつかのことを知る必要がありますACTION_MOVE
。ViewGroup はクリックのフラグをクリアしません。TouchTarget 型のmFirstTouchTarget
要素は空ではありません(すでに ChildView 要素が処理されています)。ACTION_MOVE イベントの判断フローチャートは以下の通りです。主に行うことは、ViewGroup がイベントをインターセプトするかどうかです(以下はデフォルトでインターセプトしないと仮定します)。インターセプトしない場合、Move イベントは分配されず、処理されません。同様に、ViewGroup#dispatchTouchEventメソッドを観察し、ここから分析を開始します。リセット段階には入らないでしょう。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
...
}
}
現在の ViewGroup がインターセプトを禁止されているかどうかを判断します。禁止されていなければ、自身のメソッドを呼び出してインターセプトを確認しますFLAG_DISALLOW_INTERCEPT
onInterceptTouchEvent
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// インターセプトを確認します。
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// ViewGroupがインターセプトを禁止しているかどうかを判断します
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// デフォルトではViewGroupはインターセプトしないと仮定します
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 変更された場合に備えてアクションを復元します
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
これはイベントではないため、最初のイベント分配段階ACTION_DOWN
には入らないでしょう。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// Downイベントではありません
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
}
}
...
}
ACTION_MOVE
の重点はイベントの分配です。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// alreadyDispatchedToNewTouchTargetはfalseです
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// dispatchTransformedTouchEventメソッドを分析してChildViewに分配します
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
}
次に、dispatchTransformedTouchEvent メソッドを通じて、ChildView の dispatchTouchEventACTION_MOVE
イベントを分配します。
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)
...
if (child == null) {
...
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
}
ACTION_MOVE - ViewGroup が MOVE をインターセプト#
現在の状況を理解します:すでに ChildView が ACTION_DOWN イベントを処理しており、カスタム ViewGroup を作成し、ViewGroup#onInterceptTouchEvent
メソッドをコピーします。ACTION_MOVE
のときに true を返してインターセプトします。
// カスタムViewGroup.java
public class MyViewGroup extends View {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_MOVE) {
// ViewGroupがイベントを消費します
return true;
}
return super.onInterceptTouchEvent(ev);
}
}
ACTION_MOVE イベントは複数回発生します。最初の ACTION_MOVE(ParentView から入ってくる)の目的は:
- ChildView のイベントをキャンセルします(イベントが ACTION_CANCEL に変更されます)
ViewGroup#mFirstTouchTarget
を null に設定します。この時、ParentView はイベントを処理しません。
結果:この時、ChildView は ACTION_CANCEL イベントを受け取ります(イベントがキャンセルされ、以降は ViewGroup がイベントを処理します)。
/**
* ViewGroup.java
*
* onInterceptTouchEventメソッドをオーバーライドすると、Moveイベントはtrueになるため、
* cancelChildもtrueになります。
*
* この時、イベントはACTION_CANCELに変更され、以下のChildViewはACTION_CANCELイベントを受け取ります。
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// ACTION_MOVEが最初に入る
// interceptedはtrueなので、cancelChild = true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// mFirstTouchTarget = next、nextはnull
// したがって、mFirstTouchTarget = null
if (cancelChild) {
if (predecessor == null) {
// nextがnullなので、mFirstTouchTargetがnullになります
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// cancelがtrue
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// イベントをACTION_CANCELに変更します
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
...
} else {
// ChildViewに分配されるイベントはキャンセルされます
// イベントが上層によってインターセプトされたときにトリガーされます
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
return handled;
}
2 回目の ACTION_MOVE(ParentView から入ってくる)では、mFirstTouchTarget が null になるため、ParentView はイベントを分配しません。
結果:ParentView は自分自身でイベントを処理します。
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 最初のACTION_MOVEのとき、mFirstTouchTargetがnullに設定されます
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
...
} else {
intercepted = true;
}
// intercepted = true
if (!canceled && !intercepted) {
...
}
if (mFirstTouchTarget == null) {
// ChildViewがnull
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// 完了。
transformedEvent.recycle();
return handled;
}
イベントの衝突#
なぜ衝突が発生するのか?それは、イベントは一つしか存在せず、応答するコンポーネントが自分が望む View オブジェクトでない場合、これをイベントの衝突と呼びます。
2 つの方法で処理されます。
- 内部インターセプト法:
ChildView#dispatchTouchEvent
が処理する必要があり、親コンテナ(ParentView)の onInterceptTouchEvent インターセプトと連携する必要があります。 - 外部インターセプト法(一般的に使用される):親 View が処理するだけです。
内部インターセプト - ChildView が処理#
ChildView 内でrequestDisallowInterceptTouchEvent
メソッドを使用して、FLAG_DISALLOW_INTERCEPT
要素を制御し、ParentView がイベントを分配しないようにします。
内部インターセプトの作業
ChildView は適切なタイミングでメソッドを通じて、ParentView にイベントをインターセプトしないよう要求しますrequestDisallowInterceptTouchEvent
public class MyView extends View {
int dispatchX, dispatchY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
// ParentViewにインターセプトしないよう要求します
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - dispatchX;
int deltaY = y - dispatchY;
// イベントをViewGroupに処理させます
if(Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
dispatchX = x;
dispatchY = y;
return super.dispatchTouchEvent(event);
}
}
親 View のメソッドをオーバーライドし、ACTION_DOWN のときにインターセプトしないように設定します。こうすることで、イベントが ChildView に渡ることができますonInterceptTouchEvent
public class MyViewGroup extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN) {
// ViewGroupはインターセプトしません
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
requestDisallowInterceptTouchEvent
の詳細説明:なぜ requestDisallowInterceptTouchEvent が ACTION_DOWN イベントを制御できないのか。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 初期のダウンを処理します。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// resetTouchStateを確認します
resetTouchState();
}
// インターセプトを確認します。
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // mFirstTouchTarget != null
// FLAG_DISALLOW_INTERCEPTを制御します
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// trueであれば、ParentViewは分配しません
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 変更された場合に備えてアクションを復元します
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//
}
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// すでにこの状態であれば、先祖もそうだと仮定します
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// 親に渡します
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
上記から、ViewGroup は ACTION_DOWN でresetTouchState()
メソッドを使用し、ここでこのフラグをクリアするため、イベント ACTION_DOWN は必ず分配されますFLAG_DISALLOW_INTERCEPT
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// クリア後、FLAG_DISALLOW_INTERCEPTは空になります
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// ViewGroup#onInterceptTouchEventを判断します
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 変更された場合に備えてアクションを復元します
} else {
intercepted = false;
}
}
...
}
Note
ViewGroup 内でonInterceptTouchEvent
をオーバーライドし、ViewGroup が ACTION_DOWN イベントのときに false を返すようにします(インターセプトしない)。こうすることで、ACTION_DOWN が分配され、ChildView に渡ることができます。他のアクションはインターセプトされます。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == ACTION_DOWN) {
return false;
}
return true;
}
外部インターセプト - ParentView が処理#
ParentView がイベントのインターセプトを処理するのは比較的簡単です。ParentView#onInterceptTouchEvent
をオーバーライドして、どのタイミング(条件)でイベントをインターセプトするかを決定します。
public class MyExternalViewGroup extends ViewGroup {
int interceptX, interceptY;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
boolean defaultIntercept = super.onInterceptTouchEvent(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - interceptX;
int deltaY = y - interceptY;
// 水平スワイプでインターセプト
if(Math.abs(deltaX) > Math.abs(deltaY)) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
default:
intercept = defaultIntercept;
break;
}
interceptX = x;
interceptY = y;
return intercept;
}
}