美文网首页
第三章 View事件体系(1)

第三章 View事件体系(1)

作者: notrynobug | 来源:发表于2017-02-14 13:09 被阅读0次

    本文为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的层次结构图
    ](https://img.haomeiwen.com/i3986342/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;
                }
            };
        };
    

    相关文章

      网友评论

          本文标题:第三章 View事件体系(1)

          本文链接:https://www.haomeiwen.com/subject/kpsxwttx.html