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.