ViewPager, ScrollView nested ViewPager sliding conflict resolved
This blog post focuses on a few issues
- A brief overview of the View event distribution mechanism
- The idea and method to solve the event sliding conflict
- Sliding collisions caused by nested viewPagers in ScrollView
- Sliding collisions caused by nesting ViewPager inside ViewPager
- Several realization ways of round – cast graph
CSDN:Blog.csdn.net/gdutxiaoxu/…
Take a look at the renderings first
ScrollView has nested viewPagers inside it
ViewPager nested inside ViewPager
View event distribution mechanism
This blog post is going to go into more detail about the View event distribution mechanism, because there are a number of good articles on the web, and I’m certainly not doing very well with my own limited skills.
To recap, the event distribution mechanism of a View mainly involves the following three methods
- DispatchTouchEvent, this method is basically used to send events
- OnInterceptTouchEvent, this method is mainly used to intercept events. (Note that ViewGroup has this method. Views do not have onInterceptTouchEvent
- The onTouchEvent method is primarily used to handle events
- RequestDisallowInterceptTouchEvent (true), the method can affect the parent View whether to intercept events, said true not intercept events, false said intercept events
The following referenceIllustrate the Android event distribution mechanismThe content of this blog post
- If you look closely, the diagram is divided into three layers: Activity, ViewGroup, and View from top to bottom
- Events start with the white arrow in the upper left corner and are dispatched by the Activity’s dispatchTouchEvent
- The top word of the arrow represents the method return value (return true, return false, return super.xxxxx(),super means to call the superclass implementation.
- The dispatchTouchEvent and onTouchEvent boxes have the word “true—-> consume” in them, which means that if the method returns true, the event will be consumed and will not go anywhere else, and the event will terminate.
- At present, all graph events are for ACTION_DOWN. We will analyze ACTION_MOVE and ACTION_UP at last.
- The Activity dispatchTouchEvent in the previous diagram was wrong (figure fixed), only the return super.dispatchTouchEvent(ev) was further down, and the event returned true or false was consumed (aborted).
conclusion
When a TouchEvent occurs, the Activity first passes the TouchEvent to the topmost View, and the TouchEvent first reaches the dispatchTouchEvent of the topmost View. It’s then distributed by the dispatchTouchEvent method,
-
If dispatchTouchEvent returns true consumption event, the event is terminated.
-
If dispatchTouchEvent returns false, the onTouchEvent handler is passed back to the parent View;
The onTouchEvent event returns true, the event terminates, returns false, and is handled by the parent View’s onTouchEvent method
-
If dispatchTouchEvent returns super, it calls its own onInterceptTouchEvent method by default
By default, the interceptTouchEvent calls back to the super method, which returns false by default, so it is handled by the child View’s onDispatchTouchEvent method
If the interceptTouchEvent returns true, it will be handled by its onTouchEvent.
If the interceptTouchEvent returns false, it is passed to the child view, whose dispatchTouchEvent initiates the event distribution.
For more detailed analysis, check out the original blog’s illustration of the Android event distribution mechanism. I really recommend it. It’s well written.
The idea and method to solve the event sliding conflict
There are three common cases
In the first case, you slide in different directions
In the second case, it slides in the same direction
The third case, the nesting of the above two cases
solution
If we look at the above three cases, we know that the common characteristic is that the parent View and the child View all want to respond to our touch event, but unfortunately our touch event can only be consumed by one View or one ViewGroup at a time, so there is a sliding conflict, right? Since only one View or ViewGroup can intercept an event at a time, we just need to decide that one View or ViewGroup can intercept an event at another time, and another View or ViewGroup can intercept an event at another time. To sum up, as outlined in the Art of Android Development, there are two ways to do this
The following solutions come from the Art of Android Development book
The following two methods are for the first case (sliding in different directions), where the parent View slides up and down and the child View slides left and right.
External solution
Rewrite the onInterceptTouchEvent method from the parent View, intercept it if the parent View needs to intercept it, and return false if the parent View does not
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final floatdeltaY = Math.abs(y - mDownPosY); // Here is enough to block the judgment basis is left and right slide, readers can according to their own logic to block or notif (deltaX > deltaY) {
return false; }}return super.onInterceptTouchEvent(ev);
}
Copy the code
Internal solution
From the child View, the parent View first do not intercept any events, all events passed to the child View, if the child View needs this event to consume, do not need this event to the parent View processing.
Implementation approach is as follows, to rewrite the child View dispatchTouchEvent method, method in Action_down action by requestDisallowInterceptTouchEvent (true) to request the parent View don’t intercept events, This ensures that the child View can receive the Action_move event, and then in the Action_move action according to its own logic whether to intercept the event, if not, to the parent View processing
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// Ensure that the child View can receive the Action_move event
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
// Here is enough to block the judgment basis is left and right slide, readers can according to their own logic to block or not
if (dealtX >= dealtY) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
Copy the code
Sliding collisions caused by nested viewPagers in ScrollView
External solution
As mentioned above, starting from the parent ViewScrollView, override the OnInterceptTouchEvent method to intercept events when sliding up or down, but not when sliding left or right, and return false. This ensures that the dispatchTouchEvent method of the child View is called as follows
/** * @explain: This ScrlloView does not intercept horizontal sliding events, * is used to resolve the use of nested ViewPager in ScrollView * @author: Xujun on 2016/10/25 15:28 * @email: [email protected] */ public class VerticalScrollView extends ScrollView { public VerticalScrollView(Context context) { super(context); } public VerticalScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(21) public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } privatefloat mDownPosX = 0;
private float mDownPosY = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final floatdeltaY = Math.abs(y - mDownPosY); // Here is enough to block the judgment basis is left and right slide, readers can according to their own logic to block or notif (deltaX > deltaY) {
return false; }}returnsuper.onInterceptTouchEvent(ev); }}Copy the code
Internal solution
As the above, through requestDisallowInterceptTouchEvent (true) method to influence whether the parent View intercept events, we rewrite ViewPager dispatchTouchEvent () method, Request the parent View ScrollView not to intercept events while swiping left or right, otherwise
/** * @explain: This ViewPager is used to solve the internal problem of nested ViewPager in ScrollView * @author: xujun on 2016/10/25 16:38 * @email: [email protected] */
public class MyViewPager extends ViewPager {
private static final String TAG = "xujun";
int lastX = -1;
int lastY = -1;
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// Ensure that the child View can receive the Action_move event
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
// Here is enough to block the judgment basis is left and right slide, readers can according to their own logic to block or not
if (dealtX >= dealtY) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev); }}Copy the code
Precautions (Pit)
When there are many children in the Layout of the top layer of ScrollView, when the following child is RecyclerView or ListView, it will often slide to the first item of ListView or RecyclerView. When entering the interface, it will cause the View of RecyclerView to be slipped to the interface accidentally, and it can not be seen. At this time, the user experience is relatively poor
When the structure is as follows
Related solutions in the Activity
So I found the relevant information, perfect solution in the Activity, the main two methods
First approach, rewrite the Activity onWindowFocusChanged () method, in call mNoHorizontalScrollView. ScrollTo (0, 0); Method, because onWindowFocusChanged will only be called back when all views are drawn. If you are not familiar with it, go back to the Activity life cycle
private void scroll() {mNoHorizontalScrollView. ScrollTo (0, 0). } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus);if(hasFocus && first){
first=false; scroll(); }}Copy the code
Second solution, call RecyclerView above the View of the following method, let it get focus
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.requestFocus();
Copy the code
This code gives focus to a control at the top of the interface during initialization, and the scroll bar is displayed at the top.
Related solutions in fragments
Also call the second method, call RecyclerView above the View of the following method, let it get focus
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.requestFocus();
Copy the code
This code gives focus to a control at the top of the interface during initialization, and the scroll bar is displayed at the top. However, there are disadvantages in the method. That is, when the view above slides halfway, it will switch to the next Fragment. When it switches back, the first item of RecyclerView will automatically slide to the top. At present, I have not found a relatively good solution to this problem. If you know the relevant solution, please feel free to contact me. You can add me on wechat or comment in the message area
Individual suspects
So far I haven’t found a method that calls back after the Fragemnt interface is completely drawn. If you know how to do that, please feel free to suggest it
Sliding collisions caused by nesting ViewPager inside ViewPager
Internal solution
Start with the child View ViewPager, override the child View’s dispatchTouchEvent method, intercept when the child View needs to intercept, otherwise hand over to the parent View, code as follows
public class ChildViewPager extends ViewPager {
private static final String TAG = "xujun";
public ChildViewPager(Context context) {
super(context);
}
public ChildViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int curPosition;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
curPosition = this.getCurrentItem();
int count = this.getAdapter().getCount();
Log.i(TAG, "curPosition:=" +curPosition);
// When the current page is on the last page and page 0, the parent intercepts the touch event
if (curPosition == count - 1|| curPosition==0) {
getParent().requestDisallowInterceptTouchEvent(false);
} else {// In other cases, the child intercepts the touch event
getParent().requestDisallowInterceptTouchEvent(true); }}return super.dispatchTouchEvent(ev); }}Copy the code
External solution
If we want to use the internal solution to solve the problem, relatively troublesome, let me mention my own personal idea, we can first measure the subview in which region, and then we need to according to whether we press the point is within the region, if so, according to the subview need to intercept processing
discuss
For this effect, the above is the rotation of the graph, RecyclerView or ListView, there are several ways to achieve the general
- Using our improved ScrollView to nest ViewPager and RecyclerView, this implementation needs to solve the conflicts of View sliding events, as well as the problems in fragments I improved above
- Use addHeaderView for listView, or use multiple different items
- Use RecyclerView to add headerView to achieve, or reuse a variety of different items to achieve. About RecyclerView how to add headerView can refer to hongyang Daigod this blog Android elegant for RecyclerView add headerView and FooterView
- Use controls such as CoordinatorLayout in the SupportLibrary
The layout file is shown below, and the Activity code is SixActivity in the project
<? The XML version = "1.0" encoding = "utf-8"? > <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/background_light" android:fitsSystemWindows="true" > <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="300dp" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" > <android.support.design.widget.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_scrollFlags="scroll|snap"> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" > </android.support.v4.view.ViewPager> <TextView android:id="@+id/tv_page" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:gravity="right" android:text="1/10" android:textColor="#000"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> </android.support.v7.widget.RecyclerView> </android.support.design.widget.CoordinatorLayout>Copy the code
For more information on how you can use CoordinatorLayout, you can use CoordinatorLayout to create all kinds of cool things on my blog
conclusion
- When we slide in different directions, the external solution and the internal solution have similar complexity.
- When we slide in the same direction, it is recommended to adopt the internal solution, because the complexity of using the external solution is high. And sometimes when we are using someone else’s open source controls, modifying someone else’s source code can cause unexpected bugs.
digression
- At the end of this blog improve the realization of the rotation of graph +list of several forms of implementation, at the beginning is not to write, because ScrollView nested ViewPager and RecyclerView in the fragment RecyclerView grabs the focus, in some cases the user experience is not good, Just write out, with this blog to explain the View sliding event conflict has nothing to do with, just to provide readers with a variety of ideas
- As for CoordinatorLayout, it is proposed in Google IO 2015 and has very powerful functions. It can be said that it is specially created to solve the nesting guide slide, which is very convenient for developers. For beginners, it is not necessary to master it temporarily, but to learn other basics well first
- At the same time, buy an advertisement. Welcome to star or fork on my Github. Thank you
See article: Illustrating the Android event distribution mechanism
CSDN:Blog.csdn.net/gdutxiaoxu/…
Source code download address:Github.com/gdutxiaoxu/…
Welcome to follow my wechat public account Stormjun94. Currently, I am a programmer. I not only share relevant knowledge of Android development, but also share the growth process of technical people, including personal summary, workplace experience, interview experience, etc., hoping to make you take a little detours.