Android ViewDragHelper fully parses custom ViewGroup artifacts

For reprinting, please indicate the source:
http://blog.csdn.net/lmj623565791/article/details/46858663; 
This article is from: [Zhang Hongyang's blog]

I. Overview

In customized view group, many effects include the user's finger to drag a view inside it (eg: sideslip menu, etc.). It is very difficult to write onInterceptTouchEvent and onTouchEvent for specific needs, which need to be handled by themselves: multi-finger processing, acceleration detection and so on.  
Fortunately, the official support package for v4 provides a class such as ViewDragHelper to help us easily write custom ViewGroup. Take a brief look at its notes:

ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number 
of useful operations and state tracking for allowing a user to drag and reposition 
views within their parent ViewGroup.

This blog will focus on the use of ViewDragHelper and eventually implement a custom ViewGroup similar to Drawer Layout. (ps: The official Drawer Layout is implemented in this way)

2. Introduction Examples

First of all, we use a simple example to see its fast usage, divided into the following steps:

  1. Create an instance
  2. Calls to touch-related methods
  3. Writing of ViewDragHelper.Callback Example

(1) Custom ViewGroup

package com.zhy.learn.view;

import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

/**
 * Created by zhy on 15/6/3.
 */
public class VDHLayout extends LinearLayout
{
    private ViewDragHelper mDragger;

    public VDHLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()
        {
            @Override
            public boolean tryCaptureView(View child, int pointerId)
            {
                return true;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy)
            {
                return top;
            }
        });
    }

   @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mDragger.processTouchEvent(event);
        return true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

As you can see, the entire custom ViewGroup code above is very concise, following the three steps mentioned above:

1. Create instances

mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()
        {
        });
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

Creating an instance requires three parameters, the first is the current ViewGroup and the second sensitivity, which is mainly used to set touchSlop:

helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
  • 1
  • 1

It can be seen that the larger the input, the smaller the value of mTouchSlop. The third parameter is Callback, which calls back the relevant method during the user's touch process, which will be described in detail later.

2. Touch-related methods

 @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mDragger.processTouchEvent(event);
        return true;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

In onInterceptTouchEvent, we use mDragger.shouldInterceptTouchEvent(event) to determine whether we should intercept the current event. Events are handled through mDragger.processTouchEvent(event) in onTouchEvent.

3. Implementing ViewDragHelper.CallCack Relevant Method

new ViewDragHelper.Callback()
        {
            @Override
            public boolean tryCaptureView(View child, int pointerId)
            {
                return true;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy)
            {
                return top;
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

When intercepting and handling events in ViewDragHelper, many methods in CallBack need to be called back to determine things, such as which sub-Views can be moved, control over the boundaries of a moving View, and so on.

Three methods of copying above:

  • How tryCaptureView returns to ture indicates that the view can be captured, and you can decide which can be captured based on the first view parameter passed in.
  • Clamp View Position Horizontal, clamp View Position Vertical can control the boundary of child movement in this method. Leave and top are the positions that will be moved to. For example, in the horizontal case, I hope to move only within the View Group, i.e. minimum >= paddingleft, maximum <= ViewGroup. getWidth () - paddingright-child. getWidth. You can write the following code:
 @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                final int leftBound = getPaddingLeft();
                final int rightBound = getWidth() - mDragView.getWidth() - leftBound;

                final int newLeft = Math.min(Math.max(left, leftBound), rightBound);

                return newLeft;
            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

After the above three steps, we have completed a simple custom ViewGroup, which can freely drag sub-View.

Take a brief look at the layout file

(2) Layout documents

<com.zhy.learn.view.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android"
                              xmlns:tools="http://schemas.android.com/tools"
                              android:layout_width="match_parent"
                              android:orientation="vertical"
                              android:layout_height="match_parent"
    >

    <TextView
        android:layout_margin="10dp"
        android:gravity="center"
        android:layout_gravity="center"
        android:background="#44ff0000"
        android:text="I can be dragged !"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

    <TextView
        android:layout_margin="10dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:background="#44ff0000"
        android:text="I can be dragged !"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

    <TextView
        android:layout_margin="10dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:background="#44ff0000"
        android:text="I can be dragged !"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

</com.zhy.learn.view.VDHLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

There are three TextView s in our custom ViewGroup.

Current effect:

You can see just a few lines of code to play with.~~~

With intuitive understanding, we also need to have a deep understanding of the methods in ViewDragHelper.CallBack. The first thing we need to consider is that our ViewDrag Helper is not just about allowing sub-Views to follow our fingers, but we continue to learn about other functions.

3. Functional Display

ViewDragHelper can also do the following:

  • Boundary detection and acceleration detection (eg: Drawer Layout boundary trigger pull-out)
  • Callback Drag Release (eg: Drawer Layout part, finger raised, automatic expansion/contraction)
  • Move to a specified location (eg: click Button to expand/close Drawerlayout)

Then we will revamp our most basic example, including the above several operations.

Let's first look at the effect of our modification:

Simply add different operations for each subview:

The first View is to demonstrate simple movement.
The second View demonstrates that, in addition to moving, the loosened hand automatically returns to its original position. (Note that the faster you drag, the faster you return)
The third View captures the View when the boundary moves.

Okay, after looking at the rendering, let's look at the code modification:

Modified code

package com.zhy.learn.view;

import android.content.Context;
import android.graphics.Point;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

/**
 * Created by zhy on 15/6/3.
 */
public class VDHLayout extends LinearLayout
{
    private ViewDragHelper mDragger;

    private View mDragView;
    private View mAutoBackView;
    private View mEdgeTrackerView;

    private Point mAutoBackOriginPos = new Point();

    public VDHLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()
        {
            @Override
            public boolean tryCaptureView(View child, int pointerId)
            {
                //mEdgeTrackerView prohibits direct movement
                return child == mDragView || child == mAutoBackView;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx)
            {
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy)
            {
                return top;
            }


            //Callback when finger is released
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel)
            {
                //mAutoBackView finger can be automatically returned when released
                if (releasedChild == mAutoBackView)
                {
                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
                    invalidate();
                }
            }

            //Callback when the boundary drags
            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId)
            {
                mDragger.captureChildView(mEdgeTrackerView, pointerId);
            }
        });
        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mDragger.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll()
    {
        if(mDragger.continueSettling(true))
        {
            invalidate();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        super.onLayout(changed, l, t, r, b);

        mAutoBackOriginPos.x = mAutoBackView.getLeft();
        mAutoBackOriginPos.y = mAutoBackView.getTop();
    }

    @Override
    protected void onFinishInflate()
    {
        super.onFinishInflate();

        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
        mEdgeTrackerView = getChildAt(2);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113

Layout file we just changed the text and background color will not be repeated posting.

The first View was basically unchanged.

The second View, we saved the most open location information after onLayout, and most importantly rewrote onViewReleased in Callback. We decided in onViewReleased that if it was mAutoBackView, we would call settleCapturedViewAt back back to the original location. You can see that the code that follows is invalidate(); because it uses mScroller.startScroll internally, don't forget to need invalidate() along with the computeScroll method.

The third View, we actively capture it through captureChildView in onEdgeDragStarted callback method. This method can bypass tryCaptureView, so our tryCaptureView does not affect although it returns true. Note that mDragger. setEdgeTracking Enabled (ViewDragHelper. EDGE_LEFT) should be added if boundary detection is required.

At this point, we have introduced the callback methods commonly used in Callback, but there are still some methods not introduced. Next, we modify our layout file. We add clickable=true to all our TextView, which means that sub-View can consume events. Run again and you will find that the View that could have been dragged does not move. test My brother should have found this problem, I hope you see it, not have asked questions, ha ~.

Why? The main reason is that if the sub-View does not consume events, then the whole gesture (DOWN-MOVE*-UP) goes directly into onTouchEvent, and captureView is determined when the DOWN of onTouchEvent. If consuming events, the onInterceptTouchEvent method will be used to determine whether it can be captured, while the other two callback methods will be judged in the process of judgment: getViewHorizontalDragRange and getViewVerticalDragRange, which can be captured normally only if the two methods return values greater than 0.

So, if you test with Button or add clickable = true to TextView, remember to rewrite the following two methods:

@Override
public int getViewHorizontalDragRange(View child)
{
     return getMeasuredWidth()-child.getMeasuredWidth();
}

@Override
public int getViewVerticalDragRange(View child)
{
     return getMeasuredHeight()-child.getMeasuredHeight();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

The return value of the method should be the horizontal or vertical movement range of the child View. Currently, if only one direction is needed, only one can be copied.

So, let's list all the Callback methods and see what else we haven't used:

  • onViewDragStateChanged

    Callback when ViewDragHelper status changes (IDLE,DRAGGING,SETTING [auto-scrolling])

  • onViewPositionChanged

    Callback when the position of captureview changes

  • onViewCaptured

    Callback when captureview is captured

  • onViewReleased is in use

  • onEdgeTouched

    Callback when touching the boundary.

  • onEdgeLock

    true locks the current boundary, while false unLock.

  • onEdgeDragStarted is already in use

  • getOrderedChildIndex

    Change the same coordinate (x,y) to find the location of the captureView. (Specifically in the findTopChildUnder method)

  • getViewHorizontalDragRange is already in use

  • GetViewVertical DragRange is already in use
  • tryCaptureView is in use
  • ClampViewPosition Horizontal is in use
  • clampViewPositionVertical is in use

ok, so far all callback methods have a certain understanding.

In conclusion, the general order of callbacks of the method is as follows:

shouldInterceptTouchEvent: 

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->onEdgeTouched

MOVE:
    getOrderedChildIndex(findTopChildUnder)
    ->getViewHorizontalDragRange & 
      getViewVerticalDragRange(checkTouchSlop)(MOVE More than once)
    ->clampViewPositionHorizontal&
      clampViewPositionVertical
    ->onEdgeDragStarted
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged

processTouchEvent:

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged
    ->onEdgeTouched
MOVE:
    ->STATE==DRAGGING:dragTo
    ->STATE!=DRAGGING:
        onEdgeDragStarted
        ->getOrderedChildIndex(findTopChildUnder)
        ->getViewHorizontalDragRange&
          getViewVerticalDragRange(checkTouchSlop)
        ->tryCaptureView
        ->onViewCaptured
        ->onViewDragStateChanged
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

ok, this is the general process under normal circumstances, of course, there may be a lot of judgments are not established in the whole process.

As can be explained above, we can move without writing the getViewHorizontalDragRange method in the case of TextView(clickable=false). Because you go directly to the DOWN of the process Touch Event, and then onViewCaptured, onViewDragStateChanged (into the DRAGGING state), and MOVE dragTo directly.

When sub-View consumes events, it needs to walk shouldInterceptTouchEvent, MOVE after a series of judgments (getView Horizontal DragRange, clampViewPositionVertical, etc.) to go to tryCaptureView.

ok, so we'll finish with the introduction of ViewDragHelper. In the next article, we'll use ViewDragHelper to implement a Drawer Layout by ourselves.  
Interested ones can also be implemented according to this article, as well as Drawer Layout's source code.~

Reference link

~~have a nice day ~~

Source Click Download

ok~

Keywords: Android DrawerLayout

Added by aclees86 on Tue, 25 Jun 2019 22:43:57 +0300