第三章 View事件体系(1)

来源:互联网 发布:压实度软件 编辑:程序博客网 时间:2024/06/05 15:55

本文为Android开发艺术探索的笔记,仅供学习

首先View虽然不是四大组件,但是它的作用和重要性甚至比Receiver和Provider要重要的多。View是Android提供的控件的基类,然而这些控件远远不能满足我们日常开发的需求,所以我们需要工具需求去自定义新的空间。一个典型的场景就是滑动屏幕,很多情况下我们的应多都支持滑动操作,当处于不同级别的View都去相应用户的滑动操作的时候,就会带来一个问题。滑动冲突!想要处理这个问题我们就要去理解View的分发机制,如何去分发?如何去拦截?我们会在后续中去讲解。

1 View的基本知识

现在我们先来了解一下View的一些基本知识,以为后面更好的介绍做铺垫。View的位置参数,MotionEvent和TouchSlop对象,VelocityTracker和Scroll对象,以便大家更好的去了解去理解一些复杂的内容。

1.1 什么是View

View,在前面也说了是所有Android控件的基类,不管是Button,TextView还是复杂的RelativeLayout它们的基类都是View。除了View,还有ViewGroup,从名字上看ViewGroup里面会有很多个View,对ViewGroup就是一个控件组,它里面可以有很多个View,But ViewGroup也继承了View。关于ViewGroup我们可以这么理解,它里面可以有很多个View,这种关系就是一种View的树的结构。LinearLayout它既是一个View,也可以是一个ViewGroup。
我们图来表示一下,因为图是最直观的

View的树形结构图
再附上一张控件的结构层次表
TsetButton的层次结构图
](http://upload-images.jianshu.io/upload_images/3986342-c67678af1a40f908.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

1.2 View的位置参数

View的位置有四属性来决定,top left right bottom,top对应的是左上角的纵坐标,ldft对应的是左上角的横坐标,right对应的是右下角的横坐标,bottom对应的是右下角的纵坐标。(注意这些坐标都是相对与父控件的。)还是如此我们来花个图来解释一下吧

View的位置坐标与父控件的关系

从上图,我们可以得到View的高是bottom-top View的宽是right-left,那么你们会问如何获取到这四个属性的值呢,其实只要通过getTop getLeft 就可以获取相应的值。现在我们再提四个参数,X,Y,translationX,translationiY。X,Y是View左上角的左上角的坐标,而translationX和translationY,则表示可偏移量,这几个参数相当于父控件的坐标,而且translationX和translationY的初始值都是0,同样View也为他们提供了Get和Set的方法。

这几个参数的关系式 X = left + translationX Y = top + translationY

需要注意的是View在平移的时候,top和left表示的是原始左上角的位置,其值并不会该表,此时发送改变的是X,Y,translationX,translationY.

1.3 MotionEvent 和 TouchSlop

1.MotionEvent

在手指接触屏幕后发生的一系列事件,典型的有以下几种
- ACTION_DOWN——手指刚接触屏幕的时候
- ACTION_MOVE——-手指在屏幕上移动的时候
- ACTION_UP——-手指离开屏幕的时候

正常情况下,一次手指接触屏幕会出发两种情况
1. 第一种,DOWN–>UP 当点击屏幕马上离开
2. 第二种,DOWN–>MOVE–>…–>MOVE–>UP 当点击屏幕并且在屏幕上移动在离开
3. 第三种,DOWN–>MOVE–>…–>MOVE 就是点击屏幕,并且移动,移动到屏幕外面

上述三种是典型的事件顺序,同时我们可以通过MotionEvent去获取点击时间发送的X,Y的坐标。系统提供了两组方法,getX\getY, getRawX\getRawY,其中第一组是用来返回当前View的左上角的x y坐标,第二组方法是用来返回手机屏幕左上角的x y坐标。那么我们还是看图说话吧
getX\getY, getRawX\getRawY的图解

2 TouchSlop

TouchSlop就是系统所能识别的最小滑动距离,也就是说当用于滑动的距离小于该值则视为无效滑动。这是一个常量,不同的设备是不一样的。我们可以通过ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop(); 来获取最小滑动距离。为了让大家更好的理解附上源码
可以看到最小识别的距离为8dp

1.4 VelocityTracker和Scroll

1 VelocityTracker

VelocityTracker通过跟踪一连串事件实时计算出当前的速度,通过它我们可以得数水平滑动和竖直滑动的速率,一般作用在onTouchEvent方法中,主要用到下面几个方法
addMovement(MotionEvent)函数将Motion event加入到VelocityTracker类实例中
getXVelocity() 或getXVelocity()获得横向和竖向的速率到速率时,computeCurrentVelocity(int)来初始化速率的单位 。
VelocityTracker.obtain();获得VelocityTracker类实例
话不多说直接上代码。

onTouchEvent(MotionEvent ev){    if (mVelocityTracker == null) {             mVelocityTracker = VelocityTracker.obtain();//获得VelocityTracker类实例     }     mVelocityTracker.addMovement(ev);//将事件加入到VelocityTracker类实例中     //判断当ev事件是MotionEvent.ACTION_UP时:计算速率     // 1000 provides pixels per second     velocityTracker.computeCurrentVelocity(1, (float)0.01); //设置maxVelocity值为0.1时,速率大于0.01时,显示的速率都是0.01,速率小于0.01时,显示正常     Log.i("test","velocityTraker"+velocityTracker.getXVelocity());                         velocityTracker.computeCurrentVelocity(1000); //设置units的值为1000,意思为一秒时间内运动了多少个像素     Log.i("test","velocityTraker"+velocityTracker.getXVelocity()); }

2 Scroll

弹性滑动对象,用于实现View的弹性滑动。我们知道,当使用View的scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成,没有过渡效果的滑动用户体验不好。这个时候就需要使用Scroller来实现有过渡效果的滑动,大致实现过程后面会详细介绍,下面就附上实现代码。

        Scroller scroller;        scroller = new Scroller(context);         //调用此方法滚动到目标位置    public void smoothScrollTo(int fx, int fy, boolean back) {        int dx = fx;        int dy = fy;        smoothScrollBy(dx, dy);    }    //调用此方法设置滚动的相对偏移    public void smoothScrollBy(int dx, int dy) {        //设置scroller的滚动偏移量            scroller.startScroll(scroller.getFinalX(), scroller.getFinalY(), dx, dy);            invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果}    }    @Override    public void computeScroll() {        //先判断scroller滚动是否完成        if (scroller.computeScrollOffset()) {            //这里调用View的scrollTo()完成实际的滚动            scrollTo(scroller.getCurrX(), scroller.getCurrY());            //必须调用该方法,否则不一定能看到滚动效果            postInvalidate();        }        super.computeScroll();    }

2 View的滑动

在View的事件体系(1)中,我们已经了解到View的基本知识,这一节要来讲解很重要的东西就是View的滑动。在Android的设备上,滑动可以说以一种标配,不管是下拉刷新还是什么,他们的基础都是滑动。不管是任何酷炫的滑动效果,归根结底他们都是由不同的滑动和一些动画组成。所以我们还有必要去了解滑动的基础,接下来我们来了解三种实现滑动的方法。
1.View通过自生的scrollTo/scrollBy来实现滑动
2.通过动画来给View添加平移的效果来实现滑动
3.改变View的LayoutParams使得View重新布局来实现滑动。

2.1使用scrollTo/scrollBy

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();            }        }    }   public void scrollBy(int x, int y) {        scrollTo(mScrollX + x, mScrollY + y);    }

从源码上可以看出,scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo是实现了基于所传参数的绝对滑动。利用这两个方法我们就可以实现View的滑动,但是我们要明白滑动过程中View的两个内部属性的作用mScrollX mScrollY,这两个参数我们可以通过getScrollX getScrollY去获取。mScrollX的总值等于View内容原始左边缘到View内容现在左边缘的水平方向的距离mScrollY的总值等于View内容原始上边缘到View内容现在上边缘的水平方向的距离。记住一句话上正下负右正左负,意思就是内容的上边缘在View的上边缘的上面,mScrollY为正,其他同理,给大家一个图便于理解


切记,再怎么滑动不能将View的位置进行改变,只能改变View内容的位置,比如TextView改变里面的文字

在 使用 getScrollY() 方法的时候,就是 getScrollY()的值 一直是 1.0
解决:通过查看 getScrollY() 方法 ,发现它有两个 返回值 一个 int , 一个 float , 后 将值 赋值给 int 类型后,就可以使用了;而直接 相加的为 float 类型;

2.2 使用动画

通过动画我们可以让View进行平移,而平移也是一种滑动。我们可以使用View动画也可以使用属性动画。

//View动画<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://schemas.android.com/apk/res/android"    android:duration="2000"    android:startOffset="1000"    android:fillAfter="true">    <scale        android:fromXScale="0.0"        android:toXScale="1.4"        android:fromYScale="0.0"        android:toYScale="1.4"        android:pivotX="50"        android:pivotY="50"        android:duration="700" />    <alpha        android:fromAlpha="0.0"        android:toAlpha="1.0" /></set>       Animation animation = AnimationUtils.loadAnimation(this, R.anim.demo);       tv1.startAnimation(animation);//属性动画        ObjectAnimator.ofFloat(tv1,"translationY",150).start();        ValueAnimator animator = ObjectAnimator.ofInt(tv1, "backgroundColor", 0xFFFF8080, 0xFF8080FF);        animator.setDuration(3000);        animator.setEvaluator(new ArgbEvaluator());        animator.setRepeatCount(5);        animator.setRepeatMode(ValueAnimator.REVERSE);        animator.start();

切记,我们对通过动画对View的移动其实是对View的影像的移动,若我们不把fillAfter设为true的话,移动完后又会回到起点,若为true则会保留不动。但我们也View设置一个点击事件的时候,就要区分动画的类型,若是View动画则点击移动后的View却触发不了点击事件,若是属性动画则点击移动后的View却触发点击事件。针对View动画的解决方案,我们需要在移动后的位置再建立一个通向的View。

2.3 改变布局参数

我们可以改变布局参数LayoutParams ,让我们想让一个Button向右移动100dp,那么我们只要设施其marginleft就可以了,还可以这这个Button设置一个宽度为0的View,改变其的宽度为,那个Button就会自动被挤到右边。

2.4各种滑动方式的对比

  • scrollTo/scrollBy,这种方法其实是View提供的原生的滑动方式,他可以实现View的滑动也不影响其内部的点击事件,缺点就是只能滑动View的内容

  • 动画滑动,如果是android3.0以上的话可以采用属性动画,这种方法并没有什么缺点,如果是3.0一下的话就绝不能改变View本生的属性。实际上如果动画不需要响应用户的交互,那么这种滑动方式是比较合适的。但是动画有一个明显的有点,就是一些复杂的效果必须通过动画来实现。

  • 改变布局的方式,除了使用起来麻烦以外就没有什么明显的缺点了,非常适合对象具有交互的View,因为这些View是要与用户交互,直接通过动画会有问题。

总结一下就是
scrollTo/scrollBy:操作简单,适合对View内容的滑动
动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于有交互的View

下面附上一个拖动的Demopublic class Move_textview extends TextView {    String TAG = "move";    public Move_textview(Context context) {        this(context, null);    }    private int mScaledTouchSlop;//可识别的最小滑动距离    // 分别记录上次滑动的坐标    private int mLastX = 0;    private int mLastY = 0;    public Move_textview(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public Move_textview(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        init();    }    private void init() {        mScaledTouchSlop = ViewConfiguration.get(getContext())                .getScaledTouchSlop();        Log.d(TAG, "sts:" + mScaledTouchSlop);    }    @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 move_x = x - mLastX;                int move_y = y - mLastY;                int translationX = (int) ViewHelper.getTranslationX(this) + move_x;                int translationY = (int)ViewHelper.getTranslationY(this) + move_y;                ViewHelper.setTranslationX(this, translationX);                ViewHelper.setTranslationY(this, translationY);            }            case MotionEvent.ACTION_UP: {                break;            }            default:                break;        }        mLastX = x;        mLastY = y;        return true;    }}

3.弹性滑动

知道了View的滑动,但是这样的滑动有时候是比较生硬的,用户体验太差了,所以我们要去了解弹性滑动,就是将一次滑动分成若干个小滑动。主要是通过Scroller,handler #postDelaked,Thread#sleep

3.1Scroller的使用

我们先来看看Scroller的最基本的使用Scroller scroller = new Scroller(context);private void smoothScrollTo(int destX, int destY) {//自己写的方法    int scrollX = getScrollX();//View的内容的左边缘到View左边缘的距离    int deltaX = destX + scrollX;//加上要移动的距离后的位置    scroller.startScroll(scrollX, 0, deltaX, 0);    invalidate();注解1}@Overridepublic void computeScroll() {    if (scroller.computeScrollOffset()) {        scrollTo(scroller.getCurrX(), scroller.getCurrY());        postInvalidate();    }}

我们可以看到显示构造了一个Scroller对象,在调用它的startScroll方法,其实Scroller内部什么都没做就是用来保存几个参数

public void startScroll(int startX, int startY, int dx, int dy, int duration) {    mMode = SCROLL_MODE;    mFinished = false;    mDuration = duration;    mStartTime = AnimationUtils.currentAnimationTimeMillis();    mStartX = startX;    mStartY = startY;    mFinalX = startX + dx;    mFinalY = startY + dy;    mDeltaX = dx;    mDeltaY = dy;    mDurationReciprocal = 1.0f / (float) mDuration;}

startX,startY是起始位置,dx dy是要滑动的距离,duration就是在规定的时间内滑动
那么Scroller到底是怎么进行弹性滑动的呢?注解1invalidate()

大致的流程是这样子的,invalidate方法会导致View去重绘,ondraw是绘制的方法,改方法又会去调用ComputeScroll方法,此时我们需要对ComputeScroll进行重写,在里面去判断弹性滑动是否结束,没结束就再获取Scroller当前的位置,在去进行第二次绘制,直至弹性滑动结束。

那我们来看看Scroller的computeScrolloffset方法

public boolean computeScrollOffset() {    if (mFinished) {        return false;    }    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);    if (timePassed < mDuration) {        switch (mMode) {        case SCROLL_MODE:            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);            mCurrX = mStartX + Math.round(x * mDeltaX);            mCurrY = mStartY + Math.round(x * mDeltaY);            break;        case FLING_MODE:            final float t = (float) timePassed / mDuration;            final int index = (int) (NB_SAMPLES * t);            float distanceCoef = 1.f;            float velocityCoef = 0.f;            if (index < NB_SAMPLES) {                final float t_inf = (float) index / NB_SAMPLES;                final float t_sup = (float) (index + 1) / NB_SAMPLES;                final float d_inf = SPLINE_POSITION[index];                final float d_sup = SPLINE_POSITION[index + 1];                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);                distanceCoef = d_inf + (t - t_inf) * velocityCoef;            }            mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;                  mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));            // Pin to mMinX <= mCurrX <= mMaxX            mCurrX = Math.min(mCurrX, mMaxX);            mCurrX = Math.max(mCurrX, mMinX);            mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));            // Pin to mMinY <= mCurrY <= mMaxY            mCurrY = Math.min(mCurrY, mMaxY);            mCurrY = Math.max(mCurrY, mMinY);            if (mCurrX == mFinalX && mCurrY == mFinalY) {                mFinished = true;            }            break;        }    }    else {        mCurrX = mFinalX;        mCurrY = mFinalY;        mFinished = true;    }    return true;}

该方法就是根据流逝时间的百分比会算去scrollX和scrollY,当返回true的时候表示弹性滑动还没结束,false就表示弹性滑动结束
那么现在来总结一下,scroller本生是不能滑动的,它需要配合computeScroll来实现弹性滑动,它会不断的去重绘View,每次重绘是有时间间隔的,通过这个时间间隔和Scroller的computeScrolloffset方法来返回相应的位置,通过View自身的scrollTo和返回来的位置去移动View。这个思想很巧妙,竟然连计时器都没有用到。
注意:滑动的还是View的内容而不是View

3.2 通过动画

动画本来就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果,比如以下代码可以让一个Button实现一个宽度的变化动画。

 private static class View_button {        View view;        public View_button(View view) {            this.view = view;        }        public int getWidth() {            return view.getLayoutParams().width;        }        public void setWidth(int w) {            view.getLayoutParams().width = w;            view.requestLayout();        }    }       ObjectAnimator.ofInt(new View_button(tv1),"width",500).setDuration(3000).start();

至于属性动画的详情,在后续中会详细介绍

3.3 使用延时策略

通过Handler里去改变控件的位置。

  private Handler mHandler = new Handler() {        public void handleMessage(Message msg) {            switch (msg.what) {            case MESSAGE_SCROLL_TO: {                mCount++;                if (mCount <= FRAME_COUNT) {                    float fraction = mCount / (float) FRAME_COUNT;                    int scrollX = (int) (fraction * 100);                    mButton1.scrollTo(scrollX, 0);     mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);                }                break;            }            default:                break;            }        };    };
0 0