美文网首页Android开发之道与术Android知识Android开发
Android 自定义View学习(十五)——ViewDragH

Android 自定义View学习(十五)——ViewDragH

作者: 英勇青铜5 | 来源:发表于2016-10-08 16:56 被阅读1500次

    学习资料:


    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.

    ViewDragHelper是一个自定义ViewGroup的工具类。内部提供了一系列属性和用户拖动状态,并且支持恢复

    ViewDragHelper是解决各种滑动的终极绝招,几乎可以实现各种不同的的滑动、拖动


    1. 简单使用 <p>

    简单需求:
    在屏幕上,一个TextView可以拖动,并且当拖动的距离位于屏幕上半部分1/2区域内,可以自己恢复原始位置

    ViewDragHelper使用也有一个固定的模式

    1. 初始化ViewDragHer实例,并创建所需要的回调接口
    2. 处理事件拦截和事件的消费

    代码:

    public class DragView extends LinearLayout {
        private ViewDragHelper mViewDragHelper;
    
        public DragView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initDragHelper();
        }
    
        private void initDragHelper() {
            mViewDragHelper = ViewDragHelper.create(DragView.this, 1.0f, mDragCallback);
        }
    
        /**
         *  ViewDragHelper回调接口
         */
        private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {//可以用来指定哪一个childView可以拖动
                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 onInterceptHoverEvent(MotionEvent event) {//拦截事件
            return mViewDragHelper.shouldInterceptTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {//消费事件
            //将触摸事件传递给`ViewDragHelper`,必不可少
            mViewDragHelper.processTouchEvent(event);
            return true;
        }
    }
    
    

    布局文件:

    <com.szlk.customview.custom.DragView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorAccent"
            android:padding="20dp"
            android:text="@string/drag__name"
            android:textColor="@android:color/white"
            android:textSize="30sp" />
    
    </com.szlk.customview.custom.DragView>
    

    布局文件很简单,就是包含一个TextView

    简单使用

    DragView内,TextView就可以任意拖动


    在创建ViewDragHelper对象时,create()方法有两种形式

    //方式 1
    public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
        return new ViewDragHelper(forParent.getContext(), forParent, cb);
    }
    
    //方式2
    public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
        final ViewDragHelper helper = create(forParent, cb);
        helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
        return helper;
    }
    

    方式2多了第二个参数值,代表了灵敏度。sensitivity越大,helper.mTouchSlop越小,一般写为1.0f


    ViewDragHelper.Callback mDragCallback内,重写了3个方法

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

    这个方法可以用来指定哪一个childView可以进行拖动,通过重写onFinishInflate()来获取childView子控件,然后进行childView判断

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
        return left;
    }
    
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
        return top;
    }
    

    这两个方法默认返回值都为0
    left代表在水平方向上,childView即将在x轴移动到目标坐标位置,dx代表较前一次的增量,left = child.getLeft()+dxtop同理,代表垂直方向上childView即将在y轴移动到目标坐标位置

    虽然上面的代码实现了childView的拖动,但有些问题需要考虑优化


    2.简单进行Padding优化

    简单使用使用图中,首先一个问题便是TextView超出了屏幕范围,导致内容都无法显示完全。需要对lefttop进行修改

    修改代码

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
        final int leftPadding = getPaddingLeft();
        final int rightPadding = getWidth() - child.getWidth() - getPaddingRight();
        return Math.min(Math.max(left, leftPadding), rightPadding);
    }
    
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
        final int topPadding = getPaddingTop();
        final int bottomPadding = getHeight() - child.getHeight() - getPaddingBottom();
        return Math.min(Math.max(top, topPadding), bottomPadding);
    }
    
    移动范围
    可以移动的区域就是灰色区域,水平范围便是paddingLeft <= target <= getWidth()-child.getWidth()-getPaddingRight(),垂直方向同理。padding四边的值不一定相同。

    3.恢复到默认位置 <p>

    当在竖直方向上,拖动不超过DragView高度的一半,就会回弹到默认位置

    完整代码

    public class DragView extends LinearLayout {
        private ViewDragHelper mViewDragHelper;
        private Point initPoint;
        private View autoTextView;
    
        public DragView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initDragHelper();
        }
    
        private void initDragHelper() {
            mViewDragHelper = ViewDragHelper.create(DragView.this, 1.0f, mDragCallback);
            initPoint = new Point();
        }
    
        /**
         * ViewDragHelper回调接口
         */
        private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return true;
            }
    
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
                final int leftPadding = getPaddingLeft();
                final int rightPadding = getWidth() - child.getWidth() - leftPadding;
                final int newLeft = Math.min(Math.max(left, leftPadding), rightPadding);
                return newLeft;
            }
    
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
                final int topPadding = getPaddingTop();
                final int bottomPadding = getHeight() - child.getHeight() - topPadding;
                final int newTop = Math.min(Math.max(top, topPadding), bottomPadding);
                return newTop;
            }
    
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {//拖动结束后
                super.onViewReleased(releasedChild, xvel, yvel);
                if (releasedChild == autoTextView && releasedChild.getTop()  < (getHeight()/2)){
                    mViewDragHelper.smoothSlideViewTo(releasedChild,initPoint.x,initPoint.y);//平滑移动
                    ViewCompat.postInvalidateOnAnimation(DragView.this);
                }
            }
        };
    
        @Override
        public boolean onInterceptHoverEvent(MotionEvent event) {//拦截事件
            return mViewDragHelper.shouldInterceptTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {//消费事件
            mViewDragHelper.processTouchEvent(event);
            return true;
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mViewDragHelper.continueSettling(true)) {//不停计算位置后,自动移动
                ViewCompat.postInvalidateOnAnimation(DragView.this);//重新绘制
            }
        }
        /**
         * 完成解析布局xml文件
         */
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            autoTextView =  getChildAt(0);
        }
        /**
         * 布局
         */
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
            initPoint.x = autoTextView.getLeft();
            initPoint.y = autoTextView.getTop();
        }
    }
    

    利用onLayout()方法,拿到TextView起始点一开始的初始化坐标( initPoint.x,initPoint.y),在onViewReleased()方法中,进行结束拖动后的处理


    4.处理Button <p>

    DragView中,添加一个Button或者给TextView添加android:clickable="true",android:longClickable="true",便不能进行拖动处理

    原因根据前面学过的,Buttonclickable默认为ture,事件被消费了,DragView便不会再处理ACTION_MOVE,ACTION_UP 事件,Buttonclickable设置为false后,便可以拖动,但此时Button却不在可以点击

    想要实现Button既可以点击又又可以拖动,需要在ViewDragHelper.Callback mDragCallback重写两个方法

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

    注意:
    因为Button是可以点击的,当ACTION_DOWN事件发生时(也就是手指落在按钮上),之后的ACTION_MOVE,ACTION_UP便会由Button处理,需要从Button外的区域滑到Button内后,Button再才会跟随手指动作被拖动


    5. 边缘拖动 <p>

    在使用一些侧滑的控件时,有些可以从手机屏幕最左侧边缘滑出,ViewDragHelper.Callback mDragCallback提供了回调方法,使用有两个步骤

    • 第1步:指定边缘拖动目标控件

    边缘拖动代码:

    @Override
    public void onEdgeDragStarted(int edgeFlags, int pointerId) {
        super.onEdgeDragStarted(edgeFlags, pointerId);
        mViewDragHelper.captureChildView(autoTextView,pointerId);
    }
    

    使用captureChildView()方法来指定childView主动进行边缘拖动回调方法操作


    • 第2步:在初始化ViewDragHelper时,指定边缘方向
    mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
    

    方向共有5个值:

    ViewDragHelper.EDGE_ALL 四周都可以
    ViewDragHelper.EDGE_LEFT  左边缘
    ViewDragHelper.EDGE_RIGHT 右边缘
    ViewDragHelper.EDGE_TOP  顶部
    ViewDragHelper.EDGE_BOTTOM 底部
    

    6.Callback中的其他回调方法 <p>

    方法 作用
    onViewDragStateChanged(int state) ViewDragHelper拖动状态发生改变,STATE_IDLESTATE_DRAGGINGSTATE_SETTLING[自动滚动],分别对应0,1,2
    onViewPositionChanged() 拖动目标childView位置发生改变
    onViewCaptured(View capturedChild, int activePointerId) 调用captureChildView确定拖动目标时,回调此方法
    onEdgeTouched(int edgeFlags, int pointerId) 触摸ViewGroup边缘
    onEdgeLock(int edgeFlags) true的时候会锁住当前的边界,false则unLock
    getOrderedChildIndex(int index) 默认返回传入的index,可以重写将控件重新排序

    加上上面用过的方法,Callback的回调方法就这些


    7. ViewDragHelper常用方法 <p>

    方法 作用
    cancel() 取消拖动
    abort() 取消拖动的过程,直接将控件移动了指定位置
    captureChildView(View childView, int activePointerId) 将指定的子控件移动到指定位置
    continueSettling(boolean deferCallbacks) 自动不断计算位置后移动控件
    smoothSlideViewTo(View child, int finalLeft, int finalTop) child平滑移动到指定的位置
    settleCapturedViewAt(int finalLeft, int finalTop) 以手指离开时的速度为初速度,将控件移动到指定的位置
    shouldInterceptTouchEvent(MotionEvent ev) 判断父容器是否应该拦截事件
    processTouchEvent(MotionEvent ev) 处理触摸事件由父视图接收

    其他的以后用到了再学习补充


    8.最后

    都是方法的简单调用

    本人很菜,有错误请指出

    共勉 : )

    相关文章

      网友评论

      • 放纵的卡尔:写的很好,学习了.
        英勇青铜5: @放纵的卡尔 😀😀😀
      • Freerain:讲解确实很详细 : 不懂的地方是: 想要实现Button既可以点击又又可以拖动。重写的那两个方法是啥意思??
        英勇青铜5: @放纵的卡尔 当初考虑的这个案例不好,实际开发时应该没有这样的需求,
        放纵的卡尔:@英勇青铜5 这地方确实有问题,应该要重写拦截事件吧,否则的话无法移动,我自己测试的是这样的,
        英勇青铜5: @Freerain 可以理解为确定范围的吧。因为事件要拦截,就得有一个拦截区域。这里,感觉我写的有点问题,而且我举的案例不好,不容易说明问题。你研究研究,发现我写的有问题,就留言说一下吧
      • 一个冬季:不错,记住这个功能了
        英勇青铜5:@cao高 :smile: :smile:
      • ldlywt:正在学自定义view,有时间把楼主的系列全看一遍,楼主,加油
        英勇青铜5:@斥候丶 :smile: :smile:
      • dodo_lihao:感觉讲得很细,比鸿洋大神还细。能否提供一个源码的地址,用于参考。只是自己的建议。
        英勇青铜5:@dodo_lihao 基本都全部把代码贴出来了,就布局文件没有贴。

      本文标题:Android 自定义View学习(十五)——ViewDragH

      本文链接:https://www.haomeiwen.com/subject/crguyttx.html