View弹性滑动Scroller原理解析

1 代码示例

Scroller是用于View弹性滑动的工具类,可以平滑的移动view的位置,而不会产生突变的效果。ViewPager和ListView都使用到了该类。实际上Scroller里面使用到了View类的scrollTo()和scrollBy()这两个方法,前者滑动的是绝对坐标,之后为相对坐标。下面这幅图是调用ViewGroup的scrollTo(100,100)的效果,黄色区域是ViewGroup,绿色框是其内容。


scrollerTo.png

可以看到,如果调用scroller(x,y),x>0则其内容往左移动,y>0则往上移动,getScrollX()=x,getScrollY()=y。
我们来看一个例子,将一个图片(ButtonView)随着手指滑动而移动,松手之后弹性滑动会原来的位置。
xml文件布局如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/cl_top"
    tools:context="com.study.test.CircleActivity"
    android:layout_margin="50dp">
    <com.study.test.view.MyViewGroup
        android:background="@color/colorPrimary"
        android:layout_centerInParent="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp"
        >
        <com.study.test.view.ButtonView
            android:layout_centerInParent="true"
            android:layout_height="100dp"
            android:layout_width="100dp"
            android:src="@drawable/meitu"
            >
        </com.study.test.view.ButtonView>
        <TextView
            android:text="我是个托"
            android:textSize="50dp"
            android:layout_centerHorizontal="true"
            android:layout_alignParentBottom="true"
            android:layout_marginTop="50dp"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:background="#ffffff"
            >
        </TextView>
    </com.study.test.view.MyViewGroup>
</RelativeLayout>

外层用了一个自定义的ViewGroup背景为蓝色(继承自RelativeLayout),里面是自定义的ButtonView以及系统自带的TextView,界面效果如下图。


界面效果.jpg

我们接着看自定义ButtonView:

public class ButtonView extends android.support.v7.widget.AppCompatImageView {
       ……

    ViewGroup viewGroup;
    int startX;
    int startY;
    Scroller mScroller = new Scroller(this.getContext());

    @Override
    protected void onAttachedToWindow() {
        viewGroup = (ViewGroup) getParent();
        super.onAttachedToWindow();
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = (int) event.getX();
                    startY = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int curX = (int) event.getX();
                    int curY = (int) event.getY();
                    int delX = curX-startX;
                    int delY = curY-startY;
                    startX = curX;
                    startY = curY;
                    viewGroup.scrollBy(-delX,-delY);
                    break;
                case MotionEvent.ACTION_UP:
                    mScroller.startScroll(viewGroup.getScrollX(),viewGroup.getScrollY(),
                            -viewGroup.getScrollX(),-viewGroup.getScrollY(),3000);
                    invalidate();
                    break;
                default:
                    break;
            }
            return true;
    }   

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

首先是在MotionEvent.ACTION_MOVE中使用scrollBy进行移动,如果手指向右滑动,则delX>0,要ButtonView向右移动,则scrollerX<0,所以scrollBy得参数为-delX和-delY。

在MotionEvent.MotionEvent.ACTION_UP,通过Scroller.startScroll()进行回弹。整体效果如下:
GIF.gif

从图中看到,在ButtonView随着手指移动时,有很明显的跳跃感,不连贯。手指抬起之后的移动就很平滑。另外,我只是在ButtonView调用了viewGroup.scrollBy(-delX,-delY)和mScroller方法,但是同一级的TextView页跟着滑动了,这是因为滑动的是viewGroup的内容,而不是它本身,它本身的蓝色背景并没有任何变化。

2 源码分析

 /**
     * Start scrolling by providing a starting point, the distance to travel,
     * and the duration of the scroll.
     * 
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     * @param duration Duration of the scroll in milliseconds.
     */
    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;
    }

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

startScroll中startX,startY表示起点位置,需要设置为当前位置,不然会直接从当前位置跳到起点位置之后执行移动。如果是不断调用startScroll,可以通过getFinalX()这样获取前一个scroll完成之后的终点位置,作为起点。
AnimationUtils.currentAnimationTimeMillis()获取当前移动动画开始的时间mStartTime ,结合动画持续时间duration,也就是说在 mStartTime -> mStartTime+duration 时间内,从startX移动到startX+dx
我们通过不断的重绘,每次重绘都根据当前时间与mStartTime的差值,计算出当前需要滑动到的位置,就可以实现弹性滑动了。怎样才能不断重绘呢,系统在每次调用invalidate重绘都会走到View的computeScroll方法中,我们只需要在重写该方法,在其中作判断,还没有移动到目的地,就进行移动,并且调用invalidate,则在动画完成之前,会不断重绘。

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

推荐阅读更多精彩内容