To grab your attention, here’s a GIF:



I believe you’ve seen this effect before? When I first saw this effect, I was also itchy and eager to implement this feature, but I didn’t do it because of procrastination. Now this period of time, the work is relatively light, so a few years on their ownAndroidCareer technology to do some summary and thinking. Drag and drop is a great way to form a theme. As the title suggests, the goal of today’s post is to introduce and analyze the ViewDragHelper class.

Here’s what the reader will learn from this article: 1. Implement basic drag effects without using ViewDragHelper. 2. Easy to implement complex drag effects with ViewDragHelper. 3. Analysis of ViewDragHelper source code to explain why it can achieve drag (rest easy, not dizzy, only a little source code involved).

I met ViewDragHelper

In the first month of my Android career, my first project was about Launcher, and I had to directly read the code of the system Launcher. At that time, it was the project of Launcher2, which was the facade of the Android system but had a huge amount of code. As a rookie, The difficulty of the work can be imagined. There are a lot of things you can’t read directly, like various callbacks, like dragControllers that involve drag and drop.

Drag and drop in the Launcher is mainly for the ICON and Widget of the APP on the desktop.

Launcher2’s drag-and-drop code was something I didn’t think I could understand at the time. I was an expert at understanding what I was thinking.

However, this is why I have a deep fear of drag and drop.

I decided to ———— one day, I must have this ability.

Later, a handy helper class, ViewDragHelper, was added to the Support V4 package for ease of development, so my goal went one step further.

Drag and drop without using ViewDragHelper

Active thinking is a little better than passive acceptance. The disadvantage of passive acceptance is that when we read a book, we think we understand it and have the illusion that we have “learned it”. As a result, we check it again after a period of time and find that the practical effect is very different.

Therefore, when we learn new knowledge, it is better to add our own active thinking, because only in this way can we truly absorb the knowledge of others, build it into our own knowledge system, and become our own knowledge components.

So, for drag and drop, we can leave the ViewDragHelper class behind.

Let’s start by thinking about how we would start if we were coding ourselves.

Action decomposition

First we can break down drag: 1. Touch. 2. Mobile.

Role analysis

First, the goal of our blog analysis is drag in a ViewGroup, not drag in a View. Drag and drop in View is actually drag and drop for content. The solution is scrollBy(), which is equivalent to the concept of sliding or scrolling, which is beyond the scope of this article. For those of you who are interested in this section, read my post no longer confused, maybe you’ve never really understood Scroller and sliding before.

It is easy to observe that drag-and-drop roles in viewgroups may include: 1. ViewGroup. 2. Its child Views, which are some childViews.

Interaction analysis

  1. Finger touch on ViewGroup.
  2. If the coordinates of the touch happen to fall on some childView. Drag begins.
  3. The finger begins to move and the childView position coordinates change. Drag and drop.
  4. After the finger is released, the childView falls to the new position or bounces back to the specified location, and the drag ends.

coding

When touching is involved, viewGroups are naturally handled in the onTouchEvent() and onInterceptTouchEvent() methods. OnInterceptTouchEvent () is used to determine whether to intercept childView touches. For demonstration purposes, the onInterceptTouchEvent() is set to true.

OnTouchEvent () In this method, the ViewGroup is used to handle the specific flow of the touch. That is, touch, movement and release of the fingers corresponding to the figure above.

In Android, MotionEvent encapsulates various states of touch. So the main states we deal with are: 1. MotionEvent.ACTION_DOWN: In this state, the marker finger presses the screen. We need to determine whether the current touch is in the display area of childView. If so, mark the start of the drag state. We need to record the touch position of the finger as the original coordinate. 2. MotionEvent.ACTION_MOVE: This state naturally represents the movement process of the finger. At this time, we still need to record the finger touching the new coordinate, and then if the state is at the beginning of the touch, we will offset the childView. 3. MotionEvent. ACTION_UP, MotionEvent. ACTION_CANCLE: If a childView is dragging, it needs to mark the end of the dragging state. The View usually stays at the new coordinate or bounts back to its original location, depending on the actual situation.

Now that we know the flow, we can start coding, so we can create a new ViewGroup and call it DragViewGroup, and for simplicity, make it inherit from FrameLayout. Then it implements its onInterceptTouchEvent() and onTouchEvent().

public class DragViewGroup extends FrameLayout {
    private static final String TAG = "TestViewGroup";

    // Record the coordinates of the last touch of the finger
    private float mLastPointX;
    private float mLastPointY;

    // Use to identify the minimum slip distance
    private int mSlop;
    // Used to identify the child being dragged. Null indicates that no child is being dragged
    private View mDragView;

    // State can be idle or drag
    enum State {
        IDLE,
        DRAGGING
    }

    State mCurrentState;

    public DragViewGroup(Context context) {
        this(context,null);
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }


    public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mSlop = ViewConfiguration.getWindowTouchSlop();
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();

        switch (action){
            case MotionEvent.ACTION_DOWN:
                if ( isPointOnViews(event)) {
                    // mark the state as drag and record the last touch coordinates
                    mCurrentState = State.DRAGGING;
                    mLastPointX = event.getX();
                    mLastPointY = event.getY();
                }
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaX = (int) (event.getX() - mLastPointX);
                int deltaY = (int) (event.getY() - mLastPointY);
                if(mCurrentState == State.DRAGGING && mDragView ! =null
                        && (Math.abs(deltaX) > mSlop || Math.abs(deltaY) > mSlop)) {
                    // Move the dragged child if the condition is met
                    ViewCompat.offsetLeftAndRight(mDragView,deltaX);
                    ViewCompat.offsetTopAndBottom(mDragView,deltaY);
                    mLastPointX = event.getX();
                    mLastPointY = event.getY();
                }
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if ( mCurrentState == State.DRAGGING ){
                    // Mark the state as idle and set the mDragView variable to NULL
                    mCurrentState = State.IDLE;
                    mDragView = null;
                }
                break;
        }
        return true;
    }

    /** * Determine if the touch position is on the child ** */
    private boolean isPointOnViews(MotionEvent ev) {
        boolean result = false;
        Rect rect = new Rect();
        for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); rect.set((int)view.getX(),(int)view.getY(),(int)view.getX()+(int)view.getWidth()
                ,(int)view.getY()+view.getHeight());

            if (rect.contains((int)ev.getX(),(int)ev.getY())){
                // Mark the dragged child
                mDragView = view;
                result = true;
                break; }}return  result && mCurrentState != State.DRAGGING;
    }
}Copy the code

The comments are clearly written and the process has been analyzed before. Now let’s verify that by putting three views into the DragViewGroup and checking to see if we can move it with our fingers. The layout code is relatively simple, so I won’t post it. Just look at the effect.

As you can see, the basic drag and drop function is implemented, but there is one detail that needs to be optimized. When three children are displayed overlapping, the bottom child is always responded to when touching its public area. This is a bit anti-human, and the normal operation should be the top one first. So how do you optimize?

In the code above, mDragView is used to mark the child that can be dragged. We assign the first child in isPointOnViews(), but due to FrameLayout, The topmost child is actually at the bottom of the ViewGroup index.

private boolean isPointOnViews(MotionEvent ev) {
    boolean result = false;
    Rect rect = new Rect();
    for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); rect.set((int)view.getX(),(int)view.getY(),(int)view.getX()+(int)view.getWidth()
            ,(int)view.getY()+view.getHeight());

        if (rect.contains((int)ev.getX(),(int)ev.getY())){
            // Mark the dragged child
            mDragView = view;
            result = true;
            break; }}returnresult && mCurrentState ! = State.DRAGGING; }Copy the code

So, we can fix this problem by making one small change, which is to go through children in reverse order. In this way, first check from the top layer to find the most suitable touch location, the code is as follows:

private boolean isPointOnViews(MotionEvent ev) {
    boolean result = false;
    Rect rect = new Rect();
    for (int i = getChildCount() - 1; i >=0; i--) { View view = getChildAt(i); rect.set((int)view.getX(),(int)view.getY(),(int)view.getX()+(int)view.getWidth()
            ,(int)view.getY()+view.getHeight());

        if (rect.contains((int)ev.getX(),(int)ev.getY())){
            // Mark the dragged child
            mDragView = view;
            result = true;
            break; }}returnresult && mCurrentState ! = State.DRAGGING; }Copy the code

The effect is as follows:

The springback effect

Maybe some of you are thinking, what if I want to bounce back in drag? After all, this is more consistent with the drag and drop nature. The above code assumes that after the finger leaves the screen, the child stays at the new coordinate. What if we release the finger and the child moves back to its original position?

1. Record the coordinates of the child’s original position. 2. When the finger is released, change the value from the new position to the original position with the help of the attribute animation, and move the child during the change process, and finally form the animation effect of rebound.

public class DragViewGroup extends FrameLayout {
    private static final String TAG = "TestViewGroup";

    // Record the coordinates of the last touch of the finger
    private float mLastPointX;
    private float mLastPointY;

    // Record the position of the child before it is dragged
    private float mDragViewOrigX;
    private float mDragViewOrigY;



    // Use to identify the minimum slip distance
    private int mSlop;
    // Used to identify the child being dragged. Null indicates that no child is being dragged
    private View mDragView;



    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();

        switch (action){
            case MotionEvent.ACTION_DOWN:
                if ( isPointOnViews(event)) {
                    // mark the state as drag and record the last touch coordinates
                    mCurrentState = State.DRAGGING;
                    mLastPointX = event.getX();
                    mLastPointY = event.getY();
                }
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaX = (int) (event.getX() - mLastPointX);
                int deltaY = (int) (event.getY() - mLastPointY);
                if(mCurrentState == State.DRAGGING && mDragView ! =null
                        && (Math.abs(deltaX) > mSlop || Math.abs(deltaY) > mSlop)) {
                    // Move the dragged child if the condition is met
                    ViewCompat.offsetLeftAndRight(mDragView,deltaX);
                    ViewCompat.offsetTopAndBottom(mDragView,deltaY);
                    mLastPointX = event.getX();
                    mLastPointY = event.getY();
                }
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if ( mCurrentState == State.DRAGGING ){
                    // Mark the state as idle and set the mDragView variable to NULL
                    if(mDragView ! =null ) {
                        ValueAnimator animator = ValueAnimator.ofFloat(mDragView.getX(),mDragViewOrigX);
                        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                            @Override
                            public void onAnimationUpdate(ValueAnimator animation) { mDragView.setX((Float) animation.getAnimatedValue()); }}); ValueAnimator animator1 = ValueAnimator.ofFloat(mDragView.getY(),mDragViewOrigY); animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                            @Override
                            public void onAnimationUpdate(ValueAnimator animation) { mDragView.setY((Float) animation.getAnimatedValue()); }}); AnimatorSet animatorSet =new AnimatorSet();
                        animatorSet.play(animator).with(animator1);
                        animatorSet.addListener(new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationEnd(Animator animation) {
                                super.onAnimationEnd(animation);
                                mDragView = null; }}); animatorSet.start(); }else {
                        mDragView = null;
                    }
                    mCurrentState = State.IDLE;

                }
                break;
        }
        return true;
    }

    /** * Determine if the touch position is on the child ** */
    private boolean isPointOnViews(MotionEvent ev) {
        boolean result = false;
        Rect rect = new Rect();
        for (int i = getChildCount() - 1; i >=0; i--) { View view = getChildAt(i); rect.set((int)view.getX(),(int)view.getY(),(int)view.getX()+(int)view.getWidth()
                ,(int)view.getY()+view.getHeight());

            if (rect.contains((int)ev.getX(),(int)ev.getY())){
                // Mark the dragged child
                mDragView = view;
                // Save the position coordinates of the child between drags
                mDragViewOrigX = mDragView.getX();
                mDragViewOrigY = mDragView.getY();
                result = true;
                break; }}return  result && mCurrentState != State.DRAGGING;
    }
}

Copy the code

At this point, we have our own way of implementing a relatively simple drag-and-drop function. The next part of course is to learn how to do this using ViewDragHelper.

ViewDragHelper Basic introduction

ViewDragHelper is a utility class included in the V4 compatibility package that is intended to assist in customizing viewGroups. ViewDragHelper provides a very useful set of methods and state tracking for dragging and repositioning views in viewGroups.

This is ViewDragHelper, which is essentially just a utility class that can be used for drag and drop.

Let’s see how it works first.

The creation of ViewDragHelper

static ViewDragHelper   create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb)

static ViewDragHelper   create(ViewGroup forParent, ViewDragHelper.Callback cb)
Copy the code

ViewDragHelper provides two factory methods to create instances, so for simplicity’s sake, let’s look at the second method first. It takes two arguments.

ForParent is naturally the ViewGroup associated with ViewDragHelper.

Cb is of type ViewDragHelper.Callback is a Callback.

ViewDragHelper provides a series of callbacks to indicate various signals and state changes during drag. The methods are all displayed as follows:

int clampViewPositionHorizontal(View child, int left, int dx)

int clampViewPositionVertical(View child, int top, int dy)

int getOrderedChildIndex(int index)

int getViewHorizontalDragRange(View child)

int getViewVerticalDragRange(View child)

void    onEdgeDragStarted(int edgeFlags, int pointerId)

boolean onEdgeLock(int edgeFlags)

void    onEdgeTouched(int edgeFlags, int pointerId)

void    onViewCaptured(View capturedChild, int activePointerId)

void    onViewDragStateChanged(int state)

void    onViewPositionChanged(View changedView, int left, int top, int dx, int dy)

void    onViewReleased(View releasedChild, float xvel, float yvel)

abstract boolean    tryCaptureView(View child, int pointerId)
Copy the code

As a beginner, we started out focusing on a callback method that could form a complete sequence of drag events. This was the MVP rule, but the MVP was Mininum Viable Product. Generally speaking, it is a product that can run with the least amount of resources.

So, what are the easiest callbacks for ViewDragHelper to run?

// Determines whether the child needs to be captured so that the following drag-and-drop behavior can be performed
abstract boolean    tryCaptureView(View child, int pointerId)  


// Modify the horizontal coordinate of the child, left refers to the coordinate to which the child is moved, dx relative to the last offset
int clampViewPositionHorizontal(View child, int left, int dx)

// Modify the vertical coordinate of the child, top refers to the coordinate to which the child is to be moved, dy relative to the last offset
int clampViewPositionVertical(View child, int top, int dy)


// The finger release callback
void    onViewReleased(View releasedChild, float xvel, float yvel)
Copy the code

But with these callback methods, it’s not enough.

As mentioned earlier, it is possible to implement the onTouchEvent() method by ourselves, but the code we write is certainly not as stable as the Google developers, because they design the product.

Now, if you want to use ViewDragHelper to handle this process, you naturally have to delegate touch related actions to it. There are two ViewDrageHelper methods involved here.

/** Whether the children touch event should be intercepted, * only the ViewDragHelper can be intercepted ** put it in the onInterceptTouchEvent() method in the ViewGroup **/
boolean shouldInterceptTouchEvent(MotionEvent ev)

/** Handles the sequence of touch events passed in the ViewGroup */ in the onTouchEvent() method in the ViewGroup
void    processTouchEvent(MotionEvent ev)
Copy the code

Next, you can code with ViewDragHelper. We use it to override the drag action we implemented ourselves earlier.

public class TestViewGroup extends FrameLayout {
    private static final String TAG = "TestViewGroup";

    private ViewDragHelper mDragHelper;

    public TestViewGroup(Context context) {
        this(context,null);
    }

    public TestViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mDragHelper = ViewDragHelper.create(this.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 void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel); }}); }@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        return true; }}Copy the code

See, a few lines of code do the same thing. Here’s the thing

The tryCaptureView() method returnstrueWill only lead to the following callback method is called clampViewPositionHorizontal () and clampViewPositionVertical () when handling child drag position coordinates.Copy the code

Let’s see what happens:

ViewDragHelper implements drag rebound

Now, I’m going to make it harder. As before, the child bounces back to its original position when the finger is released.

This is where another API comes in.

Position the child at the coordinate (finalLeft,finalTop). settleCapturedViewAt(int finalLeft, int finalTop)Copy the code

Therefore, we also want to record the position of the child when it was first dragged, which can be set in the callback method.

void onViewCaptured(View capturedChild, int activePointerId)
Copy the code

So here’s how: 1. In onViewCaptured(), record the coordinates before drag. 2. OnViewReleased () calls settleCapturedViewAt() to relocate the child.

So, we fix the code:

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

    @Override
    public void onViewCaptured(View capturedChild, int activePointerId) {
        super.onViewCaptured(capturedChild, activePointerId);
        mDragOriLeft = capturedChild.getLeft();
        mDragOriTop = capturedChild.getTop();
    }

    @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 void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);

        mDragHelper.settleCapturedViewAt((int)mDragOriLeft,(int)mDragOriLeft); }});Copy the code

However, the effect did not change. SettleCapturedViewAt ()

/**
 * Settle the captured view at the given (left, top) position.
 * The appropriate velocity from prior motion will be taken into account.
 * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
 * on each subsequent frame to continue the motion until it returns false. If this method
 * returns false there is no further work to do to complete the movement.
 *
 * @param finalLeft Settled left edge position for the captured view
 * @param finalTop Settled top edge position for the captured view
 * @return true if animation should continue through {@link #continueSettling(boolean)} calls
 */
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
    if(! mReleaseInProgress) {throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to "
                + "Callback#onViewReleased");
    }

    return forceSettleCapturedViewAt(finalLeft, finalTop,
            (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
            (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
}
Copy the code

The comments say it all, and when settleCapturedViewAt() returns true, the developer needs to call continueSettleing() for each frame of the animation.

1. The purpose of the settleCapturedViewAt() call is to position the child (left,top), but it does not arrive instantaneously. 2. Call the continueSettling() method for each frame of the animation process until it returns false. 3. If continureSettling() returns false, the animation is settled.

For those of you who read my post No more Confused, maybe you never really understood Scroller and sliding mechanics before, you can quickly relate to Scroller. Scroller itself only animates numerical values and doesn’t move the View itself.

A View is able to realize the sliding mechanism of work with Scroller, is computeScroll () method is called Scroller.com puteScrollOffsets () method, and then call scrollTo () method, This cause redrawn constantly, constantly invoke computeScroll () method, and then kept on Scroller.com puteScrollOffset () method, finally make the Scroller animation work, And the value of Scroller change is mapped to mScrollX and mScrollY key attributes in View.

I explained this in a previous post:

Scroller cannot drive itself, and external conditions must call its computeScrollOffset() method many times. Because of these continuous transfers, Scroller itself is driven. This is a bit like the back flywheel of a bicycle, which turns itself only once the pedal has taken a turn. The pedals should be pressed continuously so that the bicycle will move smoothly. The proportion relation between the rear flywheel gear and pedal gear can be regarded as a mapping relation between mCurrentX and mCurrentY in Scroller and mScrollerX and mScrollerY in View.

Then, I gave a GIF.

Starting settleCapturedViewAt() is just the beginning, and continueSettleing() calls are needed, so I have a hunch that there’s a scroller-related mechanism going on here. So I see settleCapturedViewAt forceSettleCapturedViewAt () is the final call () method of the source code.

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
    final int startLeft = mCapturedView.getLeft();
    final int startTop = mCapturedView.getTop();
    final int dx = finalLeft - startLeft;
    final int dy = finalTop - startTop;

    if (dx == 0 && dy == 0) {
        // Nothing to do. Send callbacks, be done.
        mScroller.abortAnimation();
        setDragState(STATE_IDLE);
        return false;
    }

    final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
    mScroller.startScroll(startLeft, startTop, dx, dy, duration);

    setDragState(STATE_SETTLING);
    return true;
}
Copy the code

When I saw this, Scroller was the hero behind the scenes. All right, we got a lead. Knock it off. Research on Scroller is not the focus of this paper, as it is known that it can trigger a smooth displacement effect.

Going back to the original problem, continueSettleing() methods are called on every frame, and the best place to encode Scroller code is in the computeScroll() method in a ViewGroup.

public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

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

        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
            mDragOriLeft = capturedChild.getLeft();
            mDragOriTop = capturedChild.getTop();
            Log.d(TAG, "onViewCaptured: left:"+mDragOriLeft
                    +" top:"+mDragOriTop);
        }

        @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 void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);

            mDragHelper.settleCapturedViewAt((int)mDragOriLeft,(int)mDragOriTop); invalidate(); }}); }@Override
public void computeScroll() {
    super.computeScroll();
    if(mDragHelper ! =null && mDragHelper.continueSettling(true)) { invalidate(); }}Copy the code

Let’s see what happens

At this point, ViewDragHelper is pretty much done. There is one more important feature, however, and that is edge triggering.

The most common type of edge trigger is the sideslip menu.

As shown above, when an edge is triggered, you can start dragging by simply touching the corresponding edge of the View that responds to the drag.

So, how do you implement this behavior with ViewDragHelper?

First, you have to declare which successive touches ViewDragHelper recognizes.

void setEdgeTrackingEnabled (int edgeFlags)Copy the code

EdgeFlags is an integer variable that represents the edges that can be identified. It has a value of


/ / the left edge
public static final int EDGE_LEFT = 1 << 0;

/ / right edge
public static final int EDGE_RIGHT = 1 << 1;

/ / on the edge
public static final int EDGE_TOP = 1 << 2;

The lower edge of the / /
public static final int EDGE_BOTTOM = 1 << 3;

// All edges
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;Copy the code

Because the position operation is involved, the value of edgeFlags can be combined by multiple or operations. Such as

// Identify the left and upper edges
setEdgeTrackingEnabled(EDGE_LEFT | EDGE_TOP) 
Copy the code

ViewDragHelper can then recognize edge touches. It also has a corresponding Callback method in viewDragHelper.callback ().


/** Edge drag starts */
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    super.onEdgeDragStarted(edgeFlags, pointerId);
}


/** The edge is clicked */
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
    super.onEdgeTouched(edgeFlags, pointerId);
}Copy the code

We usually do this in the onEdgeDragStarted() method. As you can see, this method only informs the developer that the edge drag has started, but it does not provide a View parameter, so its purpose is clear: it only provides information about the edge drag, and it is up to the developer to decide which child will be dragged.

In normal project development, only one or more children respond to a particular edge drag.

So, in the onEdgeDragStarted() method, we should manually capture these children and make them drag phenomena.

Before, in the callback method

public boolean tryCaptureView(View child, int pointerId) {
    return true;
}Copy the code

The ID and type of the child can be used to determine whether the child responds to drag.

In the onEdgeDragStarted() method, there is no child of this type, so we need to manually specify a child via the API.

public void captureChildView(View childView, int activePointerId) {
    if(childView.getParent() ! = mParentView) {throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
                + "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
    }

    mCapturedView = childView;
    mActivePointerId = activePointerId;
    mCallback.onViewCaptured(childView, activePointerId);
    setDragState(STATE_DRAGGING);
}
Copy the code

As shown in the source code, this method sets childView directly to mCaptureView and calls mCallback’s callback method onViewCaptured().

In order not to confuse some students. Let’s look at the call flow of the tryCaptureView() method.

public void processTouchEvent(MotionEvent ev) {


    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int pointerId = ev.getPointerId(0);
            final View toCapture = findTopChildUnder((int) x, (int) y);

            tryCaptureViewForDrag(toCapture, pointerId);


            break; }}}boolean tryCaptureViewForDrag(View toCapture, int pointerId) {

    if(toCapture ! =null && mCallback.tryCaptureView(toCapture, pointerId)) {

        captureChildView(toCapture, pointerId);
        return true;
    }
    return false;
}
Copy the code

With the code streamlined, we can see this flow.

What conclusions can we draw?

Callback.trycaptureview () is called before captureChildView(). It is used as a basis for determining captureChildView() in normal flow only if it returns true. CaptureChildView can only be called.

Well, we call captureChildView() directly, bypassing the normal process and specifying a child as the view to be dragged. It feels a little violent, but as long as we understand their internal processes, there’s nothing wrong with them.

We continue to experiment. For now, our goal is to make the top childView respond to the effect of the edge trigger. So we can change the code like this.

public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

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

        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
            mDragOriLeft = capturedChild.getLeft();
            mDragOriTop = capturedChild.getTop();
            Log.d(TAG, "onViewCaptured: left:"+mDragOriLeft
                    +" top:"+mDragOriTop);
        }

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            super.onEdgeDragStarted(edgeFlags, pointerId);
            Log.d(TAG, "onEdgeDragStarted: "+edgeFlags);
            mDragHelper.captureChildView(getChildAt(getChildCount()-1),pointerId);
        }

        @Override
        public void onEdgeTouched(int edgeFlags, int pointerId) {
            super.onEdgeTouched(edgeFlags, pointerId);
            Log.d(TAG, "onEdgeTouched: "+edgeFlags);
        }

        @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 void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);

            mDragHelper.settleCapturedViewAt((int)mDragOriLeft,(int)mDragOriTop); invalidate(); }}); mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL); }Copy the code



What’s interesting here is that the edges are actually the common edges of ChildView. This is also the red box in the figure below.

And I didn’t think it made any sense. Go look at the source code.


/**
* Enable edge tracking for the selected edges of the parent view.
* The callback's {@link Callback#onEdgeTouched(int, int)} and
* {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked
* for edges for which edge tracking has been enabled.
*
* @param edgeFlags Combination of edge flags describing the edges to watch
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void setEdgeTrackingEnabled(int edgeFlags) {
    mTrackingEdges = edgeFlags;
}
Copy the code

The code comments indicate that the edges triggered by the edge refer to the four edges of the ViewGroup itself, not the edges of a ChildView as I had imagined.

Sliding edges refer to the four edges of the ViewGroup itself

Now that I’ve done the edge drag, what else is interesting about ViewDragHelper? These two might be of interest to you.

// Fast scrolling means that the view will continue to slide after the finger leaves due to inertia.
void    flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop);

// Slide the child smoothly to a position
boolean smoothSlideViewTo(View child, int finalLeft, int finalTop)
Copy the code

Let’s experiment with flingCapturedView(). Previously, we returned the dragged child to its original position in the onViewRelease() callback. Now, with a slight change, the other children still do this, but the lowest child will continue to slide for some distance after being dragged.

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    super.onViewReleased(releasedChild, xvel, yvel);

    View child = getChildAt(0);
    if( child ! =null && child == releasedChild ) {
        mDragHelper.flingCapturedView(getPaddingLeft(),getPaddingTop(),
                getWidth()-getPaddingRight()-child.getWidth(),
                getHeight()-getPaddingBottom()-child.getHeight());
    } else {

        mDragHelper.settleCapturedViewAt((int)mDragOriLeft,(int)mDragOriTop);
    }
    invalidate();
}
Copy the code

The effect is as follows:

For smoothSlideViewTo(), we can write this code in TestViewGroup

public void testSmoothSlide(boolean isReverse) {
    if( mDragHelper ! =null ) {
        View child = getChildAt(1);
        if( child ! =null ) {
            if ( isReverse ) {
                mDragHelper.smoothSlideViewTo(child,
                        getLeft(),getTop());
            } else{ mDragHelper.smoothSlideViewTo(child, getRight()-child.getWidth(), getBottom()-child.getHeight()); } invalidate(); }}}Copy the code

And then on the outside, I use a Button to control it.

public class MainActivity extends AppCompatActivity {

    Button mBtnTest;
    TestViewGroup mViewGroup;
    private boolean isReverse;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_draghelper);
        mViewGroup = (TestViewGroup) findViewById(R.id.test_viewgroup);
        mBtnTest = (Button) findViewById(R.id.btn_test);
        mBtnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mViewGroup.testSmoothSlide(isReverse);
                isReverse = !isReverse;
            }
        });
// setContentView(R.layout.activity_test);}}Copy the code

The effect is as follows:

ViewDragHelper manipulated TestViewGroup to slide properly.

But seeing a Button reminds me of a common sliding conflict problem in Android development. Since the Button itself responds to click events, can ViewDragHelper move the Button?

Default has no effect, what if I have to move the Button? Can ViewDragHelper be implemented? The answer is yes, we simply override the two Callback methods in viewDragHelper.callback


@Override
public int getViewHorizontalDragRange(View child) {
    return 1;
}

@Override
public int getViewVerticalDragRange(View child) {
    return 1;
}
Copy the code

As long as the return value of these two methods is greater than zero, then it can slide.

Some of you might be wondering, why don’t you use Boolean if you just return a value greater than 0?

In fact, the return value represents an X-axis or Y-axis distance that can be dragged, which is involved in calculating the duration of an action animation, such as the displacement of the settleCapturedViewAt() call.

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {

    final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
    mScroller.startScroll(startLeft, startTop, dx, dy, duration);

    return true;
}

private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {


    int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
    int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));

    return (int) (xduration * xweight + yduration * yweight);
}


private int computeAxisDuration(int delta, int velocity, int motionRange) {
    if (delta == 0) {
        return 0;
    }

    final int width = mParentView.getWidth();
    final int halfWidth = width / 2;
    final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
    final float distance = halfWidth + halfWidth
            * distanceInfluenceForSnapDuration(distanceRatio);

    int duration;
    velocity = Math.abs(velocity);
    if (velocity > 0) {
        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
    } else {
        final float range = (float) Math.abs(delta) / motionRange;
        duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
    }
    return Math.min(duration, MAX_SETTLE_DURATION);
}
Copy the code

Can see animated refer to a lot of variables, the length of the calculation is getViewHorizontalDragRange () and getViewVerticalDragRange () returns the value for the length results.

So, we only need getViewHorizontalDragRange () and getViewVerticalDragRange () returns a value greater than 1 indicates that the direction corresponding to the child can be moved, the return value is zero if it can’t move. Of course, this child is for controls like buttons that are themselves clickable.

This is the end of the article. You can see that ViewDragHelper is not as difficult as it might seem.

conclusion

Let’s go back to that GIF at the beginning of the article.



Now, it’s pretty achievable, isn’t it?

Of course, WHEN I demonstrated using inheritance FrameLayout, but actually there is a lot of work to do, you can try to use RecyclerView to implement it. The effect of this GIF is just an introduction, how to perfect it belongs to the actual combat of ViewDragHelper, is another topic. If you’re interested, you can do your own research.

Now, a bit of a refresher on this blog post.

  1. It is possible to implement drag and drop functionality without using ViewDragHelper, as shown in the blog post example and in the Launcher2 project.
  2. ViewDragHelper is a utility class that is designed for drag. It provides a set of methods and callback methods to manipulate drag and track the position and state of a child when it is being dragged.
  3. ViewDragHelper can drag the corresponding Child only if the tryCaptureView() callback returns true. However, the captureChildView() method can be called directly to specify the dragged child.
  4. ViewDragHelper to ViewGroup in onInterceptTouchEvent () method call shouldInterceptTouchEvent () method, The onTouchEvent() method in ViewGroup then calls processTouchEvent().
  5. There’s a Scroller variable inside ViewDragHelper, So the computeScroll() method of the ViewGroup should be overwrite when it comes to moving pictures in place such as settleCapturedViewAt(), flingCapturedView(), smoothSlideViewTo(). ContinueSettling () of ViewDragHelper is called in this method.
  6. If you want to move a clickable == true control like a Button, To autotype ViewDragHelper. Two of the Callback Callback methods getViewHorizontalDragRange () and getViewVerticalDragRange (), making them corresponding method returns a value greater than 0.

Related to the source code