美文网首页android技术专栏Android开发经验谈Android开发
【Android View事件(三)】Scroll类源码分析与应

【Android View事件(三)】Scroll类源码分析与应

作者: 大圣代 | 来源:发表于2017-10-29 15:25 被阅读24次

    【本文出自大圣代的技术专栏 http://blog.csdn.net/qq_23191031
    【禁止任何商业活动。转载烦请注明出处】

    学前准备

    详解Android控件体系与常用坐标系
    Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker

    前言

    在前面的几篇文章,我向大家介绍的都是单一View事件,从这篇文章开始,我将向大家介绍连续的事件 —— 滑动。滑动是移动端设备提供的重要功能,正是由于强大的滑动事件让我们小巧的屏幕可以展现无限的数据。而滑动事件冲突却常常困扰着广大开发者。孙子云:知己知彼,百战不殆。想更好的协调滑动事件,不知道其中原理的确困难重重。当你学习本篇文章之后你会发现其实Scroll很简单,你只是被各种文章与图书弄糊涂了。

    在真正讲解之前,我们需要掌握Android坐标系与触控事件相关知识,对此不太明确的同学请参见上文的 学前准备

    View滑动产生的原理

    从原理上讲View滑动的本质就是随着手指的运动不断地改变坐标。当触摸事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后的触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标,不断的重复这样的过程,从而实现滑动过程。

    1 scrollTo 与 scrollBy

    说到Scroll就不得不提到scrollTo()与scrollBy()这两个方法。

    1.1 scrollTo

    首先我们要知道Android每一个控件都有滚动条,只不过系统对我们隐藏了,所以我们看不见。
    对于控件来说它的大小是有限的,(例如我们指定了大小、屏幕尺寸的束缚等),系统在绘制图像的时候只会在这个有限的控件内绘制,但是内容(content)的载体Canvas在本质上是无限的,例如我们的开篇图片,控件仿佛就是一个窗口我们只能通过它看到这块画布。

        /**
         * 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; // 已经滚动到的X
                int oldY = mScrollY; //已经滚动到的Y
                mScrollX = x;
                mScrollY = y;
                invalidateParentCaches();
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);//回调方法,通知状态改变
                if (!awakenScrollBars()) {
                    postInvalidateOnAnimation(); //重新绘制
                }
            }
        }
    

    通过注释Set the scrolled position of your view我们可以清楚的得知 scrollTo(x,y)的作用就是将View滚动到(x,y)这个点,注意是滚动(scroll本意滚动,滑动是translate)。

    在初始时 mScrollX 与mScrollY均为0,表示着View中展示的是从画布左上角开始的内容(如图 1),当调用scrollTo(100,100)时相当于将View的坐标原点滚动到(100,100)这个位置,展示画布上从(100,100)开始的内容(如图2),但是事实上View是静止不动的,所以最终的效果是View的内容平移了(-100,-100)的偏移量(如图3)

    image.png

    1.2 scrollBy

    /** 
        * 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);  
       }  
    

    学习scrollTo在学习scrollBy就简单了,通过源码可以看到它里面调用了ScrollTo(),传入的参数是mScrollX+x,也就是说这次x是一个增量,所以scrollBy实现的效果就是,在当前位置上,再偏移x距离
    这是ScrollTo()和ScrollBy()的重要区别。

    1.3 小结:

    1. scrollTo与scrollBy都会另View立即重绘,所以移动是瞬间发生的
    2. scrollTo(x,y):指哪打哪,效果为View的左上角滚动到(x,y)位置,但由于View相对与父View是静止的所以最终转换为相对的View的内容滑动到(-x,-y)的位置。
    3. scrollBy(x,y): 此时的x,y为偏移量,既在原有的基础上再次滚动
    4. scrollTo与scrollBy的最用效果会作用到View的内容,所以要是想滑动当前View,就需要对其父View调用二者。也可以在当前View中使用((View)getParent).scrollXX(x,y)达到同样目的。

    2 Scroller

    OK,通过上面的学习我们知道scrollTo与scrollBy可以实现滑动的效果,但是滑动的效果都是瞬间完成的,在事件执行的时候平移就已经完成了,这样的效果会让人感觉突兀,Google建议使用自然过渡的动画来实现移动效果。因此,Scroller类这样应运而生了。

    2.1 简单实例

    举一个简单的实例方便大家的理解与学习 Scroller

    主要代码

    public class CustomScrollerView extends LinearLayout {
        private Scroller mScroller;
    
        private View mLeftView;
        private View mRightView;
    
        private float mInitX, mInitY;
        private float mOffsetX, mOffsetY;
    
        public CustomScrollerView(Context context) {
            this(context, null);
        }
    
        public CustomScrollerView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public CustomScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init() {
            this.setOrientation(LinearLayout.HORIZONTAL);
    
            mScroller = new Scroller(getContext(), null, true);
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
    
            if (getChildCount() != 2) {
                throw new RuntimeException("Only need two child view! Please check you xml file!");
            }
    
            mLeftView = getChildAt(0);
            mRightView = getChildAt(1);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            switch (ev.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    mInitX = ev.getX();
                    mInitY = ev.getY();
                    super.dispatchTouchEvent(ev);
                    return true;
                case MotionEvent.ACTION_MOVE:
                    //>0为手势向右下
                    mOffsetX = ev.getX() - mInitX;
                    mOffsetY = ev.getY() - mInitY;
                    //横向手势跟随移动
                    if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {
                        int offset = (int) -mOffsetX;
                        if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {
                            return true;
                        }
                        this.scrollBy(offset, 0);
                        mInitX = ev.getX();
                        mInitY = ev.getY();
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    //松手时刻滑动
                    int offset = ((getScrollX() / (float) mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;
    //                this.scrollTo(offset, 0);
                    mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset - this.getScrollX(), 0);
                    invalidate();
                    mInitX = 0;
                    mInitY = 0;
                    mOffsetX = 0;
                    mOffsetY = 0;
                    break;
            }
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public void computeScroll() {
            if (mScroller.computeScrollOffset()) {
                this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                postInvalidate(); //允许在非主线程中出发重绘,它的出现就是简化我们在非UI线程更新view的步骤
            }
        }
    }
    

    主要布局

        <com.im_dsd.blogdemo.CustomScrollerView
            android:layout_width="200sp"
            android:layout_height="200sp"
            android:layout_centerInParent="true"
            android:orientation="horizontal"
            >
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@android:color/holo_blue_light"/>
    
            <TextView
                android:layout_width="100sp"
                android:layout_height="match_parent"
                android:background="@android:color/holo_green_light"/>
        </com.im_dsd.blogdemo.CustomScrollerView>
    
    项目效果

    通过上面实例我们可以发现在自定义View的过程中使用Scroller的流程如下图所示:

    下面我们就按照这个流程进行源码分析吧

    2.2 源码分析

    对于Scroller类 Google给出的如下解释:

    This class encapsulates scrolling. You can use scrollers ( Scroller or OverScroller) to collect the data you need to produce a scrolling animation
    for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don't automatically apply those positions to your view. It's your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.

    我们中可以看出:Scroller 是一个工具类,它只是产生一些坐标数据,而真正让View平滑的滚动起来还需要我们自行处理。我们使用的处理工具就是—— scrollTo与scrollBy

    2.2.1 构造方法分析

    public Scroller(Context context) {
        this(context, null);
    }
    
    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
            context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }
    
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        //摩擦力计算单位时间减速度
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;
    
        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }
    

    Scroller的构造方法没啥特殊的地方只不过第二个参数interpolator是插值器,不同的插值器实现不同的动画算法(这里不是重点不做展开,以后重点讲解),如果我们不传,则默认使用ViscousFluidInterpolator()插值器。

    2.2.2 startScroll与fling

    /** 
         * 使用默认滑动时间完成滑动 
         */  
        public void startScroll(int startX, int startY, int dx, int dy) {  
            startScroll(startX, startY, dx, dy, DEFAULT_DURATION);  
        }  
      
        /** 
         * 在我们想要滚动的地方调运,准备开始滚动,手动设置滚动时间
         *  
         * @param startX  滑动起始X坐标 
         * @param startY     滑动起始Y坐标 
         * @param dx    X方向滑动距离 
         * @param dy   Y方向滑动距离 
         * @param duration  完成滑动所需的时间      
         */  
        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;  
        }  
    
        /** 
         * 开始基于滑动手势的滑动。根据初始的滑动手势速度,决定滑动的距离(滑动的距离,不能大于设定的最大值,不能小于设定的最小值)  
         */
    public void fling(int startX, int startY, int velocityX, int velocityY,
        int minX, int maxX, int minY, int maxY) {
        ......
        mMode = FLING_MODE;
        mFinished = false;
        ......
        mStartX = startX;
        mStartY = startY;
        ......
        mDistance = (int) (totalDistance * Math.signum(velocity));
    
        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;
        ......
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
    }
    

    在这两个方法中,都是一些全局变量的赋值,果真没有实现滚动的方法,也佐证了Scroller是一个工具的解读。而要实现滑动还是要依靠我们手动调用View的invalidated()方法触发computeScroll()方法。

       @Override
        public void computeScroll() {
            if (mScroller.computeScrollOffset()) {
                this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                 postInvalidate(); //允许在非主线程中出发重绘,它的出现就是简化我们在非UI线程更新view的步骤
            }
        }
    

    一旦触发成功就会调用Scroller.computeScrollOffset()方法,返回结果如果为true表示当前的滑动尚未结束,如果返回false表示滑动完成。
    在Scroller类中,最最重要的就是这个computeScrollOffset方法,看上去只是返回了一个boolean类型,但他却是Scroller的核心,所有的坐标与滑动时间都由它计算完成。他将原本瞬间的滑动拆分成连续平滑的过程。

    /** 
         * Call this when you want to know the new location.  If it returns true, 
         * the animation is not yet finished.  loc will be altered to provide the 
         * new location. 
         * 调用这个函数获得新的位置坐标(滑动过程中)。如果它返回true,说明滑动没有结束。 
         * getCurX(),getCurY()方法就可以获得计算后的值。 
         */   
        public boolean computeScrollOffset() {  
            if (mFinished) {//是否结束  
                return false;  
            }  
      
            int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);//滑动开始,经过了多长时间  
          
            if (timePassed < mDuration) {//如果经过的时间小于动画完成所需时间  
                switch (mMode) {  
                case SCROLL_MODE:  
                    float x = timePassed * mDurationReciprocal;  
          
                    if (mInterpolator == null)//如果没有设置插值器,利用默认算法  
                        x = viscousFluid(x);   
                    else//否则利用插值器定义的算法  
                        x = mInterpolator.getInterpolation(x);  
          
                    mCurrX = mStartX + Math.round(x * mDeltaX);//计算当前X坐标  
                    mCurrY = mStartY + Math.round(x * mDeltaY);//计算当前Y坐标  
                    break;  
                case FLING_MODE:  
                    final float t = (float) timePassed / mDuration;  
                    final int index = (int) (NB_SAMPLES * t);  
                    final float t_inf = (float) index / NB_SAMPLES;  
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;  
                    final float d_inf = SPLINE[index];  
                    final float d_sup = SPLINE[index + 1];  
                    final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);  
                      
                    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;  
        }  
    

    从代码可以看到,如果我们没有设置插值器,就会调用内部默认算法。

    /** 
         * 函数翻译是粘性流体 
         * 估计是一种算法 
         */  
        static float viscousFluid(float x)  
        {  
            x *= sViscousFluidScale;  
            if (x < 1.0f) {  
                x -= (1.0f - (float)Math.exp(-x));  
            } else {  
                float start = 0.36787944117f;   // 1/e == exp(-1)  
                x = 1.0f - (float)Math.exp(1.0f - x);  
                x = start + x * (1.0f - start);  
            }  
            x *= sViscousFluidNormalize;  
            return x;  
        }  
    
    

    接着是两个重要的get方法

    /** 
         * Returns the current X offset in the scroll.  
         *  
         * @return The new X offset as an absolute distance from the origin. 
         * 获得当前X方向偏移 
         */  
        public final int getCurrX() {  
            return mCurrX;  
        }  
          
        /** 
         * Returns the current Y offset in the scroll.  
         *  
         * @return The new Y offset as an absolute distance from the origin. 
         * 获得当前Y方向偏移 
         */  
        public final int getCurrY() {  
            return mCurrY;  
        }  
    

    2.2.3 其他方法

    public class Scroller  {
        ......
        public Scroller(Context context) {}
        public Scroller(Context context, Interpolator interpolator) {}
        public Scroller(Context context, Interpolator interpolator, boolean flywheel) {}
        //设置滚动持续时间
        public final void setFriction(float friction) {}
        //返回滚动是否结束
        public final boolean isFinished() {}
        //强制终止滚动
        public final void forceFinished(boolean finished) {}
            //返回滚动持续时间
        public final int getDuration() {}
        //返回当前滚动的偏移量
        public final int getCurrX() {}
        public final int getCurrY() {}
        //返回当前的速度
        public float getCurrVelocity() {}
        //返回滚动起始点偏移量
        public final int getStartX() {}
        public final int getStartY() {}
            //返回滚动结束偏移量
        public final int getFinalX() {}
        public final int getFinalY() {}
        //实时调用该方法获取坐标及判断滑动是否结束,返回true动画没结束
        public boolean computeScrollOffset() {}
        //滑动到指定位置
        public void startScroll(int startX, int startY, int dx, int dy) {}
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
        //快速滑动松开手势惯性滑动
        public void fling(int startX, int startY, int velocityX, int velocityY,
                int minX, int maxX, int minY, int maxY) {}
        //终止动画,滚到最终的x、y位置
        public void abortAnimation() {}
        //延长滚动的时间
        public void extendDuration(int extend) {}
        //返回滚动开始经过的时间
        public int timePassed() {}
        //设置终止时偏移量
        public void setFinalX(int newX) {}
        public void setFinalY(int newY) {}
    }
    

    3 总结:

    1. 滑动的本质就是View随着手指的运动不断地改变坐标
    2. scrollTo(x,y)指的就是View滚动到(x,y)这个位置,但是View 要相当于父控件静止不懂,所以相对的View的内容就会滑动到(-x, -y)的位置
    3. scrollTo、scrollBy移动是瞬间的
    4. 滑动效果作用的对象是View内容
    5. Scroller类其实是一个工具类,生产滑动过程的平滑坐标,但最终的滑动动作还是需要我们自行处理
    6. Scroller类的使用流程:

    参考

    《Android群英传》
    http://blog.csdn.net/crazy__chen/article/details/45896961
    http://blog.csdn.net/yanbober/article/details/49904715

    版权声明:
    禁止一切商业行为,转载请著名出处 http://blog.csdn.net/qq_23191031。作者: 大圣代
    Copyright (c) 2017 代圣达. All rights reserved.

    相关文章

      网友评论

        本文标题:【Android View事件(三)】Scroll类源码分析与应

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