自定义ViewGroup笔记--ViewDragHelper

在自定义ViewGroup中,使用ViewDragHelper会非常方便.

参考文章:http://wenku.baidu.com/link?url=Z-BbAV7WaxKJ0i14tyQMIwK1DtSNzJ_dW1dKv2HI3xQKNkKYCYz9-xSZVDsYqtZi9uISleDbNpMe_eus4utCDTj_xFE_St7PAlSg372kysO-zwJ9wUzamU83QS-Wwzr7

1.一般在ViewGroup的构造函数中初始化拖拽辅助类

参数
//父类的容器
// sensitivity 敏感度, 越大越敏感.1.0f是默认值 也可以称之为敏感阀值
// Callback 事件回调
mhelper = ViewDragHelper.create(forParent, sensitivity, Callback);

2.转交触摸事件,由ViewDragHelper来处理我们的touch事件

转交事件拦截
        @Override
public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
        // 由 ViewDragHelper 判断触摸事件是否该拦截
        return mHelper.shouldInterceptTouchEvent(ev);
}

转交事件处理
        @Override
public boolean onTouchEvent(MotionEvent event) {
        // 由 ViewDragHelper 处理事件
    try {
        mHelper.processTouchEvent(event);
    } catch (Exception e) {
        e.printStackTrace();
    }
        return true;
}

3.接收事件的回调 ViewDragHelper.Callback

1.public boolean tryCaptureView(View child, int pointerId) {} -->确定当前手指触摸的view是否可以被拖动

参数:
// child 被用户拖拽的子View
// pointerId 多点触摸的手指id

返回值:
确定当前点击的view是否可以拖动,如果返回false,则接下来的方法都不会调用,拿不到增量
如果我们不想某个view被拖动,那么我们可以在这里判断,如果参数里面的view等于不想拖动的view返回false就ok


2.public int getViewHorizontalDragRange(View child) {} -->确定拖拽的范围.只需要返回一个>0的值,决定了动画的执行时长,水平方向是否可以被滑开,用于底层的计算,并不能真正的限定水平拖拽的范围

参数:
//child 被用户拖拽的子view

返回值:
返回拖拽的范围  在这里,一般设置为滑动的边界值,这个值可以随便设置


3.public int clampViewPositionHorizontal(View child, int left, int dx) {} -->修正子View水平方向的位置. 此时还没有发生真正的移动.

参数:
//child 被用户拖拽的子view
//left 建议移动到的位置
//dx 与旧位置的差值

返回值:
返回需要移动的位置值,在这里我们需要做边界的处理


4.public void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {} -->当控件位置变化时 调用, 可以做 : 伴随动画, 状态的更新, 事件的回调.

参数:
//changedView 位置发生变化的子孩子
//left 最新的水平方向的位置
//top 最新的垂直方向的位置
//dx 刚刚发生的水平偏移量
//dy 刚刚发生的垂直偏移量

在这里,我们一般是根据需求去传递位置的变化量,以及伴随动画, 状态的更新, 事件的回调
为了兼容低版本,在此方法里需要调用重绘的方法 invalidate();


5.public void onViewReleased(View releasedChild, float xvel, float yvel) {} -->决定了松手之后要做的事情, 结束的动画

参数:
//releasedChild 手指松开后,被释放的子view
//xvel 水平方向的速度 正数代表向右滑动,负值代表向左滑动
//yvel 垂直方向的速度 正数代表向下滑动,负值代表向上滑动

在这里我们一般是处理手指松开后的平滑动画,底部封装了Scroller(实现步骤和使用Scroller类似)
    (1).触发平滑动画
    mHelper.smoothSlideViewTo(View child, int finalLeft, int finalTop)
    参数:
    //child 哪一个子view触发
    //水平方向要滑动到的位置
    //垂直方向要滑动到的位置
    返回值:
    boolean类型值,如果当前位置不是指定的最终位置,返回true

    (2).开始重绘界面
    ViewCompat.postInvalidateOnAnimation(View view); 重绘界面,为了使滑动流畅不掉帧,
    我们利用的v4包里的ViewCompat来实现
    参数:
    //view 是执行动画的父布局

    (3).维持动画的继续, 高频率调用
        public void computeScroll() {
            if(mHelper.continueSettling(true)){
            // 如果当前位置还没有移动到最终位置. 返回true.需要继续重绘界面
            ViewCompat.postInvalidateOnAnimation(this);
        }

4: invalidate&postInvalidate&postInvalidateOnAnimation区别

    invalidate:主线程重绘

    postInvalidate:可在子线程重绘

    postInvalidateOnAnimation
    在16之前,跟调用invalidate没有区别,api16之后,会调用View. postInvalidateOnAnimation新增方法。
    所以新增了postInvalidateOnAnimation方法,这个会对invalidate的频率做调整,减少阻塞message的机率。

5:在自定义view或者ViewGroup中,以下方法调用顺序
onFinishInflate()–>onSizeChanged()–>onLayout()

如何知道view已经绘制完成,并且显示到了界面
protected void onFinishInflate() {}
当view绘制完成,那么就可以在这里拿到子view的引用,可以加以逻辑判断,使代码的逻辑性更健壮

当view的尺寸发生变化时调用,一般在这里我们可以去获取定义的view的宽高
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    mWidth = w;
    mHeight = h;
} 

6:在做属性动画的时候,目的就是找到一个百分比(percent),其他就好办了,可以利用percent和startValue,endValue做动画,特别是利用系统提供的估值器

public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }

public Object evaluateColor(float fraction, Object startValue, Object endValue) {
        int startInt = (Integer) startValue;
        int startA = (startInt >> 24) & 0xff;
        int startR = (startInt >> 16) & 0xff;
        int startG = (startInt >> 8) & 0xff;
        int startB = startInt & 0xff;

        int endInt = (Integer) endValue;
        int endA = (endInt >> 24) & 0xff;
        int endR = (endInt >> 16) & 0xff;
        int endG = (endInt >> 8) & 0xff;
        int endB = endInt & 0xff;

        return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
                (int)((startR + (int)(fraction * (endR - startR))) << 16) |
                (int)((startG + (int)(fraction * (endG - startG))) << 8) |
                (int)((startB + (int)(fraction * (endB - startB))));
    }

两个例子,做个笔记.

/**
 * 侧滑面板
 * @author poplar
 *
 */
public class DragLayout extends FrameLayout {

    private ViewDragHelper mHelper;

    public static enum Status {
        Close, Open, Draging
    }
    private Status status = Status.Close;

    public interface OnDragChangeListener {
        void onClose();

        void onOpen();

        void onDraging(float percent);
    }

    private OnDragChangeListener onDragChangeListener;

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    public OnDragChangeListener getOnDragChangeListener() {
        return onDragChangeListener;
    }

    public void setOnDragChangeListener(OnDragChangeListener onDragChangeListener) {
        this.onDragChangeListener = onDragChangeListener;
    }

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

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

    public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // forParent 父类的容器
        // sensitivity 敏感度, 越大越敏感.1.0f是默认值
        // Callback 事件回调
        // 1. 创建 ViewDragHelper 辅助类 
        mHelper = ViewDragHelper.create(this, 1.0f, callback);
    }

    // 3. 接受处理的结果.
    ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

        // 1. 返回值, 决定了child是否可以被拖拽
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            // child 被用户拖拽的子View
            // pointerId 多点触摸的手指id
            System.out.println("tryCaptureView: ");
            return true;
        }

        // 2. 返回拖拽的范围. 返回一个 >0 的值, 决定了动画的执行时长, 水平方向是否可以被滑开
        @Override
        public int getViewHorizontalDragRange(View child) {
            return mRange;
        };

        // 3. 修正子View水平方向的位置. 此时还没有发生真正的移动.
        // 返回值决定了View将会移动到的位置
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // child 被拖拽的子View
            // left 建议移动到的位置
            // dx 跟旧的位置的差值
//          int oldLeft = mMainContent.getLeft();
//          System.out.println("clamp: " + " left: " + left + " dx: " + dx + " oldLeft: " + oldLeft);

            if(child == mMainContent){
                left = fixLeft(left);
            }
            return left;
        }

        // 4. 当控件位置变化时 调用, 可以做 : 伴随动画, 状态的更新, 事件的回调.
        @Override
        public void onViewPositionChanged(View changedView, int left, int top,
                int dx, int dy) {
            // left最新的水平方向的位置
            // dx 刚刚发生的水平变化量
//          System.out.println("onViewPositionChanged: " + " left:" + left + " dx: " + dx);

            if(changedView == mLeftContent){
                // 如果移动的是左面板
                // 1. 放回原来的位置
                mLeftContent.layout(0, 0, 0 + mWidth, 0 + mHeight);
                // 2. 把左面板发生的变化量dx转递给主面板
                int newLeft = mMainContent.getLeft() + dx;

                // 修正左边值.
                newLeft = fixLeft(newLeft);
                mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
            }

            dispatchDragEvent();

            // 为了兼容低版本, 手动重绘界面所有内容.
            invalidate();
        }


        //5. 决定了松手之后要做的事情, 结束的动画
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            System.out.println("onViewReleased: xvel: " + xvel);
            // releasedChild 被释放的孩子
            // xvel 水平方向的速度 向右为+,  向左为-
            if(xvel == 0 && mMainContent.getLeft() > mRange * 0.5f){
                open();
            } else if (xvel > 0) {
                open();
            } else {
                close();
            }

        }

        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
        }

    };

    /**
     * 分发拖拽事件, 伴随动画,更新状态.
     */
    protected void dispatchDragEvent() {
        // 0.0 -> 1.0  
        float percent = mMainContent.getLeft() * 1.0f / mRange;
        System.out.println("percent: " + percent);

        if(onDragChangeListener != null){
            onDragChangeListener.onDraging(percent);
        }

        // 更新状态
        Status lastStatus = status;
        status = updateStatus(percent);
        if(lastStatus != status && onDragChangeListener != null){
            if(status == Status.Close){
                onDragChangeListener.onClose();
            }else if (status == Status.Open) {
                onDragChangeListener.onOpen();
            }
        }


        // 执行动画
        animViews(percent);
    }


    /**
     * 更新状态
     * @param percent 当前动画执行的百分比
     * @return
     */
    private Status updateStatus(float percent) {
        if(percent == 0){
            return Status.Close;
        }else if (percent == 1) {
            return Status.Open;
        }
        return Status.Draging;
    }

    private void animViews(float percent) {
        //      - 左面板: 缩放动画, 平移动画, 透明度动画
                // 缩放动画 0.0 -> 1.0  >>> 0.0 -> 0.5 >>>0.5 -> 1.0
                // percent * 0.5 + 0.5
        //      mLeftContent.setScaleX(percent * 0.5f + 0.5f);
        //      mLeftContent.setScaleY(percent * 0.5f + 0.5f);
                ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
                ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));

        //      平移动画 -mWidth / 2.0f -> 0
                ViewHelper.setTranslationX(mLeftContent, evaluate(percent,  -mWidth / 2.0f, 0));

                // 透明度动画 0.2f -> 1.0
                ViewHelper.setAlpha(mLeftContent, evaluate(percent,  0.2f, 1.0f));

        //      - 主面板: 缩放动画 1.0 -> 0.8
                ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
                ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));

        //      - 背  景: 亮度变化
                getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);
    }

    public Object evaluateColor(float fraction, Object startValue, Object endValue) {
        int startInt = (Integer) startValue;
        int startA = (startInt >> 24) & 0xff;
        int startR = (startInt >> 16) & 0xff;
        int startG = (startInt >> 8) & 0xff;
        int startB = startInt & 0xff;

        int endInt = (Integer) endValue;
        int endA = (endInt >> 24) & 0xff;
        int endR = (endInt >> 16) & 0xff;
        int endG = (endInt >> 8) & 0xff;
        int endB = endInt & 0xff;

        return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
                (int)((startR + (int)(fraction * (endR - startR))) << 16) |
                (int)((startG + (int)(fraction * (endG - startG))) << 8) |
                (int)((startB + (int)(fraction * (endB - startB))));
    }

    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }

    /**
     * 修正位置
     * @param left
     * @return
     */
    private int fixLeft(int left) {
        if(left < 0){
            return 0;
        }else if (left > mRange) {
            return mRange;
        }
        return left;
    }

    /**
     * 关闭面板
     */
    public void close() {
        close(true);
    }

    public void close(boolean isSmooth){
        int finalLeft = 0;
        if(isSmooth){
            // 走平滑动画
            // 1. 触发一个平滑动画.
            if(mHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
                // 如果当前位置不是指定的最终位置. 返回true
                // 需要重绘界面, 一定要传 子View 所在的容器
                ViewCompat.postInvalidateOnAnimation(this);
            }

        }else {
            System.out.println("open");
            mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight);
        }
    }


    /**
     * 打开面板
     */
    public void open() {
        open(true);
    }

    public void open(boolean isSmooth){
        int finalLeft = mRange;
        if(isSmooth){
            // 走平滑动画
            // 1. 触发一个平滑动画.
            if(mHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
                // 如果当前位置不是指定的最终位置. 返回true
                // 需要重绘界面, 一定要传 子View 所在的容器
                ViewCompat.postInvalidateOnAnimation(this);
            }

        }else {
            System.out.println("open");
            mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight);
        }
    }

    //2. 维持动画的继续, 高频率调用.
    @Override
    public void computeScroll() {
        super.computeScroll();

        if(mHelper.continueSettling(true)){
            // 如果当前位置还没有移动到最终位置. 返回true.需要继续重绘界面
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private ViewGroup mLeftContent;
    private ViewGroup mMainContent;
    private int mHeight;
    private int mWidth;
    private int mRange;

    // 2. 转交触摸事件
    public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
        // 由 ViewDragHelper 判断触摸事件是否该拦截
        return mHelper.shouldInterceptTouchEvent(ev);
    };

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 由 ViewDragHelper 处理事件

        try {
            mHelper.processTouchEvent(event);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // 当控件尺寸变化的时候调用
        mHeight = getMeasuredHeight();
        mWidth = getMeasuredWidth();

        // 计算拖拽的范围
        mRange = (int) (mWidth * 0.6f);

        System.out.println("mWidth: " + mWidth + " mHeight: " + mHeight + " mRange: " + mRange);

    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 代码的健壮性.
        // 孩子至少俩
        if(getChildCount() < 2){
            throw new IllegalStateException("Your viewgroup must have 2 children. 子View至少有两个!");
        } 
        // 孩子必须是ViewGroup的子类
        if(!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) instanceof ViewGroup)){
            throw new IllegalArgumentException("Child must be an instance of ViewGroup . 孩子必须是ViewGroup的子类");
        }

        // Github
        mLeftContent = (ViewGroup) getChildAt(0);
        mMainContent = (ViewGroup) getChildAt(1);
    }

}

侧拉删除

/**
 * 侧拉删除
 * @author poplar
 *
 */
public class SwipeLayout extends FrameLayout {

    public static enum Status {
        Close, Open, Swiping
    }

    public interface OnSwipeListener{
        void onClose(SwipeLayout layout);
        void onOpen(SwipeLayout layout);

        void onStartOpen(SwipeLayout layout);
        void onStartClose(SwipeLayout layout);
    }

    private Status status = Status.Close;
    private OnSwipeListener onSwipeListener;

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    public OnSwipeListener getOnSwipeListener() {
        return onSwipeListener;
    }

    public void setOnSwipeListener(OnSwipeListener onSwipeListener) {
        this.onSwipeListener = onSwipeListener;
    }

    private ViewDragHelper mHelper;

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

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

    public SwipeLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // 1. 创建ViewDragHelper
        mHelper = ViewDragHelper.create(this, callback);
    }

    // 3. 重写回调的方法
    ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

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

        public int getViewHorizontalDragRange(View child) {
            return mRange;
        };

        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // 返回的值决定了将要移动到的位置.
            if(child == mFrontView){
                if(left < - mRange){
                    // 限定左范围
                    return - mRange;
                }else if (left > 0) {
                    // 限定右范围
                    return 0;
                }
            }else if (child == mBackView) {
                if(left < mWidth - mRange){
                    // 限定左范围
                    return mWidth - mRange;
                }else if (left > mWidth) {
                    // 限定右范围
                    return mWidth;
                }
            }
            return left;
        };

        // 位置发生改变的时候, 把水平方向的偏移量传递给另一个布局
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            if(changedView == mFrontView){
                // 拖拽的是前布局,  把刚刚发生的 偏移量dx 传递给 后布局
                mBackView.offsetLeftAndRight(dx);
            } else if (changedView == mBackView) {
                // 拖拽的是后布局,  把刚刚发生的 偏移量dx 传递给 前布局
                mFrontView.offsetLeftAndRight(dx);
            }

            dispatchDragEvent();

            invalidate();
        };

        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            // 松手时候会被调用

            // xvel 向右+, 向左-
            if(xvel == 0 && mFrontView.getLeft() < - mRange * 0.5f){
                open();
            }else if(xvel < 0){
                open();
            }else {
                close();
            }
        };
    };
    private View mBackView;
    private View mFrontView;
    private int mRange;
    private int mWidth;
    private int mHeight;

    // 2. 转交触摸事件拦截判断, 处理触摸事件
    public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
        return mHelper.shouldInterceptTouchEvent(ev);
    };

    /**
     * 更新当前的状态
     */
    protected void dispatchDragEvent() {


        Status lastStatus = status;
        // 获取最新的状态
        status = updateStatus();

        // 状态改变的时候, 调用监听里的方法
        if(lastStatus != status && onSwipeListener != null){
            if(status == Status.Open){
                onSwipeListener.onOpen(this);
            }else if (status == Status.Close) {
                onSwipeListener.onClose(this);
            }else if (status == Status.Swiping) {
                if(lastStatus == Status.Close){
                    onSwipeListener.onStartOpen(this);
                }else if (lastStatus == Status.Open) {
                    onSwipeListener.onStartClose(this);
                }
            }
        }



    }

    private Status updateStatus() {
        int left = mFrontView.getLeft();
        if(left == 0){
            return Status.Close;
        }else if (left == -mRange) {
            return Status.Open;
        }

        return Status.Swiping;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        // 维持平滑动画继续
        if(mHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /**
     * 关闭
     */
    protected void close() {
        close(true);
    }
    public void close(boolean isSmooth){

        int finalLeft = 0;
        if(isSmooth){
            // 触发平滑动画
            if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
                ViewCompat.postInvalidateOnAnimation(this);
            }

        }else {
            layoutContent(false);
        }
    }

    /**
     * 打开
     */
    protected void open() {
        open(true);
    }
    public void open(boolean isSmooth){

        int finalLeft = -mRange;
        if(isSmooth){
            // 触发平滑动画
            if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
                ViewCompat.postInvalidateOnAnimation(this);
            }

        }else {
            layoutContent(false);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        try {
            mHelper.processTouchEvent(event);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return true;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        // 默认是关闭状态
        layoutContent(false);
    }

    /**
     * 根据当前的开启状态摆放内容
     * @param isOpen
     */
    private void layoutContent(boolean isOpen) {
        // 设置前布局位置
        Rect rect = computeFrontRect(isOpen);
        mFrontView.layout(rect.left, rect.top, rect.right, rect.bottom);
        // 根据前布局位置设置后布局位置
        Rect backRect = computeBackRectViaFront(rect);
        mBackView.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);

        // 把任意布局顺序调整到最上
        bringChildToFront(mFrontView);
    }

    /**
     * 计算后布局的矩形区域
     * @param rect
     * @return
     */
    private Rect computeBackRectViaFront(Rect rect) {
        int left = rect.right;
        return new Rect(left, 0, left + mRange , 0 + mHeight);
    }

    /**
     * 计算前布局的矩形区域
     * @param isOpen
     * @return
     */
    private Rect computeFrontRect(boolean isOpen) {
        int left = 0;
        if(isOpen){
            left = -mRange;
        }
        return new Rect(left, 0, left + mWidth, 0 + mHeight);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mRange = mBackView.getMeasuredWidth();

        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();

    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mBackView = getChildAt(0);
        mFrontView = getChildAt(1);

    }

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