Android Custom ViewGroup Artifact-ViewDragHelper

1. Overview

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 is the official explanation: ViewDragHelper can be used to drag and set the position of a child View (within the ViewGroup) when customizing the ViewGroup.A series of methods and state tracking are also provided.

Visible, when customizing ViewGroup s, ViewDragHelper is generally used to handle positional movement of subViews.

2. Examples of getting started

demo1.gif

The effect is simple: there are two TextView s in the middle of the screen, and the position keeps moving with our fingers.

Traditional implementation: Typically, two methods, onInterceptTouchEvent and onTouchEvent, need to be rewritten. Writing these two methods well is not an easy task and needs to be handled by yourself: event conflict, accelerated detection, etc.

ViewDragHelper simplifies a lot of work and focuses more on the needs of the Business by following these steps:

Create an instance of ViewDragHelper
Handle Touch Events for ViewGroup
Writing of ViewDragHelper.Callback
(1) Custom ViewGroup

public class VDHLinearLayout extends LinearLayout {
  ViewDragHelper dragHelper;

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

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

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

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
      return dragHelper.shouldInterceptTouchEvent(ev);
  }

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

The VDHLinearLayout code is still very simple and consists of three main steps:

Create an instance of ViewDragHelper

dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {});

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

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

The larger the incoming, the smaller the touchSlop.The third parameter is ViewDragHelper.Callback, which calls back related methods during touch.

Implement ViewDragHelper.Callback related methods

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

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

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

tryCaptureView: If returning true means capturing the relevant View, you can decide which View to capture based on the first parameter, child.
clampViewPositionVertical: Calculates the vertical position of the child, with top representing the y-axis coordinates (relative to the ViewGroup), returning 0 by default (if the method is not overridden).Here, you can control the range that can be moved vertically.
clampViewPositionHorizontal: Similar to clampViewPositionVertical, except that it controls the horizontal position.
For example, in the effect picture, "Drag 2" is obviously beyond the screen range, you can control it like this:

   @Override
     public int clampViewPositionHorizontal(View child, int left, int dx) {
        if (left > getWidth() - child.getMeasuredWidth()) // right border
        {
            left = getWidth() - child.getMeasuredWidth();
        }
        else if (left < 0) // Left Border
        {
            left = 0;
        }
        return left;
     }

Handle ViewGroup touch events

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
   return dragHelper.shouldInterceptTouchEvent(ev);
}

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

The onInterceptTouchEvent is handled directly by dragHelper.shouldInterceptTouchEvent, which is handled by dragHelper.processTouchEvent.

If you want the dragged child View to be non-clickable, you can avoid overriding the onInterceptTouchEvent method, which we'll explain later.

(2) Layout documents

<?xml version="1.0" encoding="utf-8"?>
<android.drag.viewdraghelperdemo.VDHLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:background="@color/colorPrimaryDark"
        android:textColor="@android:color/white"
        android:text="Drag 1"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:layout_marginTop="10dp"
        android:background="@color/colorPrimaryDark"
        android:textColor="@android:color/white"
        android:text="Drag 2"/>
</android.drag.viewdraghelperdemo.VDHLinearLayout>

The layout is simple, and the custom ViewGroup contains two TextView s.

3. More Usages

ViewDragHelper not only allows sub-Views to follow our fingers, but also does the following:

Boundary Touch Detection
Drag Release Callback
Move to a specified location
Let's transform the above example, and the result is as follows:

demo2.gif

First View, can be dragged anywhere
Second View, can only be dragged from the left side of ViewGroup
Third View, drag and release to return to original position

The modified ViewGroup code is as follows:

public class VDHLinearLayout extends LinearLayout {
  ViewDragHelper dragHelper;

  public VDHLinearLayout(Context context, AttributeSet attrs) {
      super(context, attrs);
      dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
          @Override
          public boolean tryCaptureView(View child, int pointerId) {
              return child == dragView || child == autoBackView;
          }

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

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

          // Callback after the currently captured View is released
          @Override
          public void onViewReleased(View releasedChild, float xvel, float yvel) {
              if (releasedChild == autoBackView)
              {
                  dragHelper.settleCapturedViewAt(autoBackViewOriginLeft, autoBackViewOriginTop);
                  invalidate();
              }
          }

          @Override
          public void onEdgeDragStarted(int edgeFlags, int pointerId) {
              dragHelper.captureChildView(edgeDragView, pointerId);
          }
      });
      // Set left edge to be Drag enabled
      dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
      return dragHelper.shouldInterceptTouchEvent(ev);
  }

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

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

  View dragView;
  View edgeDragView;
  View autoBackView;
  @Override
  protected void onFinishInflate() {
      super.onFinishInflate();
      dragView = findViewById(R.id.dragView);
      edgeDragView = findViewById(R.id.edgeDragView);
      autoBackView = findViewById(R.id.autoBackView);
  }

  int autoBackViewOriginLeft;
  int autoBackViewOriginTop;
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
      super.onLayout(changed, l, t, r, b);
      autoBackViewOriginLeft = autoBackView.getLeft();
      autoBackViewOriginTop = autoBackView.getTop();
  }
}

The tryCaptureView method, which captures only the first and third Views, dragView and autoBackView, respectively.

Use dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT) to set the left edge of ViewGroup to be dragged, and use dragHelper.captureChildView to actively capture the second View: edgeDragView in the onEdgeDragStarted method of ViewDragHelper.Callback.

Although we did not capture edgeDragView in the tryCaptureView method, dragHelper.captureChildView can bypass this method, as explained officially:

Capture a specific child view for dragging within the parent. The callback will be notified but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to capture this view.

The onViewReleased method is called after the captured child View is released, and we determine that the released View:releasedChild is an autoBackView, using the dragHelper.settleCapturedViewAt method to set the autoBackView's position to its original position.
Note that this method is implemented internally through a Scroller, so we need to use invalidate to refresh and override the computeScroll method:

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

The dragHelper.continueSettling method is used to determine if the currently captured child View still needs to move. Like Scroller's computeScrollOffset method, we need to refresh with invalidate when returning true.

So far, we've covered most of the uses of ViewDragHelper and ViewDragHelper.Callback.

Remember one of the questions we left before?

"If you want the dragged child View to be non-clickable, you can not override the onInterceptTouchEvent method. We'll show you why later."

If we try to set TextView to clickable=true, you will find that none of the Views that could have been dragged moved.Let's think about it. Why?

The reason is:

Since the child View is clickable, the onInterceptTouchEvent method of the ViewGroup is triggered.By default, events are consumed by sub-Views, which is obviously problematic because the onTouch method of ViewGroup will not be called, and the onTouch method is our key method: dragHelper.processTouchEvent.

Now that we've found the reason, someone said: Can't you go back to true directly from onInterceptTouchEvent?Why use the return value of dragHelper.shouldInterceptTouchEvent(ev)???

Indeed, if you go back to true directly, everything will work fine.

Here we need to explain:

For example, if you have another Button (or any clickable View) in your ViewGroup, but it is not within the scope of the ViewDragHelper, you may need to listen for its onClick event. If you return to true directly, you will find that the onClick event is not triggered.

Nani, why?Because the ViewGroup intercepted its event.Well, let's just write it like this:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return dragHelper.shouldInterceptTouchEvent(ev);
}

You can't wait to run the modified code.Huh?Why can't I drag it?
When this happens, I usually look at the source code for dragHelper.shouldInterceptTouchEvent (some unrelated code is omitted here):

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    switch (action) {
        case MotionEvent.ACTION_MOVE: {          
            final int pointerCount = ev.getPointerCount();
            for (int i = 0; i < pointerCount; i++) {           
                final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                            toCapture);
                final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                // break if both getViewHorizontalDragRange and getViewVerticalDragRange return values are 0
                if (horizontalDragRange == 0 && verticalDragRange == 0) {
                    break;
                }

                // mDragState=STATE_DRAGGING is set in the tryCaptureViewForDrag method
                if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                    break;
                }
            }
            break;
        }
    }
    return mDragState == STATE_DRAGGING;
}

ShouInterceptTouchEvent returns true if mDragState == STATE_DRAGGING, whereas mDragState is set to STATE_DRAGGING in the tryCaptureViewForDrag method.

Therefore, if the horizontalDragRange == 0 && verticalDragRange == 0 is always true, the tryCaptureViewForDrag method will not be called.

horizontalDragRange and verticalDragRange are values returned by the getViewHorizontalDragRange and getViewVerticalDragRange methods of Callback, respectively, which return 0 by default.

getViewHorizontalDragRange, returns the extent to which the horizontal direction of the child View can be dragged
getViewVerticalDragRange, returns the extent to which the child View can be dragged vertically
Let's try to override these two methods:

@Override
public int getViewVerticalDragRange(View child) {
   return getMeasuredHeight() - child.getMeasuredHeight();
}

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

Run it again and you'll find that TextView can also be dragged after setting clickable=true.

So far, the basic usage of ViewDragHelper has been described.

Author: zhuhf
Links: https://www.jianshu.com/p/111a7bc76a0e
Source: Short Book
Copyright belongs to the author.For commercial reprinting, please contact the author for authorization. For non-commercial reprinting, please indicate the source.

Keywords: Android xml encoding

Added by mw-dnb on Sat, 18 May 2019 07:59:49 +0300