美文网首页
Android View的事件体系(上)

Android View的事件体系(上)

作者: kongjn | 来源:发表于2017-05-03 16:56 被阅读0次

    本章将介绍Android中十分重要的一个概念View

    文章目录:

    View.png

    3.1 View的基础知识

    1. 什么是View :

    内容 含义
    View 一个单一的控件,如Button、TextView
    ViewGroup 一个控件组,如RelativeLayout、LinearLayout

    View的视图结构:


    View的视图结构
    • View的位置参数:
      View的宽高和坐标的关系

    View的宽高和坐标的关系:

    width = right - left
    height = bottom - top

    位置获取方式

    View的位置是通过view.getxxx()函数进行获取:(以Top为例)
    // 获取Top位置

    public final int getTop() {  
        return mTop;  
    }  
    
    // 其余如下:
      getLeft();      //获取子View左上角距父View左侧的距离
      getBottom();    //获取子View右下角距父View顶部的距离
      getRight();     //获取子View右下角距父View左侧的距离
    

    2. MotionEvent和TouchSlop

    • MotionEvent
      在手指接触屏幕后所产生的事件:
    常量 含义
    ACTION_DOWN 手指接触屏幕(按下)
    ACTION_MOVE 手指在屏幕上移动(滑动)
    ACTION_UP 手指从屏幕上松开的一瞬间(离开)

    上述三种情况是典型的时间序列,同时通过 MontionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标。系统提供两组方法:getX / getY 和 getRawX / getRawY。

    get() 和 getRaw() 的区别
    具体代码:
    @Override
        public boolean onTouchEvent(MotionEvent event) {
    
            //get() :触摸点相对于其所在组件坐标系的坐标
            event.getX();
            event.getY();
    
            //getRaw() :触摸点相对于屏幕默认坐标系的坐标
            event.getRawX();
            event.getRawY();
            return super.onTouchEvent(event);
        }
    
    • TouchSlop
      TouchSlop 是系统所能识别出的被认为是滑动的最小距离。换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。原因很简单滑动的距离太短,系统不认为是滑动。这是一个常量和设备有关,不同的设备上这个值可能会不同。获取方式:
         ViewConfiguration.get(this).getScaledTouchSlop(); 
    

    4. VelocityTracher、GestureDetector和Scroller

    • VelocityTracher
      速度追踪,用于追踪手指在滑动过程中的速度,包括水平方向和竖直方向的速度。
     @Override
        public boolean onTouchEvent(MotionEvent event) {
    
            VelocityTracker velocityTracker = VelocityTracker.obtain();
            velocityTracker.addMovement(event);
            velocityTracker.computeCurrentVelocity(1000);//设置时间间隔为1000
            int xVelocity = (int) velocityTracker.getXVelocity();
            int yVelocity = (int) velocityTracker.getYVelocity();
            Log.e("event", "xVelocity ;" + xVelocity + "  yVelocity ;" + yVelocity);
            //当我们结束的时候,需要调用 clear 方法来重置并且回收内存。
            velocityTracker.clear();
            velocityTracker.recycle();
            return super.onTouchEvent(event);
        }
    

    这里需要注意,第一点,获取速度之前必须先计算速度,即 getXVelocity() 和 getYVelocity() 必须在 computeCurrentVelocity 的后面,第二点,这里的速度是指一段时间内手指所划过的像素数,比如将时间间隔设置有 1000ms 时,在1s 内手指在水平方向划过100像素,那么水平速度就是 100 ,当手指从右向左滑动时,速度为负数,公式:
    <div align = center>速度 = (终点位置 - 起始位置)/ 时间段</div>
    不要管时间间隔是传统含义,这里只要根据公式来计算即可。

    • GestureDetector
      手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。首先创建一个MianActivity 实现GestureDetector.OnGestureListener ,OnDoubleTapListener接口 ,
    public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener,
           GestureDetector.OnDoubleTapListener{
    private GestureDetector mGestureDetector;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mGestureDetector = new GestureDetector(this,this);
            //解决长按屏幕后无法拖动的现象
            mGestureDetector.setIsLongpressEnabled(false);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {return mGestureDetector.onTouchEvent(event);}
        //手指轻轻触摸屏幕的一瞬间,由一个 ACTION_DOWN 触发。
        @Override
        public boolean onDown(MotionEvent e) { return false;}
        //手指轻轻触摸屏幕,尚未松开或拖动,由一个 ACTION_DOWN 触发。*注意和 onDown 的区别,它强调的是没有松开或者拖动的状态*
        @Override
        public void onShowPress(MotionEvent e) {}
        //手指(轻轻触摸屏幕后)松开,伴随着一个 MontionEvent ACTION_UP 而触发,这是单击行为。
        @Override
        public boolean onSingleTapUp(MotionEvent e) {return false; }
        //手指按下屏幕并拖动,由 1 个 ACTION_DOWN,多个 ACTION_MOVE 触发,这是拖动行为。
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {return false;}
        //用户长久地按着屏幕不放,即长按。
        @Override
        public void onLongPress(MotionEvent e) { }
        //用户按下触摸屏、快速滑动后松开,由 1 个 ACTION_DOWN 、多个 ACTION_MOVE 和 ACTION_UP 触发,这是快速滑动行为
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
        //双击,由 2 次联系的单击组成,它不可能和 onSingleTapConfirmed 共存。
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {return false; }
        //严格的单击行为
        //*注意它和 onSingleTapUp的区别,如果触发了onSingleTapConfirmed,那么后面不可能再紧跟着另一个单击行为,即这只可能是单击,而不可能是双击中的一次单击*
        @Override
        public boolean onDoubleTap(MotionEvent e) {return false;}
        //表示发生了双击行为,在双击的期间,ACTION_DOWN 、ACTION_MOVE 、ACTION_UP 都会触发此回调。 
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {return false;}
    }
    

    这点可能会奇怪 setIsLongpressEnabled(false)参数要为false,经过我测试,setIsLongpressEnabled(true)的时候 ,长按屏幕触发 onLongPress 会直接拦截掉其他的触摸
    down、move、up 事件,为 false 的时候,onLongPress 则不会触发,其他正常。

    方法很多,但是并不是所有的方法都会被时常用到,在日常开发中,比较常用的onSingleTapUp(单击),onFling(快速滑动),onScroll(推动),onLongPress(长按)和onDoubleTap(双击),另外要说明的是,在实际开发中可以不使用 GestureDetector,完全可以自己在view中的onTouchEvent中去实现。

    • Scroller
      弹性的滑动对象,用于实现View的弹性滑动。

    3.2 View 的滑动

    常见的的三种 View 滑动实现方式:

    • 通过 View 本身提供的 scrollTo / scrollBy 方法来实现滑动
    • 通过动画给 View 施加平移想过
    • 通过改变 View 的LayoutParams 使得 View 重新布局从而实现滑动。
    3.2.1 使用 scrollTo/scrollBy

      为了实现 View 的滑动, View 提供了 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 单位为像素。

    变换规律示意图(单位:像素)
    完整代码地址
    列出部分代码:
    public class ScollerView extends View implements View.OnClickListener {
    ...
     public ScollerView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mScroller = new Scroller(context);
            this.setOnClickListener(this);
    }
    private void smoothScrollTo(int destX, int destY) {
            int scrollX = getScrollX();
            int delta = destX - scrollX;
            int scrollY = getScrollY();
            int deltY = scrollY + destY;
            //100ms 内滑向 destX ,效果就是慢慢滑动
            mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
            invalidate();
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()) {
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                postInvalidate();
            }
        }
     ...
    }
    

    3.2.2 使用动画

    使用动画来移动 View ,主要操作 View 的 translatuonX 和 translationY 属性,既可以采用传统的 View 动画,也可以采用属性动画。书中的3.0兼容就不介绍了,现在也基本用不到。
      采用 View 动画的代码,如下所示,此动画可以在100ms 内将一个 View 从原来位置像右下角移动 100 个像素。

    GIF.gif
    在 res 目录中创建一个 anim 目录,在新建一个 set :
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true" android:zAdjustment="normal"
        >
        <translate
            android:duration="100"
            android:fromXDelta="0"
            android:fromYDelta="0"
            android:interpolator="@android:anim/linear_interpolator"
            android:toXDelta="100"
            android:toYDelta="100"/>
    </set>
    

    在 Activity 中使用改方法,就可以移动mButton

      ObjectAnimator.ofFloat(mButton,"translationX",0,100).setDuration(2000).start();
    

    View 动画是对 View 的影像做操作,它并不能真正改变 View 的位置参数,包括宽高。并且如果希望动画后的状态得以保存还必须将 fillAfter 设为 true,为 false 时动画结束后 View 会恢复原状。

    3.2.3 改变布局参数

    第三种实现 View 滑动的方法,那就是改变布局参数,即改变 LayoutParams。改变 View 的位置,将LayoutParams 中的位置关系设置一下即可。实现方法很简单:

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

    3.2.4 各种滑动方式的对比

    • scrollTo/scrollBy:操作简单,适合对 View 内容的滑动;
    • 动画:操作简单,主要适用于没有交互的 View 和实现复杂的动画效果;
    • 改变布局参数:操作稍微复杂,适用于有交互的 View;
    GIF.gif
    GIF.gif

    自定义一个 View 继承 Button:

    ...
    @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;
                    int translationX = (int) (getTranslationX() + deltaX);
                    int translationY = (int) (getTranslationY() + deltaY);
                    setTranslationX(translationX);
                    setTranslationY(translationY);
                    break;
                case MotionEvent.ACTION_UP:
                    break;
                default:
                    break;
            }
    ![GIF.gif](https://img.haomeiwen.com/i5531940/42d068be5205a447.gif?imageMogr2/auto-orient/strip)
    
            mLastX = x;
            mLastY = y;
            return true;
        }
    

    通过上述代码可以看出,这一全屏滑动的效果实现起来相当简单。首先我们通过 getRawX 和 getRawY 方法来获取手指当前的坐标,注意这里不能使用 getX 和 getY 方法,getRawX 是获取全屏坐标,getX 是获取 View 的相对坐标(前面有讲到)。

    3.3 弹性滑动

    知道了 View 的滑动,还要知道如何实现 View 的弹性滑动。

    3.3.1 使用 Scroller

    之前使用过Scroller,现在来分析一下它的源码,探究一下为什么它能实现 View 的弹性滑动。

    private void smoothScrollTo(int destX, int destY) {
            int scrollX = getScrollX();
            int delta = destX - scrollX;
            int scrollY = getScrollY();
            int deltY = scrollY + destY;
            //100ms 内滑向 destX ,效果就是慢慢滑动
            mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
            invalidate();
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()) {
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                postInvalidate();
            }
        }
    

    上面是 Scroller 的典型的使用方法,这里先描述它的工作原理:当我们构造一个Scroller 对象并且调用它的 startScroll 方法时,Scroller 内部其实上面也没做,它只是保存了,我们传递的几个参数。这几个参数从 startScroll 的原型上就可以看出来,如下所示。

     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 表示的是滑动时间,即整个滑动过程完成所需要的时间,注意这里的滑动是指 View 滑动内容的滑动而非 View 本身的位置的改变,可以看到仅仅是调用 startScroll 方法是无法让 View 滑动的,因为它内部并没有做滑动相关的事,那么 Scroller 到底是如何让 View 弹性滑动的呢 ?答案就是 startScroll 方法下面的 invalidate 方法,虽然有点不可思议,但是的确是这样的。invalidate 方法会导致 View 重绘, 在 View 的 draw 方法中又会去调用 computeScroll 方法, computeScroll 方法在View 中是一个空实现,因此需要我们自己去实现,方面的代码已经实现了 computeScroll 方法。正是因为这个 computeScroll 方法,View 才能实现弹性滑动。这看起来还是很抽象,其实是这样的:当 View 重绘后在 draw 方法中调用 computeScroll ,而 computeScroll 又回去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 scrollTo 方法实现滑动;接着又调用 postInvalidate 方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致 computeScroll 方法被调用了;然后继续向 Scroller 获取当前的 scrollX 和 scrollY。并通过 scrollTo 方法滑动到新的位置,如此反复,知道整个滑动的过程结束。
      我们再看一下 Scroller 的 computeScrollOffset 方法的实现,如下所示:

    public boolean computeScrollOffset() {
            ...
    
            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;
                ...
            }
            return true;
        }
    

    是不是突然就明白了?这个方法会根据时间的流逝来计算当前的scrollX和Y的值,计算方法也很简单,大意就是根据时间流逝的百分比来计算scrollX和Y改变的百分比并计算出当前的值,这个过程相当于动画的插值器的概念,这里我们先不去深究这个具体的过程,这个方法的返回值也很重要,他返回true表示滑动还未结束,false表示结束,因此这个方法返回true的时候,我们继续让View滑动

    通过上面的分析,我相信大家应该都已经明白了Scroller的滑动原理了,这里做一个概括,他本身并不会滑动,需要配合computeScroll方法才能完成弹性滑动的效果,不断的让View重绘,而每次都有一些时间间隔,通过这个事件间隔就能得到他的滑动位置,这样就可以用ScrollTo方法来完成View的滑动了,就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动形成了弹性滑动,整个过程他对于View没有丝毫的引用,甚至在他内部连计时器都没有。

    3.3.2 通过动画

    一位大神的 View 系列 传送门:http://blog.csdn.net/harvic880925/article/details/50995268
      动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果。

    GIF.gif

    代码:

    ValueAnimator animator = ValueAnimator.ofInt(0, 10, 60, 200).setDuration(2000);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float fraction = animation.getAnimatedFraction();
                    int animatedValue = (int) animation.getAnimatedValue();
                    mButton.scrollTo(animatedValue, 0);//根据动画本身移动200px
                    mButton2.scrollTo((int) (fraction * 100), 0);//自定义移动100px
                }
            });
            animator.start();
    

    在上述代码中,mButton 动画移动距离 200,我们的动画本质上没有作用于任何对象上,只是在 2000ms 内完成了整个动画过程。利用这一特性,我们就可以在动画的每一帧到来时获取动画完成的比例 fraction ,然后再根据这个比例计算出当前 View 所要滑动的距离。mButton2 通过改变百分比 fraction 来完成 View 的滑动,采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在 onAnimationUpdate 方法中加上我们的其他操作。

    3.3.3 使用延时策略

    另一种实现弹性滑动的方法,延时策略。核心思想是使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。

    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
    
    private static final int MESSAGE_SCROLL_TO = 1;
        private static final int FRAME_COUNT = 30;
        private static final int DELAYED_TIME = 30;
        private int mCount = 0;
        @SuppressLint("HandlerLeak")
        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);
                            mButton.scrollTo(scrollX, 0);
                            mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                        }
                        break;
                    }
                    default:
                        break;
                }
            };
        };
    

    相关文章

      网友评论

          本文标题:Android View的事件体系(上)

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