Android drags ViewDragHelper to resolve 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]

1. Overview

In a custom ViewGroup, many effects include a user's finger dragging a View(eg: side-sliding menu, etc.) inside it. Writing onInterceptTouchEvent and onTouchEvent for specific needs is a very difficult task and needs to be handled by himself: multi-finger processing, acceleration detection, and so on.(
Fortunately, the official support package for v4 includes a class called ViewDragHelper to help us write custom ViewGroup s.Take a quick 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 highlight the use of ViewDragHelper and ultimately implement a custom ViewGroup similar to DrawerLayout.(ps: The official Drawer Layout is implemented in this way)

2. Small examples of getting started

First, let's look at its quick usage through a simple example, divided into the following steps:

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

(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 code for the entire custom ViewGroup above is very concise, following the three steps above:

1. Create an instance

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

The larger the incoming, the smaller the value of mTouchSlop.The third parameter is Callback, which calls back the related method during the user's touch, which will be explained 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

The onInterceptTouchEvent uses mDragger.shouldInterceptTouchEvent(event) to determine if we should intercept the current event.Events are handled in onTouchEvent through mDragger.processTouchEvent(event).

3. Implement related methods of ViewDragHelper.CallCack

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 subviews can be moved, control over the boundaries of each moving View, and so on.

Three ways to override the above:

  • How tryCaptureView returns a ture means that the view can be captured, and you can decide which can be captured based on the first view parameter passed in
  • clampViewPositionHorizontal,clampViewPositionVertical can control the boundaries of child movement in this method, left, top are the locations to move to, for example, horizontally, I want to move only inside ViewGroup, 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 these three steps, we have completed a simple custom ViewGroup that allows you to drag and drop child Views freely.

A quick 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 this intuitive understanding, we need to have a deeper understanding of the methods within ViewDragHelper.CallBack.First of all, we need to consider that our ViewDragHelper is not just about having the sub-Views follow our finger movements, but we continue to learn about other features.

3. Functional Display

ViewDragHelper can also do the following:

  • Boundary detection, acceleration detection (eg:DrawerLayout boundary trigger pull-out)
  • Callback Drag Release (eg: DrawerLayout part, finger up, auto expand/shrink)
  • Move to a specified location (eg: click Button to expand/close Drawerlayout)

So let's go on to revamp our most basic example, which includes a few of the above operations.

First, let's look at the effect of our changes:

Simply add different actions for each subview:

The first View is to demonstrate simple movement.
The second View demonstrates that, in addition to moving, a free hand automatically returns to its original location.(Note that the faster you drag, the faster you return)
Third View, capturing the View as the boundary moves.

Okay, after looking at the effect diagram, let's look at the code changes:

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 can go back automatically when finger is released
                if (releasedChild == mAutoBackView)
                {
                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
                    invalidate();
                }
            }

            //Callback when dragging boundary
            @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 files we just change the text and background colors and do not paste again.

The first View did not make any changes at all.

Second View, we saved the most open location information after onLayout, most importantly rewritten onViewReleased in Callback, and we determined 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(); since it uses mScroller.startScroll internally, don't forget to need invalidate() along with the computeScroll method.

The third View, which we actively capture through the captureChildView in the onEdgeDragStarted callback method, bypasses the tryCaptureView, so our tryCaptureView, while returning true, does not affect it.Note that if you need to use boundary detection you need to add mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);.

At this point, we've introduced the common callback methods used in Callback, but there are still some that are not. Next, we'll modify our layout file, and we'll add clickable=true to all our TextView s, meaning sub-Views consume events.Run it again and you'll see that the View you could have dragged doesn't move. (If a brother who took the Button test should have discovered the problem, I hope you see it instead of asking questions, ha~).

Why?Mainly because, if the child View does not consume events, the entire gesture (DOWN-MOVE*-UP) is entered directly into the onTouchEvent, and the captureView is determined when the onTouchEvent's DOWN is present.If an event is consumed, the onInterceptTouchEvent method is used to determine if it can be captured, while the other two callback methods, getViewHorizontalDragRange and getViewVerticalDragRange, can only be captured if they return a value greater than 0.

So if you test with Button or add clickable = true to TextView, remember to override both of the following 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 extent to which the childView moves horizontally or vertically, and currently only one can be replicated if only one direction is required.

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

  • onViewDragStateChanged 

    Callback when ViewDragHelper state changes (IDLE,DRAGGING,SETTING [Autoscroll])

  • onViewPositionChanged

    Callback when captureview's position changes

  • onViewCaptured

    Callback when captureview is captured

  • onViewReleased Used

  • onEdgeTouched

    Callback when touching the boundary.

  • onEdgeLock

    true locks the current boundary, while false unLock.

  • onEdgeDragStarted used

  • getOrderedChildIndex 

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

  • getViewHorizontalDragRange is used

  • getViewVerticalDragRange used
  • tryCaptureView used
  • clampViewPositionHorizontal is already in use
  • clampViewPositionVertical used

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

To summarize, the general order of callbacks for the method is:

shouldInterceptTouchEvent: 

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->onEdgeTouched

MOVE:
    getOrderedChildIndex(findTopChildUnder)
    ->getViewHorizontalDragRange & 
      getViewVerticalDragRange(checkTouchSlop)(MOVE May occur 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 cases where judgment is not established.

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

When a subview consumes events, it takes shouldInterceptTouchEvent and MOVE to go through a series of judgments (getViewHorizontalDragRange, clampViewPositionVertical, etc.) before it can go to tryCaptureView.

ok, we're done with this introductory use of ViewDragHelper, and in the next section, we'll use ViewDragHelper to implement a DrawerLayout on our own.(
Interesting ones can also be implemented in accordance with this article, as well as DrawerLayout's source code ~

Reference Links

~~have a nice day ~~

Source Click Download

ok~

Keywords: Android DrawerLayout

Added by gthri on Sun, 30 Jun 2019 02:52:05 +0300