关于Scroller ,scrollTo,scrollBy

    最近想总结一下,关于View滑动的知识,也为下一篇View的滑动总结记录一下这个知识点点吧。提到这个,先说说Android的坐标系吧。

Android中的坐标系

     Android中有2种坐标系,分别称之为Android坐标系和视图坐标系。而对应的也有一些相关的方法可以获取坐标系中的坐标值,只有搞清楚这些区别,才能在使用的时候,不至于出错或者得不到你想要的效果。

1.Android坐标系

                 

如图所示,Android以屏幕左上角位坐标原点,从该点向右为X轴的正方向,向下为Y轴的正方向,在我们处理触屏事件的时候,使用getRawX()/getRawY(),都是相对于这个坐标原点的坐标,也就是绝对坐标啦。

2.视图坐标系

视图坐标系描述的是子View相对于父View的相对位置坐标

            

如上图所示,在这里我们可以用getX(),getY(),获取的是子视图相对于父视图的坐标位置。PS:我在开发中还是喜欢使用getRawX或者getRawY获取坐标值,这样就避免去计算相对坐标那么多麻烦的计算了额。

scrollTo和scrollBy

scrollTo(int x,int y),从字面意思,我们可以理解为滚动到某个位置,这里就是滚动到x,y位置

scrollBy(int x,int y),从字面意思,我们可以理解为滚动某一段距离,这里的意思就是相对于当前位置X方向上再滚动x距离,Y方向上在滚动y距离。

看看相关源码:

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();
            }
        }
    }
/**
     * 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()方法中,入口就是判断了mScrollX!=x || mScrollY !=y;  其中x,y是我们自己传入的值,那么mScrollX和mScrollY是什么呢

public final int getScrollX() {
        return mScrollX;
    }
/**
     * Return the scrolled top position of this view. This is the top edge of
     * the displayed part of your view. You do not need to draw any pixels above
     * it, since those are outside of the frame of your view on screen.
     *
     * @return The top edge of the displayed part of your view, in pixels.
     */
    public final int getScrollY() {
        return mScrollY;
    }

其实mScrollX和mScrollY就是View滚动的X方向和Y方向的距离。二这种距离与我们平时的理解有点区别,简单的理解和记忆就是View向右滑动mScrollX为负值,向下滑动mScrollY为负值,反之为正值,与平时的认知哟独爱你相反的感觉。

scrollTo和scrollBy移动的方式实际是内容的移动,也就是当你当前调用这两个方法的View位一个ViewGroup的时候,实际就是这个ViewGroup里面的子View的移动,如果你调用这两个方法的View是一个子View,例如TextView调用这个两个方法的时候,实际就是TextView里面的文本内容的移动了。

mScrollX 的计算方式是,View的左边缘的位置-View内容的左边缘 ,所以向右移动时,View的内容向右移动,它的X坐标肯定大于View的X坐标,所以其差值位负值

Scroller实现弹性滑动

    在上面我们看到其实调用View的scrollTo/scrollBy方法,其实都是使我们空间在瞬间移动到某个位置,这给人的感觉很不好,不能实现平滑的滑动,因此此时的Scroller就应运而生了额。说道Scroller,其实他与前面的scrollTo和scrollBy实现原理基本类似。

关于Scroller的开发步骤

1.初始化Scroller,一般在自定义View的构造函数里面初始化,避免重复创建。

mScroller = new Scroller(context);

2。重写computeScroll()方法。

@Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
           // ((View)getParent()).scrollTo();
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

在Scroller中提供了computeScrollOffset()方法,这个方法是用来判断整个滑动是否完成。在computeScroll法法中调用invalidate()方法,这其实是一个递归循环的调用invalidate()->onDraw()->computeScroll()这样才能实现平滑的滑动。

3.startScroll开启模拟过程

Scroller 是由startScroll()方法开启滑动的,他有两个重载方法

public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_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;
    }

他们两个函数的差别就在与一个有duration参数,一个没有。我们看到其实在这个方法里面也只是对一些坐标进行赋值。没有看到我们控件的实际滑动。其实控件的滑动是需要在startScroll()之后调用View的invalidate();view的重绘其实会间接的调用computeScroll()方法。

/**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    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;
    }

调用这个方法其实就是判断滑动是否结束,如果返回true表示未结束,false表示已经结束。可以看到这里面也是计算坐标的。当timePassed<duration的时候,根据时间和插值器计算出mCurrX,mCurrY的值,并且函数返回true。也就代表滑动没有结束。而computeScroll方法中调用scrollTo(mScroller.getCurrX(),mScroller.getCurrY()),其实就是调用了scrollTo(mCurrX,mCurrY);这里的mCurrX和mCurrY就是上面计算的值。

通过上面的分析可以得出,通过Scroller,并且按照上面的三个步骤去完成我们的滑动就没有问题了。

下面以一个简单的例子,来实现类似系统ViewPager的功能,这里也借鉴了一下郭霖关于Scroller的讲解,谢谢作者,当然也不完全相同,这里只是为了记录一下相关写法和步骤,以便自己以后方便阅读和查阅。

public class ScrollerLayout extends ViewGroup {
    private Scroller mScroller;
    private int mTouchSlop;
    private float lastDownX,lastDownY;
    private int leftBorder,rightBorder;
    public ScrollerLayout(Context context) {
        super(context);
        initView(context);
    }

    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }


    private void initView(Context context){
        mScroller = new Scroller(context);
        //表示滑动的手指需要移动的最小距离
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(changed){
            int count = getChildCount();
            for(int i=0;i<count;i++){
                View childView = getChildAt(i);
                if(childView !=null){
                   // view.layout(view.getMeasuredWidth()*i,0,(i+1)*view.getMeasuredWidth(),getMeasuredHeight());
                    childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
                }
            }
        }
        // 初始化左右边界值  防止最左边的View和最右边的View滑出边界了
        leftBorder = getChildAt(0).getLeft();
        rightBorder = getChildAt(getChildCount() - 1).getRight();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 为ScrollerLayout中的每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float x = ev.getRawX();
        float y = ev.getRawY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:{
                lastDownX = x;
                lastDownY = y;
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                float deltX = Math.abs(x-lastDownX);
                if(deltX>mTouchSlop){
                    return true;
                }
                break;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:{  //如果内部控件没有设置cliclable为ture,就需要拦截down的操作
                return true;
            }
            case MotionEvent.ACTION_MOVE:{
                int dx = (int)(lastDownX-event.getRawX());
                if(getScrollX()+dx<leftBorder){  //防止滑动超出左边界
                    scrollTo(leftBorder,0);
                    return true;
                }else if(getScrollX()+getWidth()+dx>rightBorder){ //防止滑动超出右边界
                    scrollTo(rightBorder-getWidth(),0);
                    return true;
                }
                scrollBy(dx,0);
                //这句话必须要,否则滑动就不能产生了
                lastDownX = event.getRawX();
                break;
            }
            case MotionEvent.ACTION_UP:{
                // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
                if(dx<getWidth()/2){    //这里是计算 我们滑动的距离如果小于一半,松开手之后就回去了
                    mScroller.startScroll(getScrollX(),0,dx,0);
                }else{  //滑动距离超过一半,松开手指之后,就惯性滑动一整个屏幕。
                    mScroller.startScroll(getScrollX(), 0, dx, 0);
                }
                invalidate();
                break;
            }
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
           // ((View)getParent()).scrollTo();
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

代码不难,注释也写的很清楚了额,不再详细解释了。Demo传送门

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值