Android 一步一步教你使用ViewDragHelper

在自定義viewgroup的時候 要重寫onInterceptTouchEvent和onTouchEvent 這2個方法 是非常麻煩的事情,好在谷歌後來推出了ViewDragHelper這個類。可以極大方便我們自定義viewgroup.先看一個簡單效果 一個layout裏有2個圖片 其中有一個可以滑動 一個不能滑:
這裏寫圖片描述

這個效果其實還蠻簡單的(原諒我讓臭腳不能動 讓BABY動)
佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.administrator.viewdragertestapp.DragLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/a1"></ImageView>

        <ImageView
            android:id="@+id/iv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/a2"></ImageView>


    </com.example.administrator.viewdragertestapp.DragLayout>

</LinearLayout>

然後我們看一下自定義的layout 如何實現2個子view 一個可以滑動 一個不能滑動的:

package com.example.administrator.viewdragertestapp;

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.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * Created by Administrator on 2015/8/12.
 */
public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragger;

    private ViewDragHelper.Callback callback;

    private ImageView iv1;
    private ImageView iv2;

    @Override
    protected void onFinishInflate() {
        iv1 = (ImageView) this.findViewById(R.id.iv1);
        iv2 = (ImageView) this.findViewById(R.id.iv2);
        super.onFinishInflate();

    }

    public DragLayout(Context context) {
        super(context);

    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        callback = new DraggerCallBack();
        //第二個參數就是滑動靈敏度的意思 可以隨意設置
        mDragger = ViewDragHelper.create(this, 1.0f, callback);
    }

    class DraggerCallBack extends ViewDragHelper.Callback {

        //這個地方實際上函數返回值爲true就代表可以滑動 爲false 則不能滑動
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            if (child == iv2) {
                return false;
            }
            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 ev) {
        //決定是否攔截當前事件
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //處理事件
        mDragger.processTouchEvent(event);
        return true;
    }


}

然後再完善一下這個layout,剛纔滑動的時候我們的view 出了屏幕的邊界很不美觀 現在我們修改2個函數 讓滑動的範圍在這個屏幕之內(準確的說是在這個layout之內,因爲我們的佈局文件layout充滿了屏幕 所以看上去是在屏幕內):

//這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。
        // 我們要讓view滑動的範圍在我們的layout之內
        //實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。
        //如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍
        //除此之外就是如果你的layout設置了padding的話,也可以讓子view的活動範圍在padding之內的.

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //取得左邊界的座標
            final int leftBound = getPaddingLeft();
            //取得右邊界的座標
            final int rightBound = getWidth() - child.getWidth() - leftBound;
            //這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left
            //如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值
            //如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值
            return Math.min(Math.max(left, leftBound), rightBound);
        }

        //縱向的註釋就不寫了 自己體會
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            final int topBound = getPaddingTop();
            final int bottomBound = getHeight() - child.getHeight() - topBound;
            return Math.min(Math.max(top, topBound), bottomBound);
        }

我們看下效果:
這裏寫圖片描述

然後我們可以再加上一個回彈的效果,就是你把babay拉倒一個位置 然後鬆手他會自動回彈到初始位置其實思路很簡單 就是你鬆手的時候 回到初始的座標位置即可。

package com.example.administrator.viewdragertestapp;

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.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * Created by Administrator on 2015/8/12.
 */
public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragger;

    private ViewDragHelper.Callback callback;

    private ImageView iv1;
    private ImageView iv2;

    private Point initPointPosition = new Point();

    @Override
    protected void onFinishInflate() {
        iv1 = (ImageView) this.findViewById(R.id.iv1);
        iv2 = (ImageView) this.findViewById(R.id.iv2);
        super.onFinishInflate();

    }

    public DragLayout(Context context) {
        super(context);

    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        callback = new DraggerCallBack();
        //第二個參數就是滑動靈敏度的意思 可以隨意設置
        mDragger = ViewDragHelper.create(this, 1.0f, callback);
    }

    class DraggerCallBack extends ViewDragHelper.Callback {

        //這個地方實際上函數返回值爲true就代表可以滑動 爲false 則不能滑動
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            if (child == iv2) {
                return false;
            }
            return true;
        }


        //這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。
        // 我們要讓view滑動的範圍在我們的layout之內
        //實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。
        //如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍
        //除此之外就是如果你的layout設置了padding的話,也可以讓子view的活動範圍在padding之內的.

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //取得左邊界的座標
            final int leftBound = getPaddingLeft();
            //取得右邊界的座標
            final int rightBound = getWidth() - child.getWidth() - leftBound;
            //這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left
            //如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值
            //如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值
            return Math.min(Math.max(left, leftBound), rightBound);
        }

        //縱向的註釋就不寫了 自己體會
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            final int topBound = getPaddingTop();
            final int bottomBound = getHeight() - child.getHeight() - topBound;
            return Math.min(Math.max(top, topBound), bottomBound);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            //鬆手的時候 判斷如果是這個view 就讓他回到起始位置
            if (releasedChild == iv1) {
                //這邊代碼你跟進去去看會發現最終調用的是startScroll這個方法 所以我們就明白還要在computeScroll方法裏刷新
                mDragger.settleCapturedViewAt(initPointPosition.x, initPointPosition.y);
                invalidate();
            }
        }
    }

    @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);
        //佈局完成的時候就記錄一下位置
        initPointPosition.x = iv1.getLeft();
        initPointPosition.y = iv1.getTop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //決定是否攔截當前事件
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //處理事件
        mDragger.processTouchEvent(event);
        return true;
    }


}

看下效果:
這裏寫圖片描述

到這裏有人會發現 這樣做的話imageview就無法響應點擊事件了。繼續修改這個代碼讓iv可以響應點擊事件並且可以響應滑動事件。首先修改xml 把click屬性設置爲true 這個代碼就不上了,然後修改我們的代碼 其實就是增加2個函數:

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

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

然後看下效果:
這裏寫圖片描述

這個地方 如果你學過android 事件傳遞的話很好理解,因爲如果你子view可以響應點擊事件的話,那說明你消費了這個事件。如果你消費了這個事件話 就會先走dragger的 onInterceptTouchEvent這個方法。我們跟進去看看這個方法:

case MotionEvent.ACTION_MOVE: {
                if (mInitialMotionX == null || mInitialMotionY == null) break;

                // First to cross a touch slop over a draggable view wins. Also report edge drags.
                final int pointerCount = MotionEventCompat.getPointerCount(ev);
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = MotionEventCompat.getPointerId(ev, i);
                    final float x = MotionEventCompat.getX(ev, i);
                    final float y = MotionEventCompat.getY(ev, i);
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];

                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    if (pastSlop) {
                        // check the callback's
                        // getView[Horizontal|Vertical]DragRange methods to know
                        // if you can move at all along an axis, then see if it
                        // would clamp to the same value. If you can't move at
                        // all in every dimension with a nonzero range, bail.
                        final int oldLeft = toCapture.getLeft();
                        final int targetLeft = oldLeft + (int) dx;
                        final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
                                targetLeft, (int) dx);
                        final int oldTop = toCapture.getTop();
                        final int targetTop = oldTop + (int) dy;
                        final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
                                (int) dy);
                        final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                                toCapture);
                        final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                        if ((horizontalDragRange == 0 || horizontalDragRange > 0
                                && newLeft == oldLeft) && (verticalDragRange == 0
                                || verticalDragRange > 0 && newTop == oldTop)) {
                            break;
                        }
                    }
                    reportNewEdgeDrags(dx, dy, pointerId);
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag
                        break;
                    }

                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
                break;
            }

注意看29行到末尾你會發現,只有當horizontalDragRange 和verticalDragRange大於0的時候 對應的move事件纔會捕獲。否則就是丟棄直接丟給子view自己處理了。另外,還有一個效果就是 假如我們的 baby被拉倒了邊界處,我們的手指不需要拖動baby這個iv,手指直接在邊界的其他地方拖動此時也能把這個iv拖走。這個效果其實也可以實現,無非就是捕捉你手指在邊界處的動作 然後傳給你要拖動的view即可。代碼非常簡單 兩行即可再重寫一個回調函數 然後加個監聽:

@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    mDragger.captureChildView(iv1, pointerId);
}

mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);      

這個效果在模擬器上不知道爲啥 鼠標拖不動,GIF圖片我就不上了大家可以自己在手機裏跑一下就可以。上面的那些效果實際上都是DrawerLayout 等類似抽屜效果裏經常用到的函數。

ViewDragHelper讓我們很容易實現一個類似於YouTube視頻瀏覽效果的控件,效果如下:
這裏寫圖片描述

代碼中的關鍵點:
1.tryCaptureView返回了唯一可以被拖動的header view;
2.拖動範圍drag range的計算是在onLayout中完成的;
3.注意在onInterceptTouchEvent和onTouchEvent中使用的ViewDragHelper的若干方法;
4.在computeScroll中使用continueSettling方法(因爲ViewDragHelper使用了scroller)
5.smoothSlideViewTo方法來完成拖動結束後的慣性操作。
需要注意的是代碼仍然有很大改進空間。

activity_main.xml:

<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:tag="list"
            />
    <com.example.vdh.YoutubeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/youtubeLayout"
            android:orientation="vertical"
            android:visibility="visible">
        <TextView
                android:id="@+id/viewHeader"
                android:layout_width="match_parent"
                android:layout_height="128dp"
                android:fontFamily="sans-serif-thin"
                android:textSize="25sp"
                android:tag="text"
                android:gravity="center"
                android:textColor="@android:color/white"
                android:background="#AD78CC"/>
        <TextView
                android:id="@+id/viewDesc"
                android:tag="desc"
                android:textSize="35sp"
                android:gravity="center"
                android:text="Loreum Loreum"
                android:textColor="@android:color/white"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#FF00FF"/>
    </com.example.vdh.YoutubeLayout>
</FrameLayout>

YoutubeLayout.java:

public class YoutubeLayout extends ViewGroup {
private final ViewDragHelper mDragHelper;
private View mHeaderView;
private View mDescView;
private float mInitialMotionX;
private float mInitialMotionY;
private int mDragRange;
private int mTop;
private float mDragOffset;
public YoutubeLayout(Context context) {
  this(context, null);
}
public YoutubeLayout(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
    mHeaderView = findViewById(R.id.viewHeader);
    mDescView = findViewById(R.id.viewDesc);
}
public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
}
public void maximize() {
    smoothSlideTo(0f);
}
boolean smoothSlideTo(float slideOffset) {
    final int topBound = getPaddingTop();
    int y = (int) (topBound + slideOffset * mDragRange);
    if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
        ViewCompat.postInvalidateOnAnimation(this);
        return true;
    }
    return false;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
  @Override
  public boolean tryCaptureView(View child, int pointerId) {
        return child == mHeaderView;
  }
    @Override
  public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
      mTop = top;
      mDragOffset = (float) top / mDragRange;
        mHeaderView.setPivotX(mHeaderView.getWidth());
        mHeaderView.setPivotY(mHeaderView.getHeight());
        mHeaderView.setScaleX(1 - mDragOffset / 2);
        mHeaderView.setScaleY(1 - mDragOffset / 2);
        mDescView.setAlpha(1 - mDragOffset);
        requestLayout();
  }
  @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);
  }
  @Override
  public int getViewVerticalDragRange(View child) {
      return mDragRange;
  }
  @Override
  public int clampViewPositionVertical(View child, int top, int dy) {
      final int topBound = getPaddingTop();
      final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();
      final int newTop = Math.min(Math.max(top, topBound), bottomBound);
      return newTop;
  }
}
@Override
public void computeScroll() {
  if (mDragHelper.continueSettling(true)) {
      ViewCompat.postInvalidateOnAnimation(this);
  }
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  final int action = MotionEventCompat.getActionMasked(ev);
  if (( action != MotionEvent.ACTION_DOWN)) {
      mDragHelper.cancel();
      return super.onInterceptTouchEvent(ev);
  }
  if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      mDragHelper.cancel();
      return false;
  }
  final float x = ev.getX();
  final float y = ev.getY();
  boolean interceptTap = false;
  switch (action) {
      case MotionEvent.ACTION_DOWN: {
          mInitialMotionX = x;
          mInitialMotionY = y;
            interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
          break;
      }
      case MotionEvent.ACTION_MOVE: {
          final float adx = Math.abs(x - mInitialMotionX);
          final float ady = Math.abs(y - mInitialMotionY);
          final int slop = mDragHelper.getTouchSlop();
          if (ady > slop && adx > ady) {
              mDragHelper.cancel();
              return false;
          }
      }
  }
  return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
  mDragHelper.processTouchEvent(ev);
  final int action = ev.getAction();
    final float x = ev.getX();
    final float y = ev.getY();
    boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
    switch (action & MotionEventCompat.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN: {
          mInitialMotionX = x;
          mInitialMotionY = y;
          break;
      }
      case MotionEvent.ACTION_UP: {
          final float dx = x - mInitialMotionX;
          final float dy = y - mInitialMotionY;
          final int slop = mDragHelper.getTouchSlop();
          if (dx * dx + dy * dy < slop * slop && isHeaderViewUnder) {
              if (mDragOffset == 0) {
                  smoothSlideTo(1f);
              } else {
                  smoothSlideTo(0f);
              }
          }
          break;
      }
  }
  return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);
}
private boolean isViewHit(View view, int x, int y) {
    int[] viewLocation = new int[2];
    view.getLocationOnScreen(viewLocation);
    int[] parentLocation = new int[2];
    this.getLocationOnScreen(parentLocation);
    int screenX = parentLocation[0] + x;
    int screenY = parentLocation[1] + y;
    return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
            screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
    int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
            resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  mDragRange = getHeight() - mHeaderView.getHeight();
    mHeaderView.layout(
            0,
            mTop,
            r,
            mTop + mHeaderView.getMeasuredHeight());
    mDescView.layout(
            0,
            mTop + mHeaderView.getMeasuredHeight(),
            r,
            mTop  + b);
}

參考博客:
Android 一步一步教你使用ViewDragHelper
ViewDragHelper詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章