转载注明:http://blog.csdn.net/itermeng/article/details/52159637 阿里噶多~

如上图简单呈现出两个方块后,提出一个需求
1.拖动方块时,方块(即子View)可以跟随手指移动。
2.一个方块移动时,另一个方块可以跟随移动。
3.将方块移动到左边区域(右边区域)后放开(即手指离开屏幕),它会自动移动到左边界(右边界)。
4.移动的时候给方块加点动画(duang~duang~duang~) 。




View移动的相关方法总结:

1. layout

在自定义控件中,View绘制的一个重写方法layout(),用来设置显示的位置。所以,可以通过修改View的坐标值来改变view在父View的位置,以此可以达到移动的效果!但是缺点是只能移动指定的View:

    //通过layout方法来改变位置    view.layout(l,t,r,b);

2.offsetLeftAndRight() 和 offsetTopAndBottom()

非常方便的封装方法,只需提供水平、垂直方向上的偏移量,展示效果与layout()方法相同。

    view.offsetLeftAndRight(offset);//同时改变left和right    view.offsetTopAndBottom(offset);//同时改变top和bottom

3. LayoutParams

此类保存了一个View的布局参数,可通过LayoutParams动态改变一个布局的位置参数,以此动态地修改布局,达到View位置移动的效果!但是在获取getLayoutParams()时,要根据该子View对应的父View布局来决定自身的LayoutParams所以一切的前提是:必须要有一个父View,否则无法获取LayoutParams

//必须获取父View的LayoutParams         LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();        layoutParams.leftMargin = getLeft() + dx;        layoutParams.topMargin = getTop() + dy;        setLayoutParams(layoutParams);

4. scrollTo 和 scrollBy

通过改变scrollXscrollY来移动,但是可以移动所有的子ViewscrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(x,y)表示移动的增量为dx,dy。

    scrollTo(x,y);    scrollBy(xOffset,yOffset);

注意:这里使用scrollBy(xOffset,yOffset);,你会发现并没有效果,因为以上两个方法移动的是View的content。若在ViewGroup中使用,移动的是所有子View;若在View中使用,移动的是View的内容(比如TextView)。

所以,不可在view中使用以上方法!应该在View所在的ViewGroup中使用:

((View)getParent()).scrollBy(offsetX, offsetY);

【视图坐标系】:

可是即使这样,你会发现view移动的效果与设想方向相反!这是Android试图移动原因,若参数为正值,content将向坐标轴负方向移动;参数为负值,content将向坐标轴正方向移动。所以要实现随手指移动而滑动的效果,应将偏移量设置为负值即可:

((View)getParent()).scrollBy(-offsetX, -offsetY);

5. canvas

通过改变Canvas绘制的位置来移动View的内容,用的少:

 canvas.drawBitmap(bitmap, left, top, paint)

总结

但是要完成最开始的提的需求,不管使用哪一种方法,都需要通过onTouchEvent方法来捕捉手势,自己手动计算移动距离,再改变子View的布局,不免有些麻烦,所以在这里引出正文,介绍一个强大的类来处理移动:ViewDragHelper






ViewDragHelper介绍:

1. 产生: ViewDragHelper在高版本的v4包(android4.4以上的v4)中,于Google在2013年开发者大会提出的

2. 作用:它主要用于处理ViewGroup中对子View的拖拽处理。

3. 使用:它主要封装了对View的触摸位置触摸速度移动距离等的检测和Scroller,通过接口回调的方式通知我们。所以我们需要做的只是用接收来的数据指定这些子View是否需要移动,移动多少等。

4. 本质:是一个对触摸事件的解析类






ViewDragHelper实现

1. ViewDragHelper实例创建

    /**     * Factory method to create a new ViewDragHelper.     *     * @param forParent Parent view to monitor     * @param sensitivity Multiplier for how sensitive the helper should be about detecting     *the start of a drag. Larger values are more sensitive. 1.0f is normal.     * @param cb Callback to provide information and receive events     * @return a new ViewDragHelper instance     */viewDragHelper = ViewDragHelper.create(forParent, sensitivity, cb);

create()就是创建ViewDragHelper实例的方法,代码中的注释是create()中参数的解释,来查看:
(1)forParent :“用来监视的父View”。传入参数父View,即可监视该父View中的所有子View。
(2)sensitivity:“检测时的敏感度;值越大越敏感,1是正常范围”。比如说手指在滑动屏幕时速度特别快,敏感度越大时,此时速度快也可以检测到,反之亦然。
(3)Callback :“提供信息和接受的事件”。最重要的参数!可以从这个回调提供的信息获取到View滑动的距离、速度等。




2. 自定义View继承FrameLayout

这里来个小提示:之前实现的布局中自定义DragLayout是继承于ViewGroup,并且实现重写了onMeasure()方法,如下:

DragLayout.javapublic class DragLayout extends ViewGroup{    ...    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        //方法一:对子View的测量需求        /*获取子View的宽度100dp  的两种方法:        int size = (int) getResources().getDimension(R.dimen.width);        int size = readView.getLayoutParams().width;*/        int measureSpec = MeasureSpec.makeMeasureSpec(redView.getLayoutParams().width, MeasureSpec.EXACTLY);//具体指定宽高,为精确模式        redView.measure(measureSpec,measureSpec);//当父控件测量完子控件,才可以填(0,0)        blueView.measure(measureSpec,measureSpec);       /* //方法二:如果说没有特殊的对子View的测量需求,可用如下方法        measureChild(redView,widthMeasureSpec,heightMeasureSpec);        measureChild(blueView,widthMeasureSpec,heightMeasureSpec);*/    }}

但是现在使DragLayout 类继承于FrameLayout即可!

public class DragLayout extends FrameLayout{        ...}

因为在自定义ViewGroup的时候,如果对子View的测量没有特殊的需求,那么可以继承系统已有的布局(比如FrameLayout、RelativeLayout),目的是为了让已有的布局帮我们实现onMeasure()

所以在继承之后,我们无需实现onMeasure()方法,以上代码全部不需要(这里选择继承FrameLayout帧布局,原因是在Android源码中其实现最简单),所以重写继承的onLayout()方法其实是重写帧布局中的onLayout(),如果也注释的话,你会发现蓝色小方块覆盖红色,一起摆放在左上角(其实就是帧布局的摆放规则)




3. callback回调创建

    private ViewDragHelper.Callback callback = new Callback() {        //必须要实现的方法        @Override        public boolean tryCaptureView(View child, int pointerId) {            return false;        }    };



4. 触摸、拦截事件

以上部分ViewDragHelper的创建部分已完成,可是还没结束。比如大家熟悉的一个类:GestureDetector手势识别器,想要它生效,必须传一个触摸事件,这样GestureDetector类才可以解析当前手势。道理相同,之前在介绍ViewDragHelper已提到,它只是一个对触摸事件的解析类,需要传一个触摸事件,才会生效。

   //处理是否拦截    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        //由viewDragHelper 来判断是否应该拦截此事件        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);        return result;    }    @Override    public boolean onTouchEvent(MotionEvent event) {        //将触摸事件传给viewDragHelper来解析处理        viewDragHelper.processTouchEvent(event);        //消费掉此事件,自己来处理        return true;    }

以上则viewDragHelper可以监视并解析我们的手势了,而且会把信息通过回调传递给callback。




5. 处理computeScroll()

该方法是Scroller类的核心,系统在绘制View的时候在draw()中调用此方法,实际与scrollTo()相同。

    @Override    public void computeScroll() {        super.computeScroll();        if(scroller.computeScrollOffset()){            scrollTo(scroller.getCurrX(),scroller.getCurrY());            invalidate();        }

如上,Scroller类提供computeScrollOffset()方法来判断是否完成了整个滑动,同时getCurrX()getCurrY()来获得当前滑动坐标。

重点是invalidate()方法,因为只能在computeScroll()方法中获取模拟过程中的scrollXscrollYcomputeScroll()方法是不会自动调用的,只能通过invalidate() —> draw() —>computeScroll()来间接调用computeScroll()方法!模拟过程结束,if判断中computeScrollOffset()方法返回false,中断循环,完成整个平滑移动过程!

但是!!!我们并不采取以上方法,之前介绍过ViewDragHelper已经封装好了Scroller,用另外一种:

    @Override    public void computeScroll() {        super.computeScroll();        if(viewDragHelper.continueSettling(true)){            ViewCompat.postInvalidateOnAnimation(DragLayout.this);        }    }}

continueSettling()方法判断是否结束,同Scroller的方法相似,主要是postInvalidateOnAnimation(),此方法不像Scroller的scrollTo,还需要传值,其实此方法体内已经封装好移动的方法,它会自动去测量当前位置进行移动,所以我们只需调用即可!(在手指抬起时回调的方法中也会用到它,后面介绍)




6. 实现callback回调中的方法

之前在创建callback时,默认只实现了tryCaptureView()方法 ,完成需求仅仅不够,还需要其它方法,依次介绍:

(1) tryCaptureView()

此方法用于判断是否捕获当前child的触摸事件,可以指定ViewDragHelper移动哪一个子View。此例中,需要移动两个方块,则判断当前View是否是自己想移动的,返回boolean值。

        /**用于判断是否捕获当前child的触摸事件         * @param child         当前触摸的子View         * @return              true:捕获并解析      false:不处理         */        @Override        public boolean tryCaptureView(View child, int pointerId) {            return child == blueView || child == redView;        }

(2) onViewCaptured()

此方法在View被开始捕获和解析时回调,即当tryCaptureView()中的返回值为true的时候,此方法才会被调用。

例如tryCaptureView()方法中只捕获红色方块,当移动红方快时,该方法会回调,移动蓝色方块时则不会!

        /** 当View被开始捕获和解析的回调(用处不大)         * @param capturedChild     当前被捕获的子View         */        @Override        public void onViewCaptured(View capturedChild, int activePointerId) {            super.onViewCaptured(capturedChild, activePointerId);            Log.e("tag","onViewCaptures");        }

(3) clampViewPositionHorizontal() 和 clampViewPositionVertical()

这两个为具体滑动方法,分别对应水平和垂直方向上的移动。要想子View移动,此方法必须重写实现!

而方法的返回值则是指定View在水平(left)垂直(top)方向上变成的值,参数中的dxdy则是代表相较于上一次位置的增量

        /**     控制child在水平方向的移动         * @param child         * @param left  ViewDragHelper会将当前child的left值改变成返回的值         * @param dx    相较于上一次child在水平方向上移动的         * @return         */        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) {            return left;        }        /**控制child在垂直方向的移动         * @param child                  * @param top           ViewDragHelper会将当前child的top值改变成返回的值         * @param dy            相较于上一次child在水平方向上移动的         * @return         */        @Override        public int clampViewPositionVertical(View child, int top, int dy) {            return top;        }    };

显示效果:

通过以上GIF动图和日志打印可以看出,仅将返回值设置成方法中的参数,方块就可以任意移动了。也证实了方法中提供的参数而dx或dy是每一次移动的距离,left或top 是指定View移动到的位置,这是计算好了的,相当于left = child.getLeft() + dx
若想要它不移动,则:
return left - dx;
将它计算好后的距离减去相较于上次移动的距离即可,此时的View就不会移动。所以根据你的需求,可以任意改变此方法的返回值来移动View。


(4) getViewHorizontalDragRange() 和 getViewVerticalDragRange()

看到以上GIF动图,你会发现我在移动方块时,它可以超过边界,没有任何限制!有些不合理,想限制它的移动范围,这两个方法就可以获取View的拖拽范围,将它的返回值设为:父控件的宽/高 - 子控件的宽/高,即控件可以移动的范围。

        //获取View水平方向的拖拽范围        @Override        public int getViewHorizontalDragRange(View child) {            return getMeasuredWidth() - child.getMeasuredWidth();        }        //     获取View垂直方向的拖拽范围        @Override        public int getViewVerticalDragRange(View child) {            return getMeasuredHeight() - child.getMeasuredHeight();        }

可是以上实现后,你会发现拖拽方块还是可以超出边界,此方法并没有起作用!是否代表此方法完全无用?这返回的值有何用?

不是,它目前确实并不可以限制边界,但此方法返回的值会用在:比如说手指抬起时,View缓慢移动的动画时间的计算会用到此值,最好不要返回0(返回也不会错)!

但是我们还想要达到限制View拖拽边界的效果,这时在第三点介绍的clampViewPositionHorizontal() 和 clampViewPositionVertical()发挥效果了,该方法是通过返回值来改变View移动的位置,这时可以在方法中加判断是否有越界:

        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) {            if(left <0){                //限制左边界                left = 0;            }else if (left > (getMeasuredWidth() - child.getMeasuredWidth())){                //限制右边界                left = getMeasuredWidth() - child.getMeasuredWidth();            }            return left;        }

显示效果:


(5)onViewPositionChanged()

目前为止,需求已经完成可以任意拖拽View了,接下来完成拖拽View时,另一块跟随移动。这时介绍一个新的方法:onViewPositionChanged()该方法在child(需要捕捉的View)位置改变时执行,参数left(top)跟之前介绍方法中含义相同,为child最新的left(top)位置,而dx(dy)是child相较于上一次移动时水平(垂直)方向上改变的距离。

了解之后,就知道这个方法很强大了,在方法体中判断具体View,再根据方法提供的参数设置另一View的位置,如下:

    /**当child位置改变时执行         * @param changedView   位置改变的子View         * @param left           child最新的left位置         * @param top            child最新的top位置         * @param dx            相较于上一次水平移动的距离         * @param dy            相较于上一次垂直移动的距离         */    @Override        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {            super.onViewPositionChanged(changedView, left, top, dx, dy);            if(changedView == blueView){                //拖动蓝色方块时,红色也跟随移动                redView.layout(redView.getLeft()+dx , redView.getTop()+dy ,                        redView.getRight()+dx , redView.getBottom()+dy);            }else if(changedView == redView){                //拖动红色方块时,蓝色也跟随移动                blueView.layout(blueView.getLeft()+dx , blueView.getTop()+dy ,                        blueView.getRight()+dx , blueView.getBottom()+dy);            }        }

显示效果:


(6)onViewReleased()

完成目前需求第三个:手指在左边(右边)区域离开屏幕后,方块自动移动到左边界(右边界)。接下来介绍最后一个方法onViewReleased()手指抬起的时候执行该方法。

这里有两个新参数:
xvel: x方向移动的速度,若是正值,则代表向右移动,若是负值则向左移动;
yvel: y方向移动的速度,若是正值则向下移动,若是负值则向上移动。

       /**手指抬起的时候执行该方法         * @param releasedChild   当前抬起的View         * @param xvel             x方向移动的速度:正值:向右移动  负值:向左移动         * @param yvel             y方向移动的速度:正值:向下移动  负值:向上移动         */        @Override        public void onViewReleased(View releasedChild, float xvel, float yvel) {            super.onViewReleased(releasedChild, xvel, yvel);            int centerLeft = getMeasuredWidth()/2 - releasedChild.getMeasuredWidth()/2;            if(releasedChild.getLeft() < centerLeft){                //在左半边,应该向左缓慢移动,不用scroller,ViewDragHelper已封装好                viewDragHelper.smoothSlideViewTo(releasedChild,0,releasedChild.getTop());                //仍需要刷新!                ViewCompat.postInvalidateOnAnimation(DragLayout.this);//                scroller.startScroll();//                invalidate();            }else {                //在右半边,向右缓慢移动                viewDragHelper.smoothSlideViewTo(releasedChild,getMeasuredWidth() - releasedChild.getMeasuredWidth(),releasedChild.getTop());                ViewCompat.postInvalidateOnAnimation(DragLayout.this);            }        }    };

显示效果:




7. 执行伴随动画

还剩下最后一个需求,在方块移动时加些动画,说到动画引入一个概念:百分比(即子View左侧占子View可移动宽度的比例)。在移动子View的时候,比如从左到右,那么百分比则是0~1。做个实验,在回调onViewPositionChanged()加入两行:

            //1.计算view移动的百分比            float fraction = changedView.getLeft() * 1f / (getMeasuredWidth() - changedView.getMeasuredWidth());            Log.e("tag","fraction:"+fraction);            //2.执行一系列的伴随动画            executeAnim(fraction);

结果:

证实了我们以上的推论,所以现在可以通过传参数百分比来完成我们想要的动画效果:

    /**     * 执行伴随动画     * @param fraction     */    private void executeAnim(float fraction){        //fraction: 0 - 1        //缩放//      ViewHelper.setScaleX(redView, 1+0.5f*fraction);//      ViewHelper.setScaleY(redView, 1+0.5f*fraction);        //旋转//      ViewHelper.setRotation(redView,360*fraction);//围绕z轴转        ViewHelper.setRotationX(redView,360*fraction);//围绕x轴转//      ViewHelper.setRotationY(redView,360*fraction);//围绕y轴转        ViewHelper.setRotationX(blueView,360*fraction);//围绕z轴转        //平移//      ViewHelper.setTranslationX(redView,80*fraction);        //透明//      ViewHelper.setAlpha(redView, 1-fraction);    }

最终成品:






以上了解后,ViewDragHelper的学习到此为止,接下来利用它做一个侧滑什么的更是不在话下,包括现在网上的彷QQ侧滑面板都是利用ViewDragHelper完成的,所以工欲善其事,必先利其器呀~

关于View移动总结的,参照了徐宜生老师的《Android群英传》,讲解了许多View相关知识,重新加深理解了,还是很有帮助的。

如需要资源的,留言一下可发



希望对你们有帮助 :)

更多相关文章

  1. 如何打开USB调试模式(Android(安卓)所有版本对应的打开方法)
  2. [Android基础] VideoView
  3. 关于Android回调的理解
  4. Android数据库学习123
  5. 浅析Android中MVP及Demo
  6. Android(安卓)里的对话框Dialog 实现机制基础
  7. Android上获得系统root权限的方法
  8. 更新Android(安卓)SDK 访问谷歌等无需代理方法
  9. Handler内存泄漏和解决方法

随机推荐

  1. MySQL半同步复制原理配置与介绍详解
  2. 数据库sql语句优化
  3. MySQL排序中使用CASE WHEN的方法示例
  4. Mysql逻辑架构详解
  5. 数据库查询优化之子查询优化
  6. MySQL分区表的正确使用方法
  7. MySQL慢查询日志的基本使用教程
  8. MySQL InnoDB 二级索引的排序示例详解
  9. MySQL执行计划的深入分析
  10. B-树的插入过程介绍