View的滑动方式

作者: MonkeyLqj | 来源:发表于2018-10-31 10:55 被阅读11次

    View的滑动是Android自定义控件的基础,同时在开发中我们也难免会遇到View的滑动处理。其实不管是哪种滑动方式,其基本思想都是类似的:当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。

    一、坐标系

    Android系统中有两种坐标系,分别为Android坐标系和View坐标系。了解这两种坐标系能够帮助我们实现View的各种操作,比如我们要实现View的滑动,必须要知道这个View的位置,才能去操作,首先我们来看看Android坐标系。

    1.Android坐标系

    在Android中,将屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y 轴正方向。另外在触控事件中,使用getRawX()和getRawY()方法获得的坐标也是 Android坐标系的坐标。

    2.View坐标系

    View坐标系以当前控件左上角为坐标原点,向左为 X 轴正方向,向下为 Y 轴正方向,MotionEvent 的 getX()、getY() 方法获取的是点击位置在视图坐标系中的坐标,View 的 mLeft、mTop 等属性也是 View 在父控件的视图坐标系中的坐标。它与Android坐标系并不冲突,两者是共同存在的,它们一起来帮助开发者更好地控制View。


    坐标系

    二、滑动原理

    View 的滑动原理,其实滑动的原理与动画效果的实现非常相似,都是通过不断改变 View 的坐标来实现这一效果。所以要实现滑动效果就必须要监听用户的触摸事件,并根据事件传入的坐标,动态且不断的改变 View 的坐标,从而实现 View 跟随用户触摸的滑动而滑动。

    三、滑动方式

    实现View滑动有很多种 方法,在这里主要讲解6种滑动方法,分别是layout()、offsetLeftAndRight()与 offsetTopAndBottom()、LayoutParams、动画、scollTo 与 scollBy,以及Scroller。

    1.layout()

    View进行绘制的时候会调用onLayout()方法来设置显示的位置,因此我们同样也可以通过修改View 的left、top、right、bottom这4种属性来控制View的位置。

    首先我们要自定义一个View,在 onTouchEvent()方法中获取触摸点的坐标,代码如下所示:

    public class CustomView extends View {
    
        int lastX;
        int lastY;
    
        public CustomView(Context context) {
            super(context);
        }
    
        public CustomView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            // 记录触摸点坐标
            int x = (int) event.getX();
            int y = (int) event.getY();
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //当按下的时候执行
                    lastX = x;
                    lastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //当移动的时候执行
                    // 计算偏移量
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
                    // 在当前left、top、right、bottom的基础上加上偏移量来控制View的位置
                    layout(getLeft() + offsetX,
                            getTop() + offsetY,
                            getRight() + offsetX,
                            getBottom() + offsetY);
                    break;
                case MotionEvent.ACTION_UP:
                   //当抬起的时候执行
                    break;
            }
            return true;
        }
    }
    

    我们需要自定义一个CustomView 继承自View,需要重写onTouchEvent()方法。
    在MotionEvent.ACTION_DOWN事件中获取当前触摸点的坐标位置,然后在MotionEvent.ACTION_MOVE事件中计算偏移量,再调用layout()方法重新放置这个CustomView的位置即可。在每次移动时都会调用layout()方法对屏幕重新布局,从而达到移动View的效果。

    在布局文件中引用CustomView即可:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp"
        android:orientation="vertical"
        tools:context=".ui.CustomActivity">
    
        <com.example.monkey.myapplication.view.CustomView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@color/colorPrimary" />
    
    </LinearLayout>
    

    具体效果,可以自己动手试试。

    2.offsetLeftAndRight()与 offsetTopAndBottom()

    这两种方法和layout()方法的效果差不多,其使用方式也差不多。我们将ACTION_MOVE中的代码替 换成如下代码:

           case MotionEvent.ACTION_MOVE:
                    //当移动的时候执行
                    // 计算偏移量
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
    //                // 在当前left、top、right、bottom的基础上加上偏移量来控制View的位置
    //                layout(getLeft() + offsetX,
    //                        getTop() + offsetY,
    //                        getRight() + offsetX,
    //                        getBottom() + offsetY);
                    //左右偏移
                    offsetLeftAndRight(offsetX);
                    //上下偏移
                    offsetTopAndBottom(offsetY);
                    break;
    
    3.LayoutParams

    LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参 数从而达到改变View位置的效果。同样,我们将 ACTION_MOVE中的代码替换成如下代码:

            case MotionEvent.ACTION_MOVE:
                    //当移动的时候执行
                    // 计算偏移量
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
                    
                    LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                    layoutParams.leftMargin = getLeft() + offsetX;
                    layoutParams.topMargin = getTop() + offsetY;
                    setLayoutParams(layoutParams);
                    break;
    

    前面我们的布局文件,因为父控件是 LinearLayout,所以我们用了 LinearLayout.LayoutParams。如果父控件是RelativeLayout, 则要使用RelativeLayout.LayoutParams。否则会报错

     java.lang.ClassCastException: android.widget.LinearLayout$LayoutParams cannot be cast to android.widget.RelativeLayout$LayoutParams
            at com.example.monkey.myapplication.view.CustomView.onTouchEvent(CustomView.java:60)
            at android.view.View.dispatchTouchEvent(View.java:11788)
    
    

    当然除了使用布局的LayoutParams外,我们还可以用 ViewGroup.MarginLayoutParams来实现。因为LinearLayout和RelativeLayout都是ViewGroup的子类。

     ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                    layoutParams.leftMargin = getLeft() + offsetX;
                    layoutParams.topMargin = getTop() + offsetY;
                    setLayoutParams(layoutParams);
    
    4.scollTo 与 scollBy

    ScrollTo(dx,dy)指移动到一个具体的坐标点(dx,dy),而ScrollBy(dx,dy)则表示移动的增量为dx,dy。我们将 ACTION_MOVE中的代码替换成如下代码:

      case MotionEvent.ACTION_MOVE:
                    //当移动的时候执行
                    // 计算偏移量
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
                    ((View) getParent()).scrollBy(-offsetX, -offsetY);
                    break;
    

    首先scrollBy移动的是View的内容content,而不是View本身,如TextView的content为文本,ImageView的content为drawable,而ViewGroup的content是View或是ViewGroup,所以要移动当前View本身,我们就需要通过它的ViewGroup改变自己的内容从而改变View本身的位置。其次,我们真正操作的是View的父控件ViewGroup,要让View往左(上/右/下)移,应该要让ViewGroup往相反方向移动,也就是右(下/左/上),即偏移量就是相反的(负的)。所以要实现 CustomView 随手指移动的效果,就需要将偏移量设置为负值。若是正数,则会向相反的方向移动。

    我们通过ScrollTo和ScrollBy的源码看下其区别:

     /**
         * Set the scrolled position of your view. This will cause a call to
         * {@link #onScrollChanged(int, int, int, int)} and the view will be
         * invalidated.
         * @param x the x position to scroll to
         * @param y the y position to scroll to
         */
        public void scrollTo(int x, int y) {
            if (mScrollX != x || mScrollY != y) {
                int oldX = mScrollX;
                int oldY = mScrollY;
                mScrollX = x;
                mScrollY = y;
                invalidateParentCaches();
                //回调方法,通知状态改变
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);
                if (!awakenScrollBars()) {
                    postInvalidateOnAnimation();//重新绘制
                }
            }
        }
    
        /**
         * Move the scrolled position of your view. This will cause a call to
         * {@link #onScrollChanged(int, int, int, int)} and the view will be
         * invalidated.
         * @param x the amount of pixels to scroll by horizontally
         * @param y the amount of pixels to scroll by vertically
         */
        public void scrollBy(int x, int y) {
            scrollTo(mScrollX + x, mScrollY + y);
        }
    

    通过源码可以看到scrollBy()里面调用了ScrollTo(),传入的参数是mScrollX+x,也就是说这次x是一个增量,所以scrollBy实现的效果就是,在当前位置上,再偏移x距离 。这是ScrollTo()和ScrollBy()的重要区别。

    • scrollTo与scrollBy都会使View立即重绘,所以移动是瞬间发生的
    • scrollTo(x,y):指哪打哪,效果为View的左上角滚动到(x,y)位置,但由于View相对与父View是静止的所以最终转换为相对的View的内容滑动到(-x,-y)的位置。
    • scrollBy(x,y): 此时的x,y为偏移量,既在原有的基础上再次滚动
    5.Scroller

    通过上面的学习我们知道scrollTo与scrollBy可以实现滑动的效果,但是滑动的效果都是瞬间完成的,在事件执行的时候平移就已经完成了,这样的效果会让人感觉突兀,Google建议使用自然过渡的动画来实现移动效果。因此,Scroller类这样应运而生了。Scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效果。

    public class CustomView extends View {
    
        int lastX;
        int lastY;
        Scroller mScroller;
    
        public CustomView(Context context) {
            this(context,null);
        }
    
        public CustomView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mScroller = new Scroller(context);
        }
    
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            // 记录触摸点坐标
            int x = (int) event.getX();
            int y = (int) event.getY();
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //当按下的时候执行
                    lastX = x;
                    lastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //当移动的时候执行
                    // 计算偏移量
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
                    smoothScrollBy(-offsetX,-offsetY);
                    break;
                case MotionEvent.ACTION_UP:
                    //当抬起的时候执行
                    break;
            }
            return true;
        }
    
        public void smoothScrollBy(int dx,int dy){
            mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),dx,dy,2000);
            invalidate(); // 必须调用改方法通知View重绘以便computeScroll方法被调用。
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            // 判断Scroller滑动是否执行完毕
            if (mScroller.computeScrollOffset()) {
                ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                // 通过重绘让系统调用onDraw,onDraw中又会调用computeScroll,如此不断循环,直到Scroller执行完毕
                invalidate();
            }
        }
    

    Android为View的滑动提供了Scroller辅助类,它本身并不能导致View滑动,需要借助computeScroll和ScrollTo方法完成View的滑动。使用Scroller类完成View的平滑。

    • 首先要创建Scroller类。
    • 然后重写computeScroll方法,这里需要注意的是computeScroll方法在onDraw中会被调用,因此需要调用invalidate方法通知View调用onDraw重绘,然后再调用computeScroll完成View的滑动,过程为invalidate->onDraw->computeScroll->invalidate->…,无限循环直到mScroller的computeScrollOffset返回false,也就是滑动完成。
    • 调用Scroller类的startScroll方法开启滚动过程。
    6.动画

    使用动画来实现View的滑动主要通过改变View的translationX和translationY参数来实现,使用动画的好处在于滑动效果是平滑的。这里我们使用属性动画来移动view,我们让 CustomView在5000ms内沿着X轴向右平移300像素,具体实现如下:

      CustomView customview =  findViewById(R.id.customview);
      ObjectAnimator.ofFloat(customview,"translationX",0,300).setDuration(5000).start();
    

    也可以使用补间动画实现,在这里就不做多介绍了。
    本文到这里就结束了,如果有不对的地方,还望指正。

    参考资料
    《Android进阶之光》

    相关文章

      网友评论

        本文标题:View的滑动方式

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