第三章 View事件体系(1)

本文为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

下面附上一个拖动的Demo
public 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
}
@Override
public 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;
            }
        };
    };
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336

推荐阅读更多精彩内容