Android Scroller解析

Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用Scroller的场景并不多,但是很多大家所熟知的控件在内部都是使用Scroller来实现的,如ViewPager、ListView等。而如果能够把Scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似于ViewPager这样的功能。那么首先新建一个ScrollerTest项目,今天就让我们�通过例子来学习一下吧。

先撇开Scroller类不谈,其实任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法,如下图所示:

这两个方法都是用于对View进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy()方法是让View相对于当前的位置滚动某段距离,而scrollTo()方法则是让View相对于初始的位置滚动某段距离。这样讲大家理解起来可能有点费劲,我们来通过例子实验一下就知道了。

修改activity_main.xml中的布局文件,代码如下所示:

外层我们使用了一个LinearLayout,然后在里面包含了两个按钮,一个用于触发scrollTo逻辑,一个用于触发scrollBy逻辑。

接着修改MainActivity中的代码,如下所示:

publicclassMainActivityextendsAppCompatActivity{privateLinearLayout layout;privateButton scrollToBtn;privateButton scrollByBtn;@OverrideprotectedvoidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        layout = (LinearLayout) findViewById(R.id.layout);        scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);        scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);        scrollToBtn.setOnClickListener(newView.OnClickListener() {@OverridepublicvoidonClick(View v) {                layout.scrollTo(-60, -100);            }        });        scrollByBtn.setOnClickListener(newView.OnClickListener() {@OverridepublicvoidonClick(View v) {                layout.scrollBy(-60, -100);            }        });    }}

没错,代码就是这么简单。当点击了scrollTo按钮时,我们调用了LinearLayout的scrollTo()方法,当点击了scrollBy按钮时,调用了LinearLayout的scrollBy()方法。那有的朋友可能会问了,为什么都是调用的LinearLayout中的scroll方法?这里一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,而LinearLayout中的内容就是我们的两个Button,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。

另外还有一点需要注意,就是两个scroll方法中传入的参数,第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,单位是像素。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位是像素。

那说了这么多,scrollTo()和scrollBy()这两个方法到底有什么区别呢?其实运行一下代码我们就能立刻知道了:

可以看到,当我们点击scrollTo按钮时,两个按钮会一起向右下方滚动,因为我们传入的参数是-60和-100,因此向右下方移动是正确的。但是你会发现,之后再点击scrollTo按钮就没有任何作用了,界面不会再继续滚动,只有点击scrollBy按钮界面才会继续滚动,并且不停点击scrollBy按钮界面会一起滚动下去。

现在我们再来回头看一下这两个方法的区别,scrollTo()方法是让View相对于初始的位置滚动某段距离,由于View的初始位置是不变的,因此不管我们点击多少次scrollTo按钮滚动到的都将是同一个位置。而scrollBy()方法则是让View相对于当前的位置滚动某段距离,那每当我们点击一次scrollBy按钮,View的当前位置都进行了变动,因此不停点击会一直向右下方移动。

通过这个例子来理解,相信大家已经把scrollTo()和scrollBy()这两个方法的区别搞清楚了,但是现在还有一个问题,从上图中大家也能看得出来,目前使用这两个方法完成的滚动效果是跳跃式的,没有任何平滑滚动的效果。没错,只靠scrollTo()和scrollBy()这两个方法是很难完成ViewPager这样的效果的,因此我们还需要借助另外一个关键性的工具,也就我们今天的主角Scroller。

Scroller的基本用法其实还是比较简单的,主要可以分为以下几个步骤:

1. 创建Scroller的实例

2. 调用startScroll()方法来初始化滚动数据并刷新界面

3. 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑

那么下面我们就按照上述的步骤,通过一个模仿ViewPager的简易例子来学习和理解一下Scroller的用法。

新建一个ScrollerLayout并让它继承自ViewGroup来作为我们的简易ViewPager布局,代码如下所示:

/**

* Created by guolin on 16/1/12.

*/publicclassScrollerLayoutextendsViewGroup{/**

* 用于完成滚动操作的实例

*/privateScroller mScroller;/**

* 判定为拖动的最小移动像素数

*/privateintmTouchSlop;/**

* 手机按下时的屏幕坐标

*/privatefloatmXDown;/**

* 手机当时所处的屏幕坐标

*/privatefloatmXMove;/**

* 上次触发ACTION_MOVE事件时的屏幕坐标

*/privatefloatmXLastMove;/**

* 界面可滚动的左边界

*/privateintleftBorder;/**

* 界面可滚动的右边界

*/privateintrightBorder;publicScrollerLayout(Context context, AttributeSet attrs) {super(context, attrs);// 第一步,创建Scroller的实例mScroller =newScroller(context);        ViewConfiguration configuration = ViewConfiguration.get(context);// 获取TouchSlop值mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);    }@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);intchildCount = getChildCount();for(inti =0; i < childCount; i++) {            View childView = getChildAt(i);// 为ScrollerLayout中的每一个子控件测量大小measureChild(childView, widthMeasureSpec, heightMeasureSpec);        }    }@OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb) {if(changed) {intchildCount = getChildCount();for(inti =0; i < childCount; i++) {                View childView = getChildAt(i);// 为ScrollerLayout中的每一个子控件在水平方向上进行布局childView.layout(i * childView.getMeasuredWidth(),0, (i +1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());            }// 初始化左右边界值leftBorder = getChildAt(0).getLeft();            rightBorder = getChildAt(getChildCount() -1).getRight();        }    }@OverridepublicbooleanonInterceptTouchEvent(MotionEvent ev) {switch(ev.getAction()) {caseMotionEvent.ACTION_DOWN:                mXDown = ev.getRawX();                mXLastMove = mXDown;break;caseMotionEvent.ACTION_MOVE:                mXMove = ev.getRawX();floatdiff = Math.abs(mXMove - mXDown);                mXLastMove = mXMove;// 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件if(diff > mTouchSlop) {returntrue;                }break;        }returnsuper.onInterceptTouchEvent(ev);    }@OverridepublicbooleanonTouchEvent(MotionEvent event) {switch(event.getAction()) {caseMotionEvent.ACTION_MOVE:                mXMove = event.getRawX();intscrolledX = (int) (mXLastMove - mXMove);if(getScrollX() + scrolledX < leftBorder) {                    scrollTo(leftBorder,0);returntrue;                }elseif(getScrollX() + getWidth() + scrolledX > rightBorder) {                    scrollTo(rightBorder - getWidth(),0);returntrue;                }                scrollBy(scrolledX,0);                mXLastMove = mXMove;break;caseMotionEvent.ACTION_UP:// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面inttargetIndex = (getScrollX() + getWidth() /2) / getWidth();intdx = targetIndex * getWidth() - getScrollX();// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面mScroller.startScroll(getScrollX(),0, dx,0);                invalidate();break;        }returnsuper.onTouchEvent(event);    }@OverridepublicvoidcomputeScroll() {// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑if(mScroller.computeScrollOffset()) {            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            invalidate();        }    }}

整个Scroller用法的代码都在这里了,代码并不长,一共才100多行,我们一点点来看。

首先在ScrollerLayout的构造函数里面我们进行了上述步骤中的第一步操作,即创建Scroller的实例,由于Scroller的实例只需创建一次,因此我们把它放到构造函数里面执行。另外在构建函数中我们还初始化的TouchSlop的值,这个值在后面将用于判断当前用户的操作是否是拖动。

接着重写onMeasure()方法和onLayout()方法,在onMeasure()方法中测量ScrollerLayout里的每一个子控件的大小,在onLayout()方法中为ScrollerLayout里的每一个子控件在水平方向上进行布局。如果有朋友对这两个方法的作用还不理解,可以参照我之前写的一篇文章Android视图绘制流程完全解析,带你一步步深入了解View(二)

接着重写onInterceptTouchEvent()方法, 在这个方法中我们记录了用户手指按下时的X坐标位置,以及用户手指在屏幕上拖动时的X坐标位置,当两者之间的距离大于TouchSlop值时,就认为用户正在拖动布局,然后我们就将事件在这里拦截掉,阻止事件传递到子控件当中。

那么当我们把事件拦截掉之后,就会将事件交给ScrollerLayout的onTouchEvent()方法来处理。如果当前事件是ACTION_MOVE,说明用户正在拖动布局,那么我们就应该对布局内容进行滚动从而影响拖动事件,实现的方式就是使用我们刚刚所学的scrollBy()方法,用户拖动了多少这里就scrollBy多少。另外为了防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置。

如果当前事件是ACTION_UP时,说明用户手指抬起来了,但是目前很有可能用户只是将布局拖动到了中间,我们不可能让布局就这么停留在中间的位置,因此接下来就需要借助Scroller来完成后续的滚动操作。首先这里我们先根据当前的滚动位置来计算布局应该继续滚动到哪一个子控件的页面,然后计算出距离该页面还需滚动多少距离。接下来我们就该进行上述步骤中的第二步操作,调用startScroll()方法来初始化滚动数据并刷新界面。startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。

现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的curX和curY坐标传入,然后刷新界面从而完成平滑滚动的操作。

现在ScrollerLayout已经准备好了,接下来我们修改activity_main.xml布局中的内容,如下所示:

可以看到,这里我们在ScrollerLayout中放置了三个按钮用来进行测试,其实这里不仅可以放置按钮,放置任何控件都是没问题的。

最后MainActivity当中删除掉之前测试的代码:

publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);    }}

好的,所有代码都在这里了,现在我们可以运行一下程序来看一看效果了,如下图所示:

http://blog.csdn.net/guolin_blog/article/details/48719871

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

推荐阅读更多精彩内容