Android Scroll分析

  滑动效果是如何产生的

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

    Android坐标系

      在物理学中,要描述一个物体的运动,就必须选定一个参考系。所谓滑动,正是相对于参考系的运动。在Android中,将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向

    系统提供了getLocationOnScreen(intlocation[])来获取Android坐标中的位置,即该视图左上角Android的坐标,另外,在触摸事件中使用getRawX(),getRawY()方法来获取坐标同样是Android坐标系中的坐标。

   视图坐标系

 Android中除了上面所说的这种坐标系之外,还有一个视图坐标系,它描述了子视图在父视图中的位置关系。这两种坐标系并不矛盾也不复杂,他们的作用是相辅相成的。与Android坐标系类似,视图坐标系同样是以原点向右为X轴正方向,以原点向下为Y轴正方向,只不过在视图坐标系中,原点不再是Android坐标系中的屏幕最左上角,而是以父视图左上角为坐标原点。

  

   在触摸事件中,通过getX(),getY()所获得的坐标就是视图坐标系中的坐标。

    触控事件---------MotionEvent

      触控事件MotionEvent在用户交互中,占着举足轻重的地位,学好触控事件是掌握后续内容的基础。首先,我们来看看MotionEvent中封装的一些常用的事件常量,它定义了触控事件的不同类型。

  public static final int ACTION_DOWN             = 0; //单点触摸按下动作
  public static final int ACTION_UP               = 1; //单点触摸离开动作
  public static final int ACTION_MOVE             = 2;//触摸点移动动作
  public static final int ACTION_CANCEL           = 3;//触摸动作取消
  public static final int ACTION_OUTSIDE          = 4;//触摸动作超出边界
  public static final int ACTION_POINTER_DOWN     = 5;//多点触控按下动作
  public static final int ACTION_POINTER_UP       = 6;//多点离开动作

     通常情况下,我们会在onTouchEvent(MotionEvent event)方法中通过event.getAction()方法来获取触控事件的类型,并通过switch-case方法来进行筛选,这个代码的模式基本固定。

   @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //获取当前输入点的X、Y坐标(视图坐标)
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //处理触摸的按下事件
                break;
            case MotionEvent.ACTION_MOVE:
                //处理触摸的移动事件
                break;
            case MotionEvent.ACTION_UP:
                //处理触摸的离开事件
                break;
        }
        return true;
    }

       在不涉及多点操作的情况下,通常可以使用以上代码来完成触控事件的监听,不过这里只是一个代码模板,后面我们会在触控事件中完成具体的逻辑。

        在Android中,系统提供了非常多的方法来获取坐标值、相对距离等。方法丰富固然好,但是也给初学者带来了很多困惑,不知道在什么情况下使用什么方法,下面总结了一些API,结合Android坐标系来看看该如何使用它们。

      

  这些方法可以分为如下两个类型:

 1.View提供的获取坐标的方法

   getTop():获取到的是View自身的顶边到其父布局顶边的距离

   getLeft():       获取到的是View自身的左边到其父布局左边的距离

   getRight():获取到的是View自身的右边到其父布局左边的距离

   getBottom():获取到的是View自身的底边到其父布局顶边的距离

2.MotionEvent提供的方法

getX():获取点击事件距离控件左边的距离,即视图坐标。

getY():获取点击事件距离控件顶边的距离,即视图坐标。

getRawX();获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

     实现滑动的七钟方法

  当了解了Android坐标系和触控坐标系后,我们再来看看如何使用系统提供的API来实现动态地修改一个View的坐标,即实现滑动效果。而不管采用哪一种方式,其实现的思想基本是一致的,当触摸View时,系统记下当前触摸点坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量来修改View的坐标,这样不断重复,从而实现滑动过程。

    例:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.administrator.myapplication.MainActivity">
<com.example.administrator.myapplication.DragView
    android:id="@+id/dragView"
    android:layout_width="100dp"
    android:layout_height="100dp" />

</LinearLayout>

   我们的目的就是让这个自定义View跟随手指在屏幕上的滑动而滑动。

  Layout方法

  我们知道,在View进行绘制的时候,会调用onLayout()方法来设置显示的位置。同样,可以通过修改View的left、top、right、bottom四个属性来控制View的坐标。与前面提供的模板代码一样,在每次回调onTouchEvent的时候,我们都来获取一下触摸点的坐标。

 
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getRawX();
    int rawY = (int) event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录触摸点的坐标
            lastX = rawX;
            lastY = rawY;

            break;
        case MotionEvent.ACTION_MOVE:
            //计算偏移量
          int offsetX = rawX - lastX;
          int offsetY  = rawY - lastY;
          layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
          //重新设置开始坐标
            lastX = rawX;
            lastY = rawY;

            break;
        case MotionEvent.ACTION_UP:
            //处理输入的离开动作
            break;
    }
    return true;
}

这里有点非常需要注意,就是如果使用视图坐标系,是不需要在每次移动后重置起始坐标的,因为它的参考坐标是不会随着View的移动而改变的,但是绝对布局,需要在每次移动后进行重置。

   offsetLeftAndRight()与offsetTopAndBottom()

   这个方法相当于系统提供的一个对左右,上下移动的API的封装。当计算出偏移量后,只需要使用如下代码代码就可以完成View的重新布局,效果与使用layout方法一样。

 

//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getX();
    int rawY = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录触摸点的坐标
            lastX = rawX;
            lastY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            //计算偏移量
            int officeX = rawX - lastX;
            int officeY = rawY - lastY;
            offsetLeftAndRight(officeX);
            offsetTopAndBottom(officeY);
            break;
        case MotionEvent.ACTION_UP:
            //处理输入的离开动作
            break;
    }
    return true;
}

    LayoutParams

   LayoutParams保存了一个View的布局参数。因此可以在程序中,通过改变LayoutParams来动态地修改一个布局的位置参数,从而达到改变View位置的效果。我们可以很方便地在程序中使用getLayoutParams()来获取一个View的layoutParams。当然计算偏移量的方法与在Layout方法中计算offset也是一样。当获取到偏移量之后,就可以通过setLayoutParams来改变其LayoutParams。

 

//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getX();
    int rawY = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录触摸点的坐标
            lastX = rawX;
            lastY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            //计算偏移量
            int officeX = rawX - lastX;
            int officeY = rawY - lastY;

            LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
            layoutParams.leftMargin = getLeft() + officeX;
            layoutParams.topMargin = getTop() + officeY;
            setLayoutParams(layoutParams);



            break;
        case MotionEvent.ACTION_UP:
            //处理输入的离开动作
            break;
    }
    return true;
}

    不过这里需要注意的是,通过getLayoutParams()获取layoutParams是,需要根据View所在的父布局的类型来设置不同的类型,比如这里将View放置在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。类似地,如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams.当然,这一切的前提是你必须要有一个父布局,不然系统无法获取LayoutParams。

     在通过改变LayoutParams来改变一个View的位置时,通常改变的是这个View的magin属性,所以除了使用布局的LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams:

 

//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getX();
    int rawY = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录触摸点的坐标
            lastX = rawX;
            lastY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            //计算偏移量
            int officeX = rawX - lastX;
            int officeY = rawY - lastY;

            ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
            layoutParams.leftMargin = getLeft() + officeX;
            layoutParams.topMargin = getTop() + officeY;
            setLayoutParams(layoutParams);


            break;
        case MotionEvent.ACTION_UP:
            //处理输入的离开动作
            break;
    }
    return true;
}

    我们可以发现,使用ViewGroup.MarginLayoutParams更加方便,不需要考虑父布局的类型,当然它们的本质都是一样的。

   scrollTo()与scrollBy()

   在一个View中,系统提供了scrollTo、scrollBy()两种方式来改变一个View的位置。这两个方法的区别非常好理解,与英文To与By的区别类似,scrollTo(x,y)标识移动到一个具体的坐标点(x,y),而scrollBy(dx,dy)表示移动的增量为dx,dy.

    与前面几种方式相同,在获取偏移量后使用scrollBy来移动View。

 //触摸事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getX();
        int rawY = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //记录触摸点的坐标
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int officeX = rawX - lastX;
                int officeY = rawY - lastY;

                scrollBy(officeX,officeY);


                break;
            case MotionEvent.ACTION_UP:
                //处理输入的离开动作
                break;
        }
        return true;
    }

       但是当我们拖动View的时候,发现View并咩有移动。因为它移动的并不是我们想要移动的东西。scrollTo、scrollBy方法移动的是View的 content,即让View的内容移动,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移动的将是所有子View,但如果在View中使用,那么移动的将是View的内容,例如TextView,content就是它的文本;imageview,content就是它的drawable对象。

      下面改进:

 

((View)getParent()). scrollBy(officeX,officeY);

    但是,当再次拖动View的时候,你会发现View虽然动了,但是却在乱动。

上图,中间的矩形相当于屏幕,即可是区域,后面的content相当于画布,代表视图,大家可以看到,只有视图的中间部分目前是可视的,其他部分都不可见,可见区域中设置一个button,他的坐标是(20.10),下面我们使用scrollBy方法来进行移动后如图

 

我们会发现,虽然设置scrollBy(20.10),偏移量均为XY的正方向,但是屏幕的可视区域,Button却向反方向移动了,这就是参考系选择的不同,而产生的不同效果。

通过上面的分析,可以发现,我们将scrollBy的参数dx,dy设置成正数,那么content将向坐标轴负方向移动,反之,则正方向

完善:

 

//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
    int rawX = (int) event.getX();
    int rawY = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录触摸点的坐标
            lastX = rawX;
            lastY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            //计算偏移量
            int officeX = rawX - lastX;
            int officeY = rawY - lastY;

            ((View)getParent()). scrollBy(-officeX,-officeY);


            break;
        case MotionEvent.ACTION_UP:
            //处理输入的离开动作
            break;
    }
    return true;
}

   scroller

 既然提到了scrollTo、scrollBy方法,就不得不再说一说Scroller类。Scroller类与scrollTo、scrollBy方法十分相似,有着千丝万缕的联系。

  1.初始阿虎scroller

  首先,通过它的构造方法来创建一个人Scroller对象

//初始化mScroller
mScroller = new Scroller(context);

重写computeScroll()方法,实现模拟滑动

   下面我们需要重写computeScroll()方法,它是使用Scroller类的核心,系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际上就是使用scrollTo方法。再结合Scroller对象,帮助获取到当前的滚动值。我们可以通过不断地瞬间移动一个小距离来实现整体上的平滑效果。通常情况下,computeScroll的代码可以利用如下模板代码来实现。

   

  /**
     * 模拟滑动
     */
    @Override
    public void computeScroll() {
        super.computeScroll();

        //判断Scroller是否执行完毕
        if(mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        }
        //通过重绘来不断调用computeScroll
        
invalidate()

(); }

   Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了getCurrX()、getCurrY()方法来获得当前的滑动坐标。在上面的代码中,唯一需要注意的是invalidate()方法,因为只能在computeScroll()方法中获取模拟过程中的scrollX和scrollY坐标。但computeScroll()方法是不会自动调用的,只能通过invalidate()->draw()->computeScroll()来间接调用computeSroll()方法,所以需要在模拟代码中调用invalidate()方法,实现循环获取scrollX和scrollY的目的。而当模拟过程结束后,scroller。computeScrollOffset()方法会返回false,从而中断循环,完成整个平滑移动过程。

  startScroll开启模拟过程

最后,万事具备只欠东风。我们在需要使用平滑移动的事件中,使用Scroller类的startScroll()方法来开启平滑移动过程。startScroll()方法具有两个重载方法。

    public void startScroll(int startX,int startY,int dx,int dy,int duration)
    public void startScroll(int startX,int startY,int dx,int dy)

    可以看到它们的区别就是一个具有指定的持续时长,而另一个没有。这个非常好理解,与在动画中设置duration和使用默认的显示时长是一个道理。而其他四个坐标,则与它们的命名含义相同,就是起始坐标与偏移量。在获取坐标的时候,通常可以使用getScrollX()和getScrollY()方法来获取父视图中content所滑动到的点的坐标,不过要注意的是这个值得正负,它与在scrollBy、scrollTo中讲解的情况是一样的。

     通过上面三个步骤,我们就可以使用Scroller类来实现平滑移动了,下面回到实例中,在构造方法中初始化Scroller对象,并重写View的computeScroll()方法。最后,需要监听手指离开屏幕的事件,并在该事件中通过调用startScroll()方法完成平滑移动。

case MotionEvent.ACTION_UP:
                //处理输入的离开动作
                View view = ((View)getParent());
                mScroller.startScroll(view.getScrollX(),view.getScrollY(),view.getScrollX(),view.getScrollY());
                invalidate();
                break;

 

 

 

  

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值