Android开发Android技术知识Android开发经验谈

View的滑动方式

2018-10-31  本文已影响11人  MonkeyLqj

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()的重要区别。

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的平滑。

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进阶之光》

上一篇下一篇

猜你喜欢

热点阅读