Android中View的滑动机制分析

来源:互联网 发布:软件测试就业前景 编辑:程序博客网 时间:2024/04/29 09:38

1、View基础知识

1.1 View介绍

View是Android中所有控件的基类,是一种界面层控件的抽象,它代表了一个控件。ViewGroup,控件组,内部包含了多个控件,即一组View。ViewGroup继承View,意味着View可以是单个控件,也可以是由多个控件组成的一组控件,形成了View的树结构。

举例说明View和ViewGroup的关系。Button是个View,LinearLayout是一个View,而且还是一个ViewGroup,而ViewGroup内部可以是有子View的,这个子View同样还可以是ViewGroup,以此类推。

View的层级结构:
这里写图片描述

1.2 View的位置参数

View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top,left,right,bottom。从android3.0开始 View增加了额外的几个参数:x,y,translationX,和translationY。

  • top:左上角纵坐标
  • left:左上角横坐标
  • right:右下角横坐标
  • bottom:右下角纵坐标

    这里写图片描述

View的宽高和坐标的关系:width = right - leftheight = bottom - top
  • x和y:View左上角的坐标
  • translationX和translationY:View左上角相对于父容器的偏移量
参数换算关系:x = left + translationXy = top + translationY

1.3 MotionEvent和TouchSlop

(1)MotionEvent

在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

  • ACTION_DOWN : 手指刚接触屏幕
  • ACTION_MOVE : 手指在屏幕上移动
  • ACTION_UP : 手指离开屏幕的一瞬间

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,例如:

  • 点击屏幕后离开,事件序列为DOWN->UP
  • 点击屏幕滑动一会松开,事件序列为DOWN->MOVE->…->MOVE->UP

通过MotionEvent的对象我们可以得到点击事件发生的x和y坐标。因此,系统系统提供了两组方法:getX/getY和getRawX/getRawY。它们的区别很简单,getX和getY返回的是相当于当前View左上角的x和y坐标,而getRawX和getRawY返回的是相对于手机屏幕左上角的x和y坐标。

(2)TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离。当我们在处理滑动时,就可以利用这个常量做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为未滑动。

这个值是一个常量与设备有关,获取这个常量方法:

ViewConfiguration.get(getContext()).getScaledTouchSlop()

1.4 VelocityTracker、GestureDetector和Scroller

(1)VelocityTracker速度追踪

VelocityTracker是速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。它的使用很简单,首先,在View的onTouchEvent方法中追踪当前单击事件的速度:

VelocityTracker vt=VelocityTracker.obtain();vt.addMovement(event);

接着就想知道当前的滑动速度时,就可以采用这个方式来获得速度:

//计算速度vt.computeurrentVelocity(100);//100表示时间间隔为100ms//获取速度Int x=(int)vt.getXVelovity();Int y=(int)vt.getYVelovity();

当不需要使用它的时候,用clear方法来重置回收内存:

vt.clear();vt.recycle();

速度 = (终点位置 - 起始位置)/ 时间段

(2)GestureDetector手势检测

手势检测,用于辅助检测用户的单击,滑动,长按,双击等行为。

首先,我们创建一个GestureDetector 对象并实现onGestureListener接口,根据需要我们还可以实现onDoubleTapListener从而能够监听双击行为:

GestureDetector mGestureDetector =new GestureDetector(this);//解决长按屏幕无法拖动的现象mGestureDetector.setIsLongPressEnable(false);

接着实现目标View的onTouchEvent方法,在待监听的View的onTouchEvent方法中添加如下实现:

Boolean consume=mGestureDetector.onTouchEvent(event);

做完上两步,我们就可以有选择地实现OnGestureListener和OnDoubleTapListenter中的方法了。另外,在实际开发中,我们可以不使用GestureDetector,完全可以自己在View的onTouchEvent方法中实现所需的监听,当需要监听双击这种行为时,再使用GestureDetector.

(2)Scroller弹性滑动现象

弹性滑动对象,用于实现View的弹性滑动。我们知道,当使用View的scrollTo/scrollBy方法进行滑动时,其过程是可以瞬间完成的,这个没有过渡效果的滑动用户体验不好。这个时候,我们就可以使用Scroller来实现由过渡效果的滑动,其过程不是瞬间完成的,而是在一定时间间隔内完成的。Scroller配合view的computeScroll就能完成这个功能。它的典型代码固定如下:

Scroller scroller =new Scroller(mContext);//缓慢滑动到制定位置private void smoothScrollTo(int destX, int destY) {    int scrollX=getScrollX();    int delta = destX - scrollX;    //1000ms内滑向destX,效果就是慢慢滑动    mSroller.startScroll(scrollX, 0, delta, 0, 1000);    invalidate();}@Overridepublic void computeScroll(){    if(mSroller.computeScrollOffset()) {        scrollTo(mScroller.getCurrX().mScroller.getCurrY());        postInvalidate();    }}

2、View的滑动

不管一些滑动效果多么绚丽,归根到底,他们都是由不同的滑动外加一些特效所组成的。因此。掌握滑动的方法是实现绚丽的自定义控件的基础。通过三种方式可以实现View的滑动: 第一种事通过View 本身提供的scrollTo/scrollBy 方法来实现滑动;第二种是通过动画给View施加平移来实现滑动;第三种是通过改变View的LayoutParams使得View重新布局从而实现滑动。

2.1 使用scrollTo/scrollBy

为了实现View的滑动,View提供了专门的方法来实现这个功能。那就是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方法,而scrollTo则实现了基于所传递参数的绝对滑动。利用scrollTo和scrollBy来实现View的滑动,这不困难,但我们要知道。滑动过程中,View内部的两个属性mScrollX和mScrollY的改变规则。这两个属性可以通过getScrollX和getScrollY方法分别得到。

在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向上的距离,而mScrollY的值,总是等于View上边缘和View内容上边缘的竖直方向的距离。scrollTo和scrollBy只能改变View的内容的位置,而不能改变View在布局中的位置。mScrollX和mScrollY的单位是像素,并且当View左边缘在View内容左边缘右边时,mScrollX为正值,反之为负值。当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。换句话说,如果从左向右滑动,那么mScrollX为负值,反之为正值。如果从上往下滑动,mScrollY为负值,反之为正值。
这里写图片描述

2.2 使用动画

使用动画来移动View,主要操作View的translationX和translationY属性,即可以使用传统的View动画来完成,也可以采用属性动画,如果采用属性动画的话,为了兼容3.0一下版本,需要采用开源动画库nineoldandroids。

使用传统的View动画,可以xml中的指定translate,平移动画。

采用属性动画,更简单了,比如:

ObjectAnimator.ofFloat(targetView,”translationX”,0,100).setDuration(100).start();

*注意
View动画是对View的影像的操作,它并不能真正改变View的位置包括宽/高,造成View无法响应点击事件,因为View的真身还在原来的位置,并且如果希望动画后的状态得以保留。还必须将fillAfter的属性设置为true,否则动画完成后,其动画效果就会消失。
属性动画解决兼容问题,不存在此问题。

2.3 改变布局参数

改变布局参数,即改变LayoutParams比较好理解,比如我们想把一个Button向右平移100px,我们主要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可。

MarginLayoutParams params = (MarginLayoutParams )mButton1.getLayoutParams();Params.width +=100;params.leftMargin +=100;mButton1.requestLayout();//或者 mButton1.setLayoutParams(params);

2.4 各种滑动方式的对比

  • scrollTo/scrollBy:操作简单。适合View滑动,滑动后支持响应事件。

  • 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果。推荐使用nineoldandroids兼容Android 3.0以下版本。

  • 改变布局参数:操作稍微复杂,适用于有交互的Veiw.

3、弹性滑动

如果View生硬的滑动过去,用户体验实在太差。因此,我们要实现渐进式滑动。渐进式滑动有一个共同思想:将依次大的滑动分成若干次小的滑动,并在一个时间段内完成。常用的弹性滑动方式有Scroller,使用动画,还有延时策略。

(1) 使用Scroller

Scroller方法已经分析过了,如果有兴趣可以分析它的源码。原理如下:

Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断的让View重绘,而每一次重绘滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View的当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。

(2) 通过动画

动画本身就是一种渐进过程,因此通过它来实现滑动天然就具有弹性效果,比如下面代码可以让一个View的内容在100ms内向左移动100ms.

ObjectAnimator.ofFloat(targetView,”translationX”,0,100).setDuration(100).start();

不过这里想说的并不是这个问题,我们可以利用动画特性来实现一些动画不能实现的效果。还拿scrollTo来说,我们也想模仿Scroller来实现View的弹性滑动。那么利用动画的特性。我们可以采用如下方式来实现。

final int startX=0;final int deltax=100;valueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);animator.addUpdateListener(new AnimatorUpdateListener(){@Overridepublic void onAnimatorUpdate(ValueAnimator animator){    float fraction=animator.getAnimatedFraction();    mButton1.scrollTo(startX+(int)(deltaX*fraction),0)    }});animator.start();

上述代码,我们的动画本质上没有作用于任何对象上,它只是在1000ms内完成了整个动画过程。利用这个特性,我们就可以在动画的每一帧到来时获取动画完成比例,然后再根据这个比例计算当前View的滑动距离。注意,这里的滑动特性针对的是View的内容而非View本身。它与Scroller比较类似,都是通过改变一根百分比配合scrollTo方法来完成View的滑动。需要说明一点,采用这种方法除了能够完成弹性滑动以外,还可以实现其动动画效果,我们可以在onAnimationUpdate方法中加上我们想要的操作。

(3) 使用延时策略

延时策略的核心思想是通过发送一系列延时消息,从而达到渐进式的效果,具体来说,可以使用Handler 或View的postDelayed方法,也可因使用线程的sleep方法。使用起来比较简单。

4、实战手滑动效果

(1)效果说明
这是一个自定义View,拖动它可以让它在整个屏幕上随意滑动。

(2)实现思路
实现思路较为简单,重写它的onTouchEvent方法并处理ACTION_MOVE事件,根据两次滑动之间的距离就可以实现它的滑动了。为了实现全屏滑动,采用动画实现。这个效果无法通过scrollTo实现。改变布局方式也是可以的。

(3)代码实现

package com.chunsoft.testviewevent.ui;import android.annotation.SuppressLint;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.ViewConfiguration;import com.nineoldandroids.view.ViewHelper;//继承TextViewpublic class TestButton extends TextView {    private static final String TAG = "TestButton";    private int mScaledTouchSlop;    // 分别记录上次滑动的坐标    private int mLastX = 0;    private int mLastY = 0;    public TestButton(Context context) {        this(context, null);    }    public TestButton(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public TestButton(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        init();    }    private void init() {    //获取滑动最小距离,超过这个常量则认为滑动了        mScaledTouchSlop = ViewConfiguration.get(getContext())                .getScaledTouchSlop();        Log.d(TAG, "sts:" + mScaledTouchSlop);    }    @SuppressLint("ClickableViewAccessibility")    @Override    public boolean onTouchEvent(MotionEvent event) {        int x = (int) event.getRawX();        int y = (int) event.getRawY();        switch (event.getAction()) {        case MotionEvent.ACTION_DOWN: {            break;        }        case MotionEvent.ACTION_MOVE: {            int deltaX = x - mLastX;            int deltaY = y - mLastY;            Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);            //这里使用了动画兼容库,nineoldandroids            int translationX = (int)ViewHelper.getTranslationX(this) + deltaX;            int translationY = (int)ViewHelper.getTranslationY(this) + deltaY;            ViewHelper.setTranslationX(this, translationX);            ViewHelper.setTranslationY(this, translationY);            break;        }        case MotionEvent.ACTION_UP: {            break;        }        default:            break;        }        mLastX = x;        mLastY = y;        return true;    }}

首先通过getRawX和getRawY方法获取手指当前的坐标。其次,获取两次活动之间的位移;再,有了位移通过nineoldandroids中的ViewHelper类的setTranslationX和setTranslationY方法实现移动,如果给View添加o nClick事件3.0以下版本无法在新位置响应用户的点击。

0 0
原创粉丝点击