This article is the third installment of Android event distribution
1. Event Distribution — Part 1 of the Tetralogy of Event Distribution
Analysis of Nested Sliding Events
3. CoordinatorLayout Event Analysis
4. Put All Together
At present, most of the online event distribution analysis is to use the logic of linear thinking to analyze, there are even flow chart analysis, I deeply think otherwise. The data structure of Android View is tree. We must study event distribution with tree traversal to truly understand it. In this article I will use tree thinking to get you familiar with CoordinatorLayout event distribution.
Tree the View
Suppose you have a scenario where the CoordinatorLayout is the root View of the layout. It has three sub-views, namely, VP1, vp2, and vp3. Their corresponding behaviors are called vp1 Behavior, VP2 Behavior, and VP3 Behavior respectively. Vp1 and VP2 have view1, view2, view3 and view4, view5, view6 respectively. The children of VP3 are view7, view8, view9.
Look at the scene above it feels very convoluted, isn’t it a little confusing? Will you end up in a fog if you use linear thinking or flow charts to explain everything to you? Let’s turn it into a tree like this and see if it suddenly becomes clear.
Behavior’s onInterceptTouchEvent, onTouchEvent methods
Now that we’ve covered the scenario, let’s briefly introduce Behavior. It’s a static inner class for CoordinatorLayout. You can set the direct child View of CoordinatorLayout in the layout file using the app:layout_behavior property, or you can set it in code. Its role is very powerful, there are the following:
- CoordinatorLayout changes the traditional event distribution mechanism
- Coordinate Layout for nested slide event processing
- CoordinatorLayout handles the position changes of dependent subviews
In this article I will focus on how CoordinatorLayout changes the traditional event distribution mechanism. When it comes to event distribution, it is inevitable that we need to talk about the following three methods and see how they are implemented in CoordinatorLayout.
dispatchTouchEvent | onInterceptTouchEvent | onTouchEvent |
---|---|---|
Without rewriting | Have to rewrite | Have to rewrite |
- The dispatchToucheEvent method was not rewritten in the CoordinatorLayout source code, so Dispatch still uses the ViewGroup distribution mechanism.
- OnInterceptTouchEvent and onTouchEvent have been overwritten, and we will examine them in sequence, following the traditional event distribution mechanism.
We’ll start by Posting their source code. Without further understanding, we can get a sense that they’re not many lines of code, and they all end up calling the performIntercept method
CoordinatorLayout#onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors(true);
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
Copy the code
CoordinatorLayout#onTouchEvent
@Override @SuppressWarnings("unchecked") public boolean onTouchEvent(MotionEvent ev) { boolean handled = false; boolean cancelSuper = false; MotionEvent cancelEvent = null; final int action = ev.getActionMasked(); if (mBehaviorTouchView ! = null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) { // Safe since performIntercept guarantees that // mBehaviorTouchView ! = null if it returns true final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams(); final Behavior b = lp.getBehavior(); if (b ! = null) { handled = b.onTouchEvent(this, mBehaviorTouchView, ev); } } // Keep the super implementation correct if (mBehaviorTouchView == null) { handled |= super.onTouchEvent(ev); } else if (cancelSuper) { if (cancelEvent == null) { final long now = SystemClock.uptimeMillis(); CancelEvent = MotionEvent. Obtain (now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); } super.onTouchEvent(cancelEvent); } if (cancelEvent ! = null) { cancelEvent.recycle(); } if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { resetTouchBehaviors(false); } return handled; }Copy the code
private boolean performIntercept(MotionEvent ev, final int type) { boolean intercepted = false; boolean newBlock = false; MotionEvent cancelEvent = null; final int action = ev.getActionMasked(); final List<View> topmostChildList = mTempList1; getTopSortedChildren(topmostChildList); // Let topmost child views inspect first final int childCount = topmostChildList.size(); for (int i = 0; i < childCount; i++) { final View child = topmostChildList.get(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Behavior b = lp.getBehavior(); if ((intercepted || newBlock) && action ! = MotionEvent.ACTION_DOWN) { // Cancel all behaviors beneath the one that intercepted. // If the event is "down" then we don't have anything to cancel yet. if (b ! = null) { if (cancelEvent == null) { final long now = SystemClock.uptimeMillis(); CancelEvent = MotionEvent. Obtain (now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); } switch (type) { case TYPE_ON_INTERCEPT: b.onInterceptTouchEvent(this, child, cancelEvent); break; case TYPE_ON_TOUCH: b.onTouchEvent(this, child, cancelEvent); break; } } continue; } if (! intercepted && b ! = null) { switch (type) { case TYPE_ON_INTERCEPT: intercepted = b.onInterceptTouchEvent(this, child, ev); break; case TYPE_ON_TOUCH: intercepted = b.onTouchEvent(this, child, ev); break; } if (intercepted) { mBehaviorTouchView = child; } } // Don't keep going if we're not allowing interaction below this. // Setting newBlock will make sure we cancel the rest of the behaviors. final boolean wasBlocking = lp.didBlockInteraction(); final boolean isBlocking = lp.isBlockingInteractionBelow(this, child); newBlock = isBlocking && ! wasBlocking; if (isBlocking && ! newBlock) { // Stop here since we don't have anything more to cancel - we already did // when the behavior first started blocking things below this point. break; } } topmostChildList.clear(); return intercepted; }Copy the code
Here I have posted the source code of three methods related to event distribution, in order to give you a preliminary impression, without going into depth. I will explain these methods step by step in combination with cases.
The difference between traditional event distribution and CoordinatorLayout event distribution
For simplicity, onIntercept is used instead of onInterceptTouchEvent, onTouch is used instead of onTouchEvent and CL is used instead of CoordinatorLayout
- Traditional event distribution diagrams return false for all viewgroups onIntercept and false for all viewgroups and View onTouch
From the article “In-depth Traversal of Android Event Distribution mechanism”, one of the four steps of event distribution, we can know the event invocation process as follows
vp0#onIntercept -> vp3#onIntercept -> vp4#onIntercept -> view9#onTouch -> view8#onTouch -> view7#onTouch -> vp4#onTouch -> vp3#onTouch -> vp2#onIntercept -> view6#onTouch -> view5#onTouch -> view4#onTouch -> vp2#onTouch -> vp1#onIntercept -> view3#onTouch -> view2#onTouch -> view1#onTouch -> vp1#onTouch -> vp0#onTouch
If you don’t understand the call flow, I suggest you draw lines in the tree with a pen. You may find that Down events are distributed via a backward iteration in a traditional event distribution mechanism.
- In the CL event distribution diagram, we assume that onIntercept and onTouch of all behaviors and views return false.
So let’s do a Demo. Let’s Log it
/** * Scenario 1: None of the Behaviors intercepts or handles events. All the View not intercept events, all the View does not handle events * / class CoordinatorLayoutEventOneActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val coordinatorLayout: CoordinatorLayout = CoordinatorLayout(this) //vp1 vp2 VP3 Default does not intercept events Default does not distribute events val vp1 = MyFrameLayout(this) vp1.name = "vp1" val vp1Params = CoordinatorLayout.LayoutParams( CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.MATCH_PARENT ) vp1Params.behavior = MyBehavior().apply { name = "VP1 Behavior" } coordinatorLayout.addView(vp1, vp1Params) val vp2 = MyFrameLayout(this) vp2.name = "VP2" val vp2Params = CoordinatorLayout.LayoutParams( CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.MATCH_PARENT ) vp2Params.behavior = MyBehavior().apply { name = "VP2 Behavior" } coordinatorLayout.addView(vp2, vp2Params) val vp3 = MyFrameLayout(this) vp3.name = "VP3" val vp3Params = CoordinatorLayout.LayoutParams( CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.MATCH_PARENT ) vp3Params.behavior = MyBehavior().apply { name = "VP3 Behavior" } coordinatorLayout.addView(vp3, vp3Params) vp1.addView(MyView(this).apply { name = "view1" }) vp1.addView(MyView(this).apply { name = "view2" }) vp1.addView(MyView(this).apply { name = "view3" }) vp2.addView(MyView(this).apply { name = "view4" }) vp2.addView(MyView(this).apply { name = "view5" }) vp2.addView(MyView(this).apply { name = "view6" }) val vp4 = MyFrameLayout(this).apply { name = "VP4" } vp3.addView(vp4) vp4.addView(MyView(this).apply { name = "view7" }) vp4.addView(MyView(this).apply { name = "view8" }) vp4.addView(MyView(this).apply { name = "view9" }) setContentView(coordinatorLayout) } }Copy the code
Print logs in custom event handlers for MyFrameLayout, MyView, and MyBehavior
override fun onInterceptTouchEvent( parent: CoordinatorLayout, child: View, ev: MotionEvent ): Boolean { Log.d(LogTag.tag,"$name onInterceptTouchEvent "+MotionEvent.actionToString(ev.action)) return interceptValue } @RequiresApi(Build.VERSION_CODES.KITKAT) override fun onTouchEvent(parent: CoordinatorLayout, child: View, ev: MotionEvent): Boolean { Log.d(LogTag.tag,"$name onTouchEvent "+MotionEvent.actionToString(ev.action)) return touchValue }Copy the code
The log is printed as follows, ignoring the Cancel event and marking the uplink number
VP1 Behavior onInterceptTouchEvent ACTION_CANCEL VP2 Behavior onInterceptTouchEvent ACTION_CANCEL VP3 Behavior onInterceptTouchEvent ACTION_CANCEL 1. VP3 Behavior onInterceptTouchEvent ACTION_DOWN 2. VP2 Behavior onInterceptTouchEvent ACTION_DOWN 3. VP1 Behavior onInterceptTouchEvent ACTION_DOWN 4. VP3 onInterceptTouchEvent ACTION_DOWN 5. VP4 onInterceptTouchEvent ACTION_DOWN 6. view9 onTouchEvent ACTION_DOWN 7. view8 onTouchEvent ACTION_DOWN 8. view7 onTouchEvent ACTION_DOWN 9. VP4 onTouchEvent ACTION_DOWN 10. VP3 onTouchEvent ACTION_DOWN 11. VP2 onInterceptTouchEvent ACTION_DOWN 12. view6 onTouchEvent ACTION_DOWN 13. view5 onTouchEvent ACTION_DOWN 14. view4 onTouchEvent ACTION_DOWN 15. VP2 onTouchEvent ACTION_DOWN 16. VP1 onInterceptTouchEvent ACTION_DOWN 17. view3 onTouchEvent ACTION_DOWN 18. view2 onTouchEvent ACTION_DOWN 19.view1 onTouchEvent ACTION_DOWN 20. VP1 onTouchEvent ACTION_DOWN 21. Behavior onTouchEvent ACTION_DOWN 22. VP2 Behavior onTouchEvent ACTION_DOWN 23. VP1 Behavior onTouchEvent ACTION_DOWNCopy the code
We can draw the conclusion that:
- When CL calls onIntercept, it will call the onIntercept of Behavior one by one from the last subview (note that anchor, dependent and other situations are not considered here, and topological ordering will change the order of traversal in these cases. Since topological ordering is not included in this paper, Please check for yourself).
- When CL calls onTouch, it also calls Behavior’s onTouch from back to front
The difference between traditional event distribution and CL event distribution is as follows: When CL processes an event, it gives priority to the Behavior of its child View to handle it. If the Behavior of its child View does not handle the event, the traditional event distribution mechanism will distribute the event
More scenes
The onIntercept and onTouch methods of the Behavior class return Boolean values, so there are at least four cases depending on the return value. We’ve looked at the simplest case,case1
case | onInterceptTouchEvent | onTouchEvent |
---|---|---|
case1 | false | false |
case2 | true | false |
case3 | false | true |
case4 | true | true |
There are also four cases of viewgroups
case | onInterceptTouchEvent | onTouchEvent |
---|---|---|
case1 | false | false |
case2 | true | false |
case3 | false | true |
case4 | true | true |
There are a total of 4X4=16 species, and space is not enough to list them all. I have uploaded the code to Github. I suggest you download it, experience it yourself, and consolidate your gains.
The DEMO address
My open source project
I open source a convenient RecyclerView top sucking Android library, welcome you to visit the top sucking project address, if you use this library, please put forward your valuable opinions.
It currently supports the following features:
- Support complex top View function
- Support multiple types of top suction function
- Enable or disable the top suction function
- Supports setting top offset
- Supports ItemDecoration and ItemAnimator
- Support RecyclerView data changes and scrolling to the specified location
- More features
If you have any questions, please follow bytecode’s official wechat account, or leave a comment directly in the comments section