[컴][안드로이드] Android ViewDragHelper 사용법 및 분석

ViewDragHelper 사용법 / ViewDragHelper 소스분석 / Analysis of the ViewDragHelper
youtube like view


How to use ViewDragHelper : ViewDragHelper 사용법


Code to be inserted in your custom Layout
자신이 만드는 Layout 에서 추가할 코드


public TestLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}

With this code, ViewDragHelper is connected to "this", ie. "this" instance will be a parent.

여기서 this 와 연결이 되는데, this 가 ViewDragHelper 의  parent 가 된다.


// *.java


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  final int action = MotionEventCompat.getActionMasked(ev);
  if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      mDragHelper.cancel();
      return false;
  }
  return mDragHelper.shouldInterceptTouchEvent(ev);
}
 
@Override
public boolean onTouchEvent(MotionEvent ev) {
  mDragHelper.processTouchEvent(ev);
  return true;
}

ViewDragHelper function should be called in the touch-treat functions such as onInterceptTouchEvent() and onTouchEvent().

이렇게 터치 이벤트 처리되는 곳에서 ViewDragHelper의 함수를 호출해 주면 된다.

참고로, 여기서 Captured View 는 drag 를 하려고 선택한 상태의 view 를 이야기 한다.





ViewDragHelper 의 이해


clampViewPosition* / onViewPositionChanged


// Stack trace

clampViewPositionVertical():105, YoutubeLayout$DragHelperCallback {com.example.samples.draghelper}
dragTo():1375, ViewDragHelper {android.support.v4.widget}
processTouchEvent():1117, ViewDragHelper {android.support.v4.widget}
onTouchEvent():165, YoutubeLayout {com.example.samples.draghelper}



// Source flow

processTouchEvent()
  case MotionEvent.ACTION_MOVE: 
    dragTo()

In processTouchEvent function, dratTo function is called. This dragTo invokes the below functions

processTouchEvent 에서 dragTo 를 호출한다. 이 dragTo() 를 보면 아래 함수들을 호출한다.
  • clampViewPositionHorizontal()
  • clampViewPositionVertical()
  • onViewPositionChanged()



private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            mCapturedView.offsetTopAndBottom(clampedY - oldTop);
        }

        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                    clampedDx, clampedDy);
        }
    }

clampViewPosition*() functions returns the how much size the View will be moved with the invokation of the below two functions. For the offsetLeftAndRight explanation, please see See Also part.
  • offsetLeftAndRight()
  • offsetTopAndBottom()
In onViewPositionChanged(), you can insert the code which is run during the movement of the View, such as changing the size of the view during the movement.

FYI, if you decide to change the UI in onViewPositionChanged(), you should, definitely, call the invalidate() or requestLayout() to redraw.



clampViewPosition*() 함수에서는 움직이는 위치를 return 하면 그 값으로 view 에서
  • offsetLeftAndRight()
  • offsetTopAndBottom()
을 호출해서 실제로 view 를 움직인다. offsetLeftAndRight 는 See Also 를 참고 하자.
  • onViewPositionChanged()
에서는 view 가 움직이면서 동시에 처리하고 싶은 동작이 있으면 여기서 처리하면 된다. 예를 들면 움직이면서 점점 크기가 작아진다든지 하는 동작들 말이다.

참고로, onViewPositionChanged 에서 만약 UI 의 변화를 줬다면 당연히 invalidate() 나 requestLayout() 등을 호출 해 줘야 한다.



onViewReleased


// Stack Trace

onViewReleased():88, YoutubeLayout$DragHelperCallback {com.example.samples.draghelper}
dispatchViewReleased():760, ViewDragHelper {android.support.v4.widget}
releaseViewForPointerUp():1362, ViewDragHelper {android.support.v4.widget}
processTouchEvent():1180, ViewDragHelper {android.support.v4.widget}
onTouchEvent():165, YoutubeLayout {com.example.samples.draghelper}


// Source flow

processTouchEvent
  case MotionEvent.ACTION_UP: 
      if (mDragState == STATE_DRAGGING) {
          releaseViewForPointerUp();
      }

private void releaseViewForPointerUp() {
        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
        final float xvel = clampMag(
                VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
                mMinVelocity, mMaxVelocity);
        final float yvel = clampMag(
                VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
                mMinVelocity, mMaxVelocity);
        dispatchViewReleased(xvel, yvel);
    }

private void dispatchViewReleased(float xvel, float yvel) {
        mReleaseInProgress = true;
        mCallback.onViewReleased(mCapturedView, xvel, yvel);
        mReleaseInProgress = false;
        ...
        

Like the name of onViewReleased, you can put your code to treat the captured view when release the captured view on touch-screen such as the view will go back to the original position. To do like this kind of action, you can use ViewDragHelper.
  • DragHelper.settleCapturedViewAt(finalLeft, finalTop);


onViewReleased 는 이름처럼 release 할 때의 일을 처리하면 된다. drag 하던 view (captured view) 를 놓았을 때, 할 일을 넣으면 된다. 예를 들면, 살짝 드래그 하고 터치를 때면 제자리로 돌아가게 한다던지 하는 일을 여기서 하게 할 수 있다.

이 때 많이 쓰이는 함수가 ViewDragHelper 에서 제공하는

  • DragHelper.settleCapturedViewAt(finalLeft, finalTop);

이다.




settleCaptureViewAt / getView*DragRange / smoothSlideViewTo

settleCaptureViewAt, at least you supply the left, top position, make your captured view go back to the original position with the similar speed you used at dragging.

At this time, getView*DragRange() function is used because of determining the duration.

For the details, please refer to the below source codes.


settleCaptureViewAt 는 현재 left, top 의 위치만 알려주면, captured view 를 그 위치에서 다시 예전의 위치로 드래그할 때와 비슷한 속도로 돌아간다.

이 때 getView*DragRange 함수가 사용된다. 얼마나 이동했는지를 알고 있어야 duration 계산을 할 수 있기 때문이다.

forceSettleCapturedViewAt 에서는 startScroll 을 호출한다. 그렇기 때문에 computeScroll 을 override 해서 사용할 수도 있다.

자세한 사항은 아래 소스를 참고하자.


smoothSlideViewTo 도 settleCapturedViewAt 과 비슷하다. forceSettleCapturedViewAt() 의 인자에서 velocity 부분만 '0' 으로 set 하는 부분만 다르다.



// Stack trace

getViewVerticalDragRange():97, YoutubeLayout$DragHelperCallback {com.example.samples.draghelper}
computeSettleDuration():612, ViewDragHelper {android.support.v4.widget}
forceSettleCapturedViewAt():589, ViewDragHelper {android.support.v4.widget}
settleCapturedViewAt():562, ViewDragHelper {android.support.v4.widget}
onViewReleased():92, YoutubeLayout$DragHelperCallback {com.example.samples.draghelper}



// Source flow

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    int top = getPaddingTop();
    if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {
        top += mDragRange;
    }
    mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
}


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));
}

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

    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;
}

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;
    }

    ...

    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);
}




tryCaptureView / onViewCaptured

"주어진 child view" 가 capture 되기를 원할 때 호출된다. capture 를 여기서 직접하는 것이 아니고 mCapturedView 에 현재 view 의 reference 를 저장해 놓는다.

user 가 "주어진 child view" 가 drag 되기를 원한다면 callback 은 return true 를 해야 한다.

onViewCapture 는 기본적으로 아무런 일도 하지 않는다. drag 하려는 view, 즉 captured view 가 결정된 후에 하고 싶은 일이 있다면 이 곳에 code 를 적어놓으면 된다. 쉽게 이야기 하면, drag 를 시작할 때 필요한 작업이 있다면 onViewCapture 에 넣으면 된다.

// processTouchEvent
processTouchEvent(MotionEvent ev) or shouldInterceptTouchEvent()
{
   tryCaptureViewForDrag(View toCapture, int pointerId);
}

// tryCaptureViewForDrag
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
   if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
      captureChildView(toCapture, pointerId);
   }
}

// captureChildView
public void captureChildView(View childView, int activePointerId) {
   ...
   mCallback.onViewCaptured(childView, activePointerId);
   ...
}




// ViewDragHelper.java

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
...

  switch (action) {
      case MotionEvent.ACTION_DOWN: {

      ...
      // Catch a settling view if possible.
          if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
              tryCaptureViewForDrag(toCapture, pointerId);
          }
      }

      case MotionEventCompat.ACTION_POINTER_DOWN: {
          ...
          // A ViewDragHelper can only manipulate one view at a time.
          if (mDragState == STATE_IDLE) {
              ...
          } else if (mDragState == STATE_SETTLING) {
              // Catch a settling view if possible.
              ...
              if (toCapture == mCapturedView) {
                  tryCaptureViewForDrag(toCapture, pointerId);
              }
              ...
          }
      }

      case MotionEvent.ACTION_MOVE: {
          // First to cross a touch slop over a draggable view wins. Also report edge drags.
          ...
          for (int i = 0; i < pointerCount; i++) {
              ...

              if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
                      tryCaptureViewForDrag(toCapture, pointerId)) {
                  break;
              }
          }
          ...
          break;
      }



public void processTouchEvent(MotionEvent ev) {
  ...

  switch (action) {
    case MotionEvent.ACTION_DOWN: {
        ...

        // Since the parent is already directly processing this touch event,
        // there is no reason to delay for a slop before dragging.
        // Start immediately if possible.
        tryCaptureViewForDrag(toCapture, pointerId);
        ...
      }

    case MotionEventCompat.ACTION_POINTER_DOWN: {
        ...

        // A ViewDragHelper can only manipulate one view at a time.
        if (mDragState == STATE_IDLE) {
            // If we're idle we can do anything! Treat it like a normal down event.
            ...
            tryCaptureViewForDrag(toCapture, pointerId);

            final int edgesTouched = mInitialEdgesTouched[pointerId];
            if ((edgesTouched & mTrackingEdges) != 0) {
                mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
            }
        } else if (isCapturedViewUnder((int) x, (int) y)) {
            // We're still tracking a captured view. If the same view is under this
            // point, we'll swap to controlling it with this pointer instead.
            // (This will still work if we're "catching" a settling view.)

            tryCaptureViewForDrag(mCapturedView, pointerId);
        }
      }

      case MotionEvent.ACTION_MOVE: {
        if (mDragState == STATE_DRAGGING) {
           ...
            dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

            saveLastMotion(ev);
        } else {
            // Check to see if any pointer is now over a draggable view.
            ...
            for (int i = 0; i < pointerCount; i++) {
                ...
                if (checkTouchSlop(toCapture, dx, dy) &&
                        tryCaptureViewForDrag(toCapture, pointerId)) {
                    break;
                }
            }
        }
      }

      case MotionEventCompat.ACTION_POINTER_UP: {
        final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
        if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
            // Try to find another pointer that's still holding on to the captured view.
            ...
            for (int i = 0; i < pointerCount; i++) {
                ...

                if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
                        tryCaptureViewForDrag(mCapturedView, id)) {
                    newActivePointer = mActivePointerId;
                    break;
                }
            }
        }
      }




onViewDragStateChanged


void setDragState(int state) {
    if (mDragState != state) {
        mDragState = state;
        mCallback.onViewDragStateChanged(state);
        if (state == STATE_IDLE) {
            mCapturedView = null;
        }
    }
}

onViewDragStateChanged is invoked when the setDragState() is called in the ViewDragHelper class.
You can use this to process about the just changed state.


onViewDragStateChanged 는 ViewDragHelper 내에서 setDragState() 가 실행돼서, state 가 바뀔 때 마다 호출된다.
state 가 특정state 로 변경될 때에 대한 처리등을 할 수 있다.




See Also

  1. offsetLeftAndRight() 과 scrollTo() 의 차이



References

  1. Android ViewDragHelper 2013/09/01 11:59 Posted in 안드로이드 개발/View by 김민수
  2. ViewDragHelper.Callback | Android Developers



댓글 없음:

댓글 쓰기