イベントの伝達#
イベントは 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 操作を行い、実際の Action を取得
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 つの条件があり、さらに 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 操作を行い、実際の Action を取得
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Action down のとき、FLAG_DISALLOW_INTERCEPT を初期化します
final boolean intercepted;
// ここに 2 つの判断があり、ViewGroup 自身の onInterceptTouchEvent メソッドを呼び出すかどうかを決定します
// 1. 現在は ACTION_DOWN
// 2. すでに Child View がこのクリックイベントを処理しています
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 操作を行い、実際の Action を取得
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) {
// それに加えて新しいポインタを与えます。
// Child View がすでにクリックを受け取っています
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 つの条件があり、さらに FLAG の判断があります。現在はイベント ACTION_DOWN
であり、すでに ChildView がこのクリックイベントを処理しています(子 View がイベントを処理すると、mFirstTouchTarget に値が設定されます)。現在 ViewGroup が遮断を禁止されているかどうか(一般的に ViewGroup が受信したとき、遮断が禁止されていなければ、onInterceptTouchEvent メソッドが実行されます)。もし 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) {
// クリックタスクを 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 の dispatchTouchEvent ACTION_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
を空にします。この時、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 は空なので、mFirstTouchTarget は空になります
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 が空になるため、ParentView はイベントを分配しません。
結果:ParentView 自身がイベントを処理します。
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 最初の ACTION_MOVE では mFirstTouchTarget が空になります
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
...
} else {
intercepted = true;
}
// intercepted = true
if (!canceled && !intercepted) {
...
}
if (mFirstTouchTarget == null) {
// ChildView は空です
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;
}
}