Androidイベント配信メカニズムとネストによるタッチイベントの競合解決策
17342 ワード
スライドを実現するための一般的な方法は、 を行う. Scrollerを使用してスライド を行う.
上記のコードは基本的にScrollerの基本ソケットを用い,まずプロパティアニメーションを使用してスライド イベント配信ソースプロセス
イベント配布のフローチャート:
まず、いくつかの問題を持ってソースコードを見に行きます.そうしないと、ソースコードの多くの枝葉末節に干渉されます.
1.Actionの出発点はどこですか.
タッチパネルイベントが最初に渡されるのはActivityであるため,イベント配信の起点はActivityのdispatchTouchEventメソッドである.
このコードからActivityがgetWindow()を呼び出すsuperDispatchTouchEvent(MotionEvent ev)メソッドがわかります.ここでAndroidのWindowオブジェクトには実装クラスPhoneWindowが1つしかないことを知っていますので、PhonwViewのsuperDispatchTouchEvent(MotionEvent ev)メソッドを見てみましょう.
ここでは実際にDecorViewに渡されたsuperDispatchTouchEvent(MotionEvent ev)メソッドで処理されているのが見えますが、DecorViewをよく知っているのは、実際にパッケージされたFrameLayout、つまりTop-levelのView Groupであることがわかります.ここから私たちがよく知っているViewイベントの配布メカニズムの始まりです.
2.View GroupはどのようにしてサブViewにイベントを配布しますか?
まず、ViewGroupのブロックイベントの呼び出しを無視し、最も簡単な状況を考慮します.
コードから、イベントをサブViewに配布する方法は、
コードから、childがnullに等しくない場合、childのdispatchTouchEvent
ここでは、
コードからこのもう一つのTouchTargetのチェーンテーブルが見えます.
もちろん、
コードから主に2つの点を判断していることがわかります
1.クリックしたポイントがサブビューの領域内にあるか.
2.サブViewがアニメーションを実行しているかどうか.
上記のいずれかに該当する場合は直接continueし,ループの次の項目を行う.
3.ビューグループのonInterceptTouchEvent(MotionEvent ev)メソッドがトリガーされるのはどのような場合ですか?
コードからonInterceptTouchEvent(ev)を実行する条件がいくつか見られ、私たちは一つずつ何を代表しているのかを見ることができます.
を見ることができます
ここには
4.View Groupのすべてのタッチスクリーンイベントをブロックしないようにしてもらえますか?
この問題は実は否定的で、コードから私たちは見ることができます
コードからタッチパネルイベントがACTIONであることがわかります.DOWNではすべてのTouchTargetsとFLAG_がリセットされますDISALLOW_INTERCEPTなので、タッチパネルイベントはACTION_DOWNの場合、親View Groupは必ず
5.setOnTouchListneer後にonTouchEvent(MotionEvent ev)メソッドがトリガーされますか?
Viewの
このコードから、
6.あるViewがイベントの処理を開始すると、同じイベントシーケンスのイベントは他のView処理に渡されますか?
ここから、
ネストによるスライドイベントの競合の解決方法自分でイベントの配布を処理する過程は衝突を解決するこのような方式は自分でイベントの配布とブロックを処理する必要があり、比較的複雑である.同時に外部処理と内部処理の2種類に分けられる.開発者のイベント配信メカニズムの理解に対する要求は比較的高い.面接でよく聞かれる質問を簡単に挙げると、 NestedScrollingの実装案に基づくNestedScrollingの主な考え方は、Childがイベントの受信者として働き、ChildがMoveイベントを受信すると、 CoordinatorLayout、Behaviorに基づく実装案(後にまとめを専門に書く) 参考資料
Android Scroller完全解析、Scrollerについてあなたが知っていることはすべて
<>:任玉剛
Androidネストスライド機構
NestedScrollingは簡単なネストスライドを実現します
Android NestedScrolingメカニズム完全解析ネストスライドを回す
Awesome-Android-Interview
scrollTo()
、scrollBy()
によりスライド @Override
public boolean onTouchEvent(MotionEvent event) {
case ACTION_UP:
scroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
上記のコードは基本的にScrollerの基本ソケットを用い,まず
startScroll(int startX, int startY, int dx, int dy)
を呼び出し,次にcomputeScroll()
メソッドを書き換えてScrollerが算出したX,Y座標を取得してscrollTo(int x, int y)
を呼び出してスライドする.イベント配布のフローチャート:
まず、いくつかの問題を持ってソースコードを見に行きます.そうしないと、ソースコードの多くの枝葉末節に干渉されます.
1.Actionの出発点はどこですか.
タッチパネルイベントが最初に渡されるのはActivityであるため,イベント配信の起点はActivityのdispatchTouchEventメソッドである.
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
このコードからActivityがgetWindow()を呼び出すsuperDispatchTouchEvent(MotionEvent ev)メソッドがわかります.ここでAndroidのWindowオブジェクトには実装クラスPhoneWindowが1つしかないことを知っていますので、PhonwViewのsuperDispatchTouchEvent(MotionEvent ev)メソッドを見てみましょう.
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
ここでは実際にDecorViewに渡されたsuperDispatchTouchEvent(MotionEvent ev)メソッドで処理されているのが見えますが、DecorViewをよく知っているのは、実際にパッケージされたFrameLayout、つまりTop-levelのView Groupであることがわかります.ここから私たちがよく知っているViewイベントの配布メカニズムの始まりです.
2.View GroupはどのようにしてサブViewにイベントを配布しますか?
まず、ViewGroupのブロックイベントの呼び出しを無視し、最も簡単な状況を考慮します.
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
コードから、イベントをサブViewに配布する方法は、
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
メソッドを1回実行することであり、このメソッドがtrueに戻った場合、すぐにループを停止し、タッチスクリーンイベントもこのサブViewによって消費されることがわかります.dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
が何をしたのか詳しく見ることができます. /**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Perform any necessary transformations and dispatch.
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
コードから、childがnullに等しくない場合、childのdispatchTouchEvent
dispatchTouchEvent(MotionEvent event)
メソッドが呼び出されることがわかる.childViewのイベント配信プロセスを続行します.ここでは、
addTouchTarget(child, idBitsToAssign);
という方法の具体的な役割に注目します. // First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
コードからこのもう一つのTouchTargetのチェーンテーブルが見えます.
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
でtrueを返した後、mFirstTouchTargetに値を割り当てます.このとき、mFirstTouchTargetはnullではありません.もちろん、
dispatchTransformedTouchEvent
が実行される前に、タッチスクリーンイベントを受信することが不可能なサブViewがフィルタリングされる.if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
コードから主に2つの点を判断していることがわかります
1.クリックしたポイントがサブビューの領域内にあるか.
2.サブViewがアニメーションを実行しているかどうか.
上記のいずれかに該当する場合は直接continueし,ループの次の項目を行う.
3.ビューグループのonInterceptTouchEvent(MotionEvent ev)メソッドがトリガーされるのはどのような場合ですか?
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
コードからonInterceptTouchEvent(ev)を実行する条件がいくつか見られ、私たちは一つずつ何を代表しているのかを見ることができます.
actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null
actionMasked == MotionEvent.ACTION_DOWN
これはよく理解できますタッチパネルイベントがACTIONであればDONWなら条件が成立します.mFirstTouchTarget != null
上記のソースコードの分析から分かるように、mFirstTouchTarget!=null
はタッチスクリーンイベントを表す前にすでにViewに消費されており、残りのイベントシーケンスはすべてそのViewに渡されて処理されます.!disallowIntercept
ここではコードfinal boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
ここには
FLAG_DISALLOW_INTERCEPT
というマークがありますが、このマークが0でなければdisallowIntercept
はtrueであり、このとき!disallowIntercept
は成立しません.では、この値をtrueにすれば?ここでは、requestDisallowInterceptTouchEvent(boolean disallowIntercept)
という方法に関連しています.この方法により、親Viewが子Viewのイベントをブロックしないようにすることができます.そのため、この方法もイベントの衝突を解決する方法の一つです.4.View Groupのすべてのタッチスクリーンイベントをブロックしないようにしてもらえますか?
この問題は実は否定的で、コードから私たちは見ることができます
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
コードからタッチパネルイベントがACTIONであることがわかります.DOWNではすべてのTouchTargetsとFLAG_がリセットされますDISALLOW_INTERCEPTなので、タッチパネルイベントはACTION_DOWNの場合、親View Groupは必ず
onInterceptTouchEvent(ev)
メソッドを実行します.5.setOnTouchListneer後にonTouchEvent(MotionEvent ev)メソッドがトリガーされますか?
Viewの
dispatchTouchEvent(MotionEvent event)
法を解析することができます ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
このコードから、
mOnTouchListener!=null
であればmOnTouchListener.onTouch(this, event)
を実行し、trueを返すとresult =true
であり、onTouchEvent(event)
はこれ以上実行されないため、onTouchEvent(event)
がまだ実行されているかどうかはmOnTouchListener.onTouch(this, event)
の戻り値を見る必要があることがわかります.6.あるViewがイベントの処理を開始すると、同じイベントシーケンスのイベントは他のView処理に渡されますか?
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
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;
}
}
ここから、
mFirstTouchTarget != null
であれば、タッチイベントViewGroupはブロックされず、mFirstTouchTarget
を巡ってdispatchTransformedTouchEvent()
が実行されることが分かる.ViewGroup
のonTouchEvent()
がいつ実行されるかmFirstTouchTarget
がnullの場合、ViewGroupのonTouchEvent()メソッドが呼び出されます.つまり2つの可能性がある.ACTION_DOWNの場合、ViewGroupのonInterceptTouchEvent()
はtrueに戻ります.2.childを1回呼び出したdispatchTouchEvent()
タッチイベントは消費されず、このときもViewGroupのonTouchEvent()
が実行されるネストによるスライドイベントの競合の解決方法
ScrollView ListView
はどのように処理すればよいのか、通常はスライドイベントがScrollView
にブロックされて消費されるが、一般的にはView GroupがACTION_DONE
イベントをブロックしない場合はrequestDisallowInterceptTouchEvent(true);
を呼び出してScrollView
がスライドイベントをブロックしないようにすると、スライドイベントがListView
に正常に伝達され、ListViewも正常にスライドできるようになる. @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getY();
requestDisallowInterceptTouchEvent(true);
break;
return super.dispatchTouchEvent(ev);
}
しかし、この方法にも欠陥があり、ScrollViewにHeadViewが存在する場合、ListViewが一番上にスライドしたとき、インタラクティブなイベントをScrollViewに渡すとしたら?通常のイベント配布では、あるViewがイベントを消費すると、イベントシーケンスの次のイベントが消費されます.このとき、ScrollView
に手動でイベントを渡すことができますが、自分で書くと結合が非常に大きくなります.このように、 case MotionEvent.ACTION_MOVE:
boolean isMoveUp = mLastY - ev.getY() < 0;
if (isMoveUp && firstVisiblePosition == 0) {
Log.d("tset", "needParent consume!");
requestDisallowInterceptTouchEvent(false);
((ViewGroup)getParent()).onTouchEvent(ev);
return false;
} else if (!isMoveUp && lastVisiblePosition == getAdapter().getCount() - 1) {
Log.d("tset", "needParent consume!");
requestDisallowInterceptTouchEvent(false);
((ViewGroup)getParent()).onTouchEvent(ev);
return false;
}
break;
でAndroidはNestedScrollingベースのソリューションを提供してくれました.dispatchNestedPreScroll()
を呼び出してイベントをParentに渡して処理し、ParentがコールバックonNestedPreScroll()
を受信すると、Parentはイベントを消費する必要があるかどうか、距離のスライドを消費する必要があるかどうかを判断する.その後、Childは残りの消費されていない距離に従ってスライドを継続する.Android Scroller完全解析、Scrollerについてあなたが知っていることはすべて
<>:任玉剛
Androidネストスライド機構
NestedScrollingは簡単なネストスライドを実現します
Android NestedScrolingメカニズム完全解析ネストスライドを回す
Awesome-Android-Interview