1, location,
1.1 coordinate system
Here is the basic View coordinate system in Android. To get the position of a View, we can use two objects, a View and a MotionEvent. Here is what the location of some of their methods means:
The mLeft, mRight, mTop and mBottom variables in the View contain the coordinates of the View. You can obtain their meanings in the source code:
mLeft
: Specifies the distance between the left edge of the control and the left edge of its parent, in pixels.mRight
: Specifies the distance between the right edge of the control and the left edge of its parent control, in pixels.mTop
: Specifies the position of the upper edge of the control from the upper edge of its parent control, in pixels.mBottom
: Specifies the position of the lower edge of the control from the upper edge of its parent control, in pixels.
In addition, there are several methods in the View to obtain information such as the position of the control, which are essentially getters for the above four variables:
getLeft()
: that is,mLeft
;getRight()
: that is,mRight
;getTop()
: that is,mTop
;getBottom()
: that is,mBottom
;
So, we can get two methods to get the height and width of the View:
getHeight()
: that is,mBottom - mTop
;getWidth()
: that is,mRight - mLeft
;
The other two are the getX() and getY() methods in the View, which you need to distinguish from the MotionEvent method of the same name. When the control is not panned, getX() returns the same result as getLeft(), except that the former adds the panned distance to the latter:
getX()
: that is,mLeft + getTranslationX()
, that is, the left edge of the control plus the translation distance in the X direction;getY()
: that is,mTop + getTranslationY()
, that is, the upper edge of the control plus the translation distance in the Y direction;
Above is our View to get control position method comb, you can go to the source code to View them more detailed definition, that is more helpful to their understanding.
1.2 MotionEvent
MotionEvent is usually used when you touch listen on a control, which blocks information such as the location of the touch. Here we sort out the method of obtaining the position of the click event in MotionEvent, which mainly involves the following four methods:
MotionEvent.getX()
: Gets the distance of the click event from the left edge of the control, in pixels;MotionEvent.getY()
: Gets the distance of the click event from the edge of the control, in pixels;MotionEvent.getRawX()
: Gets the distance of the click event from the left edge of the screen, in pixels;MotionEvent.getRawY()
: Gets the distance of the click event from the edge of the screen, in pixels.
The other three typical actions in touch events are pressing, moving and lifting. We’ll use them in the following code examples to determine finger behavior and respond to it:
MotionEvent.ACTION_DOWN
: the act of pressing down;MotionEvent.ACTION_MOVE
: The act of moving your fingers across a screen;MotionEvent.ACTION_UP
: The act of lifting the finger.
2, sliding
There are several ways to implement View sliding:
2.1 the layout () method
Call the control’s Layout () method to slide, which is defined below:
public void layout(int l, int t, int r, int b) { /*... * /}Copy the code
The four parameters L, t, r, b represent the left, top, right, and bottom distance of the control relative to the parent control, corresponding to mLeft, mTop, mRight, and mBottom respectively. So, calling this method can change both the height and width of the control, but sometimes we don’t need to change the height and width of the control, just move it. So, we have methods offsetLeftAndRight() and offsetTopAndBottom(), which only pan the position of the control. Therefore, we can do the following code tests:
private int lastX, lastY; private void layoutMove(MotionEvent event) { int x = (int) event.getX(), y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: int offsetX = x - lastX, offsetY = y - lastY; getBinding().v.layout(getBinding().v.getLeft() + offsetX, getBinding().v.getTop() + offsetY, getBinding().v.getRight() + offsetX, getBinding().v.getBottom() + offsetY); break; case MotionEvent.ACTION_UP: break; }}Copy the code
The effect of the above code is that the specified control moves with the movement of a finger. Here we first record the position of the press, then record the position of the pan as the finger moves, and finally call Layout ().
2.2 offsetLeftAndRight () and offsetTopAndBottom ()
These two methods, mentioned above, only change the position of the control, not the size. We only need to make a few changes to the above code to achieve the same effect:
getBinding().v.offsetLeftAndRight(offsetX);
getBinding().v.offsetTopAndBottom(offsetY);
Copy the code
2.3 Changing Layout Parameters
We can also change the position of the control by getting and modifying its LayoutParams. After all, the object itself represents the layout of the control:
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getBinding().v.getLayoutParams();
lp.leftMargin = getBinding().v.getLeft() + offsetX;
lp.topMargin = getBinding().v.getTop() + offsetY;
getBinding().v.setLayoutParams(lp);
Copy the code
2.4 the animation
Animation can also be used to move the control. Animation is the transitionX and transitionY properties of the View:
getBinding().v.animate().translationX(5f);
getBinding().v.animate().translationY(5f);
Copy the code
We’ll talk more about animation later.
2.5 scrollTo () and scrollBy ()
The scrollBy() method calls scrollTo() internally, and here’s the source code. ScrollBy () means panning above the current position, while scrollTo() means panning to the specified position:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
Copy the code
By doing the same with the above code, we can achieve the same effect:
((View) getBinding().v.getParent()).scrollBy(-offsetX, -offsetY);
Copy the code
or
View parent = ((View) getBinding().v.getParent());
parent.scrollTo(parent.getScrollX()-offsetX, parent.getScrollY()-offsetY);
Copy the code
One more thing to note: Unlike offsetLeftAndRight() and offsetTopAndBottom() above, we use the negation of the panned value. The reason is simple, because to use these methods we need to call the parent container of the specified control (as above, we get the parent control first). When we want the control to move down and right relative to its previous position, we should have the parent container move up and left relative to its previous position. Because in fact the position of the control relative to the parent does not change, what changes is the position of the parent control. (Reference coordinate system is different)
2.6 Scroller
Above, our test code makes the specified control move with the finger, but what if we want the control to move from one position to another? Of course, they can be done, but this is done almost instantaneously, and the actual UI is not going to look good. So, to make the sliding process look smoother, we can use the Scroller to do it.
Before using Scroller, we need to instantiate a Scroller:
private Scroller scroller = new Scroller(getContext());
Copy the code
Then, we need to override the custom control’s computeScroll() method, which is called when the View is drawn. So, the implication here is that the computeScroll() method is called when the View is redrawn, and the computeScroll() method determines if it needs to scroll further, and if it needs to scroll further, the invalidate() method is called. This method causes the View to be redrawn further. So, it’s this constant redrawing that makes scrolling work.
The final determination of the end of the sliding effect is achieved by Scroller’s computeScrollOffset() method, which returns false when the scrolling stops so that the invalidate() method is not called and therefore the drawing is not continued. Here is a typical overwrite of the method:
@Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()) { ((View) getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY()); invalidate(); }}Copy the code
Then, we add a method to scroll to the specified position, inside which we use 2000ms to specify the time required to complete the entire slide:
public void smoothScrollTo(int descX, int descY) {
scroller.startScroll(getScrollX(), getScrollY(), descX - getScrollX(), descY - getScrollY(), 2000);
invalidate();
}
Copy the code
Once defined, we simply call our custom View’s smoothScrollTo() method whenever we need to scroll.
3, gestures
3.1 ViewConfiguration
In the ViewConfiguration class, a list of constants is defined to indicate the specified behavior. For example, TouchSlop is the minimum distance to slide. You can get the ViewConfiguration instance via viewConfiguration.get (context), and then get the definitions of these constants through its getter methods.
3.2 VelocityTracker
The VelocityTracker detects the speed of finger swiping and is very simple to use. Before using it, we obtain an instance using its static method Obtain (), and then call its addMovement(MotionEvent) method in the onTouch() method:
velocityTracker = VelocityTracker.obtain();
Copy the code
ComputeCurrentVelocity (int) computeCurrentVelocity(int) computeCurrentVelocity(int) computeCurrentVelocity(int) getXVelocity()
velocityTracker.computeCurrentVelocity((int) duration);
getBinding().tvVelocity.setText("X:" + velocityTracker.getXVelocity() + "\n"
+ "Y:" + velocityTracker.getYVelocity());
Copy the code
Essentially, the rate is calculated by dividing the change in length of a given time by the time slice that we pass in. When we have finished using VelocityTracker, we need to recycle the resources:
velocityTracker.clear();
velocityTracker.recycle();
Copy the code
3.3 GestureDectector
GestureDectector is used to detect finger gestures. Before using it we need to get an instance of GestureDetector:
mGestureDetector = new GestureDetector(getContext(), new MyOnGestureListener());
Copy the code
Here we use the GestureDetector constructor, passing in an OnGestureListener object. Here we use the MyOnGestureListener instance. MyOnGestureListener is a custom class that implements the OnGestureListener interface:
private class MyOnGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapUp(MotionEvent e) { ToastUtils.makeToast("Click detected"); return false; } @Override public void onLongPress(MotionEvent e) { LogUtils.d("Long press detected"); } @Override public boolean onDoubleTap(MotionEvent e) { LogUtils.d("Double tab detected"); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { LogUtils.d("Fling detected"); return true; }}Copy the code
In MyOnGestureListener, we override some of its methods. For example, click, double click, long press, etc. These methods are called when the corresponding gesture is detected.
We can then use the GestureDetector like this, simply by calling it in the control’s touch event callback:
getBinding().vg.setOnTouchListener((v, event) -> {
mGestureDetector.onTouchEvent(event);
return true;
});
Copy the code
4. Event distribution mechanism
4.1 Process of event transmission
When discussing event distribution, we first need to understand the structure of the View in Android. On Android, an Activity contains a PhoneWindow, and when we call setContentView() in the Activity, The PhoneWindow setContentView() method is called and a DecorView is generated in this method as the Activity’s successor View.
According to the above analysis, when a click event is triggered, the Activity is the first to receive the event. Because the Activity covers the entire screen, we need to have it receive events, which it then passes to the root View, which then passes down. This Narrows the search down to the top View. Of course, any parent can decide whether the event should be passed down or not, so we can roughly get the following graph of event passing:
The figure on the left shows the View and Window organization inside an Activity. The figure on the right can be seen as a slice of it, with the black arrows representing the passing of events. Here events are passed from bottom to top, and then from top to bottom. That is, large to small, constantly positioned to touch controls, where each parent container can decide whether to pass events on. (Note that if a parent container has more than one child, the traversal of the child elements is done from the top down, in the order shown).
Above we have analyzed the Android event passing process, I believe you have a general understanding. However, in order to understand the whole event transmission process specifically involved in what methods, how to function, we also need to analyze the source code.
4.2 Principle of Event transmission
When a touch event occurs, it is first received by the Activity, which then passes the event to the internal PhoneWindow via its dispatchTouchEvent(MotionEvent). The PhoneWindow then passes the event to the DecorView, which passes it to the root ViewGroup. The rest of the event passing is just between the ViewGroup and View. We can prevent events from passing to the PhoneWindow by overwriting the Activity’s dispatchTouchEvent(MotionEvent). In fact, the Window event-passing method will not be overridden during development, usually for ViewGroup or View. Therefore, the following analysis is only between these two controls.
When discussing the event distribution mechanism of a View, there are three methods:
boolean onInterceptTouchEvent(MotionEvent ev)
: is used to intercept events. This method exists only in viewGroups. Typically, we override this method to intercept the touch event so that it is not passed on to the child View.boolean dispatchTouchEvent(MotionEvent event)
: is used to distribute touch events, normally we do not override this method, returntrue
Indicates that the event was handled. In View, it handles events based on the type of gesture and the state of the control, calling back to usOnTouchListener
orOnClickListener
; In a ViewGroup, this method is overridden and its responsibility is to distribute events, traversing all child views and deciding whether to distribute events to the specified View.boolean onTouchEvent(MotionEvent event)
: used to handle touch events, returntrue
Indicates that the touch event was handled. The ViewGroup does not override this method, so the function in a ViewGroup is the same as in a View. Note that if we set for the controlOnTouchListener
And returned in ortrue
, then the method will not be called, i.eOnTouchListener
This method has a higher priority than this method. For us in development, it isOnTouchListener
比OnClickListener
和OnLongClickListener
Have a higher priority.
As a result, we get the following pseudocode. This code is the core of the event distribution mechanism that exists in the ViewGroup:
boolean dispatchTouchEvent(MotionEvent e) {
boolean result;
if (onInterceptTouchEvent(e)) {
result = super.dispatchTouchEvent(e);
} else {
result = child.dispatchTouchEvent(e);
}
return result;
}
Copy the code
Following the above analysis, the touch event passes through the Activity to the root ViewGroup:
If ViewGourp overwrites onInterceptTouchEvent() and returns true, it wants to intercept the method, The touch event is then handed to the current ViewGroup for processing (triggering OnTouchListener or OnClickListener, etc.); Otherwise, it is handed over to the child’s continued distribution. If the child element is a ViewGroup, the above logic will be executed in the child View, otherwise the event will be handled in the current child element (triggering OnTouchListener or OnClickListener etc.)… And so on layer by layer, essentially a depth-first search algorithm.
Here we make a sketch of the whole event distribution mechanism. In the following articles we will analyze the source code of each method in detail. In case you get lost in the following article, we first describe the overall logic as follows:
4.3 Source code analysis of event transmission
We’ve looked at how the event distribution mechanism works, but let’s look at the source code to see how it’s designed. Again, we’ll focus on just three approaches that need to be focused on.
4.3.1 Deciding whether to intercept events
First, let’s look at the dispatchTouchEvent(MotionEvent) method in ViewGroup. We’ve excerpted part of it:
@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) { / / 1
// Reset all state if it is a new touch event, including setting mFirstTouchTarget to NULL
cancelAndClearTouchTargets(ev);
resetTouchState();
}
MFirstTouchTarget is the wrapper of the View that handled the touch event before
final boolean intercepted;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
/ / here whether this ViewGroup disabled to intercept, by requestDisallowInterceptTouchEvent Settings
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); }else {
intercepted = false; }}else {
// A non-pressed event and mFirstTouchTarget is null indicates that interception logic has been judged and interception is enabled
intercepted = true;
}
// ...
}
// ...
return handled;
}
Copy the code
The above code is part of our excerpt of ViewGroup intercepting event code, the logic here is obviously much more complex than the pseudo-code. Nevertheless, the code is essential. Because, when we try to determine whether to intercept a touch event, the touch event is still continuing, which means that the method will be called continuously; Press it again when it’s lifted, and it’s another call. Given this continuity, we need to do a little more logic.
Here we first determine if the action is a new touch event at 1 by whether it is “pressed”, if so we need to reset the current touch state. Second, we need to call onInterceptTouchEvent() based on the event type, because we only need to check onInterceptTouchEvent() once when we “press” it for a touch event. So, obviously we need to use motionEvent.action_down as a judgment condition. We then use the global variable mFirstTouchTarget to record the result of the last interception — it is not null if the previous event was handled by a child element.
In addition to mFirstTouchTarget, we also need to use the FLAG_DISALLOW_INTERCEPT bit of mGroupFlags to determine if interception is disabled for the ViewGroup. This logo can be through the ViewGroup requestDisallowInterceptTouchEvent (Boolean) to set up. OnInterceptTouchEvent () is only needed if intercepting is not disabled.
Distribute events to child elements
If the event is not intercepted and cancelled in the above operation, then the following logic is entered. This part of code is in dispatchTouchEvent(). Events are passed to child elements based on their state in the following logic:
// Traverses the child elements in reverse order, that is, from top to bottom
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// ...
// Determine if the child can receive touch events: it can receive events and is not animating
if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// ...
/ / call here dispatchTransformedTouchEvent () method to pass events to child elements
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
/ /... Log some status information
// Complete the assignment to mFirstTouchTarget here, representing the touch event quilt element handling
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// Completes the loop and completes the traversal of the child elements
break;
}
// Obviously, if you get to this point, traversal of the child elements will continue
}
Copy the code
When judging the specified will be called after the View can receive touch events dispatchTransformedTouchEvent distribute events () method. Here’s an excerpt from its definition:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
// ...
if (child == null) {
// Essentially the same logic as the View's dispatchTouchEvent()
handled = super.dispatchTouchEvent(transformedEvent);
} else {
// ...
// Pass to the child element to continue distributing events
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
Copy the code
DispatchTransformedTouchEvent () according to the incoming child whether null into two calls: events are intercepted, let a child elements continue to distribute events; The other is to call the current ViewGroup’s super.DispatchTouchEvent (transformedEvent) to handle the event when it is intercepted.
4.3.3 dispatchTouchEvent in View
The dispatchTouchEvent(MotionEvent) we analyzed above is the overwritten method in ViewGroup. However, as we analyzed above, the method before overwriting is always called, just with a different object. Here we will analyze the role of the following method.
public boolean dispatchTouchEvent(MotionEvent event) {
// ...
boolean result = false;
/ /...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// This calls back the OnTouchListener passed in by the setOnTouchListener() method
ListenerInfo li = mListenerInfo;
if(li ! =null&& li.mOnTouchListener ! =null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// If OnTouchListener is not called back or returns false, onTouchEvent() is called to handle it
if(! result && onTouchEvent(event)) { result =true; }}// ...
return result;
}
Copy the code
Based on the source code analysis above, we know that if the current View has an OnTouchListener set and returns true in the onTouch() callback method, then onTouchEvent(MotionEvent) will not be called. So, let’s look at the onTouchEvent() method again:
public boolean onTouchEvent(MotionEvent event) {
// ...
// Determine if the current control is clickable: it has clickable properties, or is clickable
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// ...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// ...
if(! focusTaken) {if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
// ...
break;
case MotionEvent.ACTION_DOWN:
// ...
if(! clickable) { checkForLongClick(0, x, y);
break;
}
// ...
break;
// ...
}
return true;
}
return false;
}
Copy the code
Check whether the specified control is clickable, that is, whether a click or long press event is set. The performClick() method is then called when the gesture is raised, and an OnClickListener callback from ListenerInfo is attempted in this method; Will listen while holding down to invoke the corresponding holding event; Other events are similar and can be analyzed for themselves. Therefore, we can conclude that when the control’s touch event is assigned and true is returned, the event is consumed. The click and hold events are not called back even if they are set, and the touch events have higher priority than the latter two.
The View dispatchTouchEvent(MotionEvent) method is used to process gestures. If a ViewGroup intercepts a touch event, it handles the event itself; Otherwise, the touch event is passed to the child element for processing.
4.4.4 summary
That’s all we have to say about the event distribution mechanism in Android, and you can use a combination of images and code to understand it better. This section of code is long and bulky, but the design makes sense everywhere.
The source code
You can obtain the source code of the above application at Github: Android-References.