利用ViewDragHelper自定义ViewGroup之上拉抽屉布局效果

科普一下

  • 2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用,其实研究他们的源码你会发现这两个类都运用了ViewDragHelper来处理拖动。ViewDragHelper是Framework中不为人知却非常有用的一个工具。ViewDragHelper解决了android中手势处理过于复杂的问题,在DrawerLayout出现之前,侧滑菜单都是由第三方开源代码实现的,其中著名的当属MenuDrawer,MenuDrawer重写onTouchEvent方法来实现侧滑效果,代码量很大,实现逻辑也需要很大的耐心才能看懂。如果每个开发人员都从这么原始的步奏开始做起,那对于安卓生态是相当不利的。所以说ViewDragHelper等的出现反映了安卓开发框架已经开始向成熟的方向迈进。引用了网上的片段,其实说了那么多,ViewDragHelper就如名字说的一样,一个拖拽View的帮助类。只要我们这些普通开发者使用这个类,就可以轻松实现以前写了一大堆代码才实现的效果,代码更简洁,更优雅。

ViewDragHelper基本用法

  • 创建ViewDragHelper,建议在ViewGroup内部创建使用,而不是在外面使用。
//第二个参数设置滑动灵敏度
mViewDragHelper = ViewDragHelper.create(this, 1.0f,mCallback);
  • 看看mCallback回调接口ViewDragHelper.Callback,到底有什么重要的方法,直接上源码。
public static abstract class Callback {
        /**
         * 当ViewDragHelper状态发生变化时回调(IDLE,DRAGGING,SETTING[自动滚动时])
         * @see #STATE_IDLE
         * @see #STATE_DRAGGING
         * @see #STATE_SETTLING
         */
        public void onViewDragStateChanged(int state) {}

        /**
         * 当captureView的位置发生改变时回调
         * @param changedView View whose position changed
         * @param left New X coordinate of the left edge of the view
         * @param top New Y coordinate of the top edge of the view
         * @param dx Change in X position from the last call
         * @param dy Change in Y position from the last call
         */
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

        /**
         *当CaptureView被捕获时回调
         * @param capturedChild Child view that was captured
         * @param activePointerId Pointer id tracking the child capture
         */
        public void onViewCaptured(View capturedChild, int activePointerId) {}

        /**
         *手指释放的时候回调
         *
         * @param releasedChild The captured child view now being released
         * @param xvel X velocity of the pointer as it left the screen in pixels per second.
         * @param yvel Y velocity of the pointer as it left the screen in pixels per second.
         */
        public void onViewReleased(View releasedChild, float xvel, float yvel) {}

        /**
         * 当触摸到边界时回调。
         *
         * @param edgeFlags A combination of edge flags describing the edge(s) currently touched
         * @param pointerId ID of the pointer touching the described edge(s)
         * @see #EDGE_LEFT
         * @see #EDGE_TOP
         * @see #EDGE_RIGHT
         * @see #EDGE_BOTTOM
         */
        public void onEdgeTouched(int edgeFlags, int pointerId) {}

        /**
         *true的时候会锁住当前的边界,false则unLock。
         *
         * @param edgeFlags A combination of edge flags describing the edge(s) locked
         * @return true to lock the edge, false to leave it unlocked
         */
        public boolean onEdgeLock(int edgeFlags) {
            return false;
        }

        /**
         *
         *在边界拖动时状态回调
         * @param edgeFlags A combination of edge flags describing the edge(s) dragged
         * @param pointerId ID of the pointer touching the described edge(s)
         * @see #EDGE_LEFT
         * @see #EDGE_TOP
         * @see #EDGE_RIGHT
         * @see #EDGE_BOTTOM
         */
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {}

        /**
         * 改变同一个坐标(x,y)去寻找captureView位置的方法
         *
         * @param index the ordered position to query for
         * @return index of the view that should be ordered at position index
         */
        public int getOrderedChildIndex(int index) {
            return index;
        }

        /**
         * 
         *水平方向,子View要是消耗事件,就要重写此方法返回大于1的数。
         * @param child Child view to check
         * @return range of horizontal motion in pixels
         */
        public int getViewHorizontalDragRange(View child) {
            return 0;
        }

        /**
         *垂直方向,子View要是消耗事件,就要重写此方法返回大于1的数。
         *
         * @param child Child view to check
         * @return range of vertical motion in pixels
         */
        public int getViewVerticalDragRange(View child) {
            return 0;
        }

        /**
         *tryCaptureView如何返回ture则表示可以捕获该view即是哪个View可以滑动,哪个不可以滑动,在这个方
         *法里面控制
         * @param child Child the user is attempting to capture
         * @param pointerId ID of the pointer attempting the capture
         * @return true if capture should be allowed, false otherwise
         */
        public abstract boolean tryCaptureView(View child, int pointerId);

        /**
         * 水平滑动的边界处理
         *
         *
         * @param child Child view being dragged
         * @param left Attempted motion along the X axis
         * @param dx Proposed change in position for left
         * @return The new clamped position for left
         */
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return 0;
        }

        /**
         * 垂直滑动的边界处理
         *
         *
         * @param child Child view being dragged
         * @param top Attempted motion along the Y axis
         * @param dy Proposed change in position for top
         * @return The new clamped position for top
         */
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }
    }

大概了解所有的回调方法后,回归主题。
一个上拉抽屉布局,我脑子里首先想到的就是这样一个东东,图1。

利用ViewDragHelper自定义ViewGroup之上拉抽屉布局效果_第1张图片
Paste_Image.png

既然是这样,新建一个PullUpDragLayout直接继承ViewGroup,代码如下:

/**
 * @作 用:上拉的抽屉布局
 * @创 建 人: linguoding 邮箱:[email protected]
 * @日 期: 2016年11月16日  10:32
 */

public class PullUpDragLayout extends ViewGroup {
    private ViewDragHelper mViewDragHelper;//拖拽帮助类
    private View mBottomView;//底部内容View
    private View mContentView;//内容View
    private int mBottomBorderHeigth = 20;//底部边界凸出的高度
    public PullUpDragLayout(Context context) {
        super(context);
    }

    public PullUpDragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
    }
}

PullUpDragLayout 有两个子View,这两个子View怎么创建?可以java代码new,可以在xml布局里面写,也可以自定义属性,引用属性得到。new 的话就pass掉,这里直接使用后面两种方式。


        
        
        
    

有了属性,接下来就可以实例化mBottomView , mContentView ,还有得到边界高度 mBottomBorderHeigth的值。代码如下:

/**
 * @作 用:上拉的抽屉布局
 * @创 建 人: linguoding 邮箱:[email protected]
 * @日 期: 2016年11月16日  10:32
 */
public class PullUpDragLayout extends ViewGroup {
    private ViewDragHelper mViewDragHelper;//拖拽帮助类
    private View mBottomView;//底部内容View
    private View mContentView;//内容View
    private LayoutInflater mLayoutInflater;
    private int mBottomBorderHeigth = 20;//底部边界凸出的高度

    public PullUpDragLayout(Context context) {
        this(context, null, 0);
    }

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

    public PullUpDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
        initCustomAttrs(context, attrs);
    }


    private void init(Context context) {
        mLayoutInflater = LayoutInflater.from(context);
    }

    /**
     * 初始化自定义属性,并实例化子View
     * @param context
     * @param attrs
     */
    private void initCustomAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PullUpDragLayout);
        if (typedArray != null) {
            if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_ContentView)) {
                inflateContentView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_ContentView, 0));
            }
            if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomView)) {
                inflateBottomView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_BottomView, 0));
            }
            if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth)) {
                mBottomBorderHeigth = (int) typedArray.getDimension(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth, 20);
            }
            typedArray.recycle();
        }

    }

    private void inflateContentView(int resourceId) {
        mContentView = mLayoutInflater.inflate(resourceId, this, true);
    }

    private void inflateBottomView(int resourceId) {
        mBottomView = mLayoutInflater.inflate(resourceId, this, true);
    }


    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {

    }
}

实例化了子View后,PullUpDragLayout 既然是直接继承ViewGroup,那肯定要计算子View,还有子View在PullUpDragLayout怎么布局。那就要重写onMeasure(int widthMeasureSpec, int heightMeasureSpec),还有onLayout(boolean changed, int left, int top, int right, int bottom)方法。

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法计算子View的大小
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
             mContentView = getChildAt(0);
             mBottomView = getChildAt(1);
             measureChild(mBottomView, widthMeasureSpec, heightMeasureSpec);
             int bottomViewHeight = mBottomView.getMeasuredHeight();
             measureChild(mContentView, widthMeasureSpec, heightMeasureSpec);
             int contentHeight = mContentView.getMeasuredHeight();
             setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), bottomViewHeight +contentHeight  + getPaddingBottom() + getPaddingTop());

    }
  • onLayout(boolean changed, int left, int top, int right, int bottom)方法排列子View的位置
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        mContentView = getChildAt(0);
        mBottomView = getChildAt(1);
      
        mContentView.layout(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), mContentView.getMeasuredHeight());
        mBottomView.layout(getPaddingLeft(), mContentView.getHeight() - mBottomBorderHeigth, getWidth() - getPaddingRight(), getMeasuredHeight() - mBottomBorderHeigth);
      
    }

应用ViewDragHelper实现上拉效果

到此,就实现上面图1的静态效果啦。想要底部可以上拉,或者点击上拉到指定位置。那就要用到ViewDragHelper。在init()方法中创建ViewDragHelper.create(this, 1.0f,mCallback)。重写onInterceptTouchEvent()、onTouchEvent,将event事件交给ViewDragHelper拦截及处理,代码如下:

    private void init(Context context) {
        mLayoutInflater = LayoutInflater.from(context);
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);
    }

    ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mBottomView == child;
        }
    };
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

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

这样底部抽屉就可以随意滑动啦,但是这样还远远没有达到我要的效果。我们要实现以下效果

  • 指定方向滑动
  • 边界检测、加速度检测
  • 手指抬起,自动展开/收缩
  • 点击Button,展开/关闭
  • 监听展开/关闭,上拉过程发生改变时回调

慢慢来,下面一个一个地去实现。

  • 指定方向滑动
    控制水平方向不滑动,上下滑动。重写ViewDragHelper.Callback中的clampViewPositionHorizontal()方法,还有这个clampViewPositionVertical()。代码如下:
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            final int leftBound = getPaddingLeft();
            final int rightBound = getWidth() - mBottomView.getWidth() - leftBound;
            final int newLeft = Math.min(Math.max(left, leftBound), rightBound);

            return newLeft;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            int topBound = mContentView.getHeight() - mBottomView.getHeight();
            int bottomBound = mContentView.getHeight() - mBottomBorderHeigth;
            return Math.min(bottomBound, Math.max(top, topBound));
        }
  • 手指抬起,自动展开/收缩

重写onViewReleased()方法:代码如下:

        //手指释放的时候回调
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (releasedChild == mBottomView) {
                if (releasedChild.getY() < mBoundTopY || yvel <= -1000) {
                    mViewDragHelper.settleCapturedViewAt(mAutoBackTopPos.x, mAutoBackTopPos.y);
                    isOpen = true;
                    if (mOnStateListener != null) mOnStateListener.open();
                } else if (releasedChild.getY() >= mBoundTopY || yvel >= 1000) {
                    mViewDragHelper.settleCapturedViewAt(mAutoBackBottomPos.x, mAutoBackBottomPos.y);
                    isOpen = false;
                    if (mOnStateListener != null) mOnStateListener.close();
                }
                invalidate();
            }
        }
    };

      @Override 
      public void computeScroll() {
             if (mViewDragHelper.continueSettling(true)) { invalidate(); 
          }
      }
  • 点击Button,展开/关闭
    /**
     * 切换底部View
     */
    public void toggleBottomView() {
        if (isOpen) {
            mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackBottomPos.x, mAutoBackBottomPos.y);
            if (mOnStateListener != null) mOnStateListener.close();
        } else {
            mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackTopPos.x, mAutoBackTopPos.y);
            if (mOnStateListener != null) mOnStateListener.open();
        }
        invalidate();
        isOpen = !isOpen;
    }

  • 监听展开/关闭,上拉过程发生改变时回调

定义两个接口

    public void setOnStateListener(OnStateListener onStateListener) {
        mOnStateListener = onStateListener;
    }

    public void setScrollChageListener(OnScrollChageListener scrollChageListener) {
        mScrollChageListener = scrollChageListener;
    }

    public interface OnStateListener {
        void open();

        void close();
    }

    public interface OnScrollChageListener {
        void onScrollChange(float rate);
    }

重写onViewPositionChanged 监听位置的改变,计算上拉过程的比率值0-1。代码如下:

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            if (changedView == mBottomView) {
                float startPosition = mContentView.getHeight() - mBottomView.getHeight();
                float endPosition = mContentView.getHeight() - mBottomBorderHeigth;
                float totalLength = endPosition - startPosition;
                float rate = 1 - ((top - startPosition) / totalLength);
                if (mScrollChageListener != null) {
                    mScrollChageListener.onScrollChange(rate);
                }

            }
        }

到此,整个过程就走一遍了。下面放上完整代码:

/**
 * @作 用:上拉的抽屉布局
 * @创 建 人:  linguoding 邮箱:[email protected]
 * @日 期: 2016年10月28日  14:13
 */

public class PullUpDragLayout extends ViewGroup {
    private ViewDragHelper mViewDragHelper;//拖拽帮助类
    private View mBottomView;//底部内容View
    private View mContentView;//内容View
    LayoutInflater mLayoutInflater;
    private int mBottomBorderHeigth = 20;//底部边界凸出的高度

    private Point mAutoBackBottomPos = new Point();
    private Point mAutoBackTopPos = new Point();
    private int mBoundTopY;
    private boolean isOpen;
    private OnStateListener mOnStateListener;
    private OnScrollChageListener mScrollChageListener;


    public void setOnStateListener(OnStateListener onStateListener) {
        mOnStateListener = onStateListener;
    }

    public void setScrollChageListener(OnScrollChageListener scrollChageListener) {
        mScrollChageListener = scrollChageListener;
    }

    public interface OnStateListener {
        void open();

        void close();
    }

    public interface OnScrollChageListener {
        void onScrollChange(float rate);
    }


    public PullUpDragLayout(Context context) {
        this(context, null, 0);
    }

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

    public PullUpDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
        initCustomAttrs(context, attrs);
    }

    private void init(Context context) {
        mLayoutInflater = LayoutInflater.from(context);
        mViewDragHelper = ViewDragHelper.create(this, 1.0f,mCallback);
    }

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

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

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

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            final int leftBound = getPaddingLeft();
            final int rightBound = getWidth() - mBottomView.getWidth() - leftBound;
            final int newLeft = Math.min(Math.max(left, leftBound), rightBound);

            return newLeft;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            int topBound = mContentView.getHeight() - mBottomView.getHeight();
            int bottomBound = mContentView.getHeight() - mBottomBorderHeigth;
            return Math.min(bottomBound, Math.max(top, topBound));
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            if (changedView == mBottomView) {
                float startPosition = mContentView.getHeight() - mBottomView.getHeight();
                float endPosition = mContentView.getHeight() - mBottomBorderHeigth;
                float totalLength = endPosition - startPosition;
                float rate = 1 - ((top - startPosition) / totalLength);
                if (mScrollChageListener != null) {
                    mScrollChageListener.onScrollChange(rate);
                }

            }
        }

        //手指释放的时候回调
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (releasedChild == mBottomView) {
                if (releasedChild.getY() < mBoundTopY || yvel <= -1000) {
                    mViewDragHelper.settleCapturedViewAt(mAutoBackTopPos.x, mAutoBackTopPos.y);
                    isOpen = true;
                    if (mOnStateListener != null) mOnStateListener.open();
                } else if (releasedChild.getY() >= mBoundTopY || yvel >= 1000) {
                    mViewDragHelper.settleCapturedViewAt(mAutoBackBottomPos.x, mAutoBackBottomPos.y);
                    isOpen = false;
                    if (mOnStateListener != null) mOnStateListener.close();
                }
                invalidate();
            }
        }


    };

    public boolean isOpen() {
        return isOpen;
    }

    /**
     * 切换底部View
     */
    public void toggleBottomView() {
        if (isOpen) {
            mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackBottomPos.x, mAutoBackBottomPos.y);
            if (mOnStateListener != null) mOnStateListener.close();
        } else {
            mViewDragHelper.smoothSlideViewTo(mBottomView, mAutoBackTopPos.x, mAutoBackTopPos.y);
            if (mOnStateListener != null) mOnStateListener.open();
        }
        invalidate();
        isOpen = !isOpen;
    }


    private void initCustomAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PullUpDragLayout);
        if (typedArray != null) {
            if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_ContentView)) {
                inflateContentView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_ContentView, 0));
            }
            if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomView)) {
                inflateBottomView(typedArray.getResourceId(R.styleable.PullUpDragLayout_PullUpDrag_BottomView, 0));
            }
            if (typedArray.hasValue(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth)) {
                mBottomBorderHeigth = (int) typedArray.getDimension(R.styleable.PullUpDragLayout_PullUpDrag_BottomBorderHeigth, 20);
            }
            typedArray.recycle();
        }

    }

    private void inflateContentView(int resourceId) {
        mContentView = mLayoutInflater.inflate(resourceId, this, true);
    }

    private void inflateBottomView(int resourceId) {
        mBottomView = mLayoutInflater.inflate(resourceId, this, true);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mContentView = getChildAt(0);
        mBottomView = getChildAt(1);
        measureChild(mBottomView, widthMeasureSpec, heightMeasureSpec);
        int bottomViewHeight = mBottomView.getMeasuredHeight();

        measureChild(mContentView, widthMeasureSpec, heightMeasureSpec);
        int contentHeight = mContentView.getMeasuredHeight();
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), bottomViewHeight + contentHeight + getPaddingBottom() + getPaddingTop());


    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        mContentView = getChildAt(0);
        mBottomView = getChildAt(1);
        mContentView.layout(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), mContentView.getMeasuredHeight());
        mBottomView.layout(getPaddingLeft(), mContentView.getHeight() - mBottomBorderHeigth, getWidth() - getPaddingRight(), getMeasuredHeight() - mBottomBorderHeigth);
        mAutoBackBottomPos.x = mBottomView.getLeft();
        mAutoBackBottomPos.y = mBottomView.getTop();

        mAutoBackTopPos.x = mBottomView.getLeft();
        mAutoBackTopPos.y = mContentView.getHeight() - mBottomView.getHeight();
        mBoundTopY = mContentView.getHeight() - mBottomView.getHeight() / 2;

    }

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

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

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

XML中使用:

    
        
   

效果大概可以是这样的:

利用ViewDragHelper自定义ViewGroup之上拉抽屉布局效果_第2张图片
Paste_Image.png

展开时:

利用ViewDragHelper自定义ViewGroup之上拉抽屉布局效果_第3张图片
Paste_Image.png

大概就是这样的...............................

你可能感兴趣的:(利用ViewDragHelper自定义ViewGroup之上拉抽屉布局效果)