Scroller的使用及解析
在学习这个之前,你首先要了解android的消息机制,Android的坐标系统。
-
scrollBy 个 scrollTo的区别
scrollTo:相对View的初始位置移动的距离。
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();
}
}
}
//mScrollX 当前的X偏移量,mScrollY 当前的Y偏移量
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
- 在实际过程中,例如我们想使控件水平向右移动200dp,那么使用方式:
scrollTo(-200,0);
此时用户可能会很奇怪为什么是-200,因为这个偏移量是相对距离,而相对点不是屏幕左上角,而是最终View位置的左上角。
成员变量mScrollX, mScrollY,X轴方向的偏移量和Y轴方向的偏移量,这个是一个相对距离,相对的不是屏幕的左上角,而是最终View的左上角,举个通俗易懂的例子,一列火车从吉安到深圳,途中经过赣州,那么原点就是赣州,偏移量就是 负的吉安到赣州的距离,大家从getScrollX()方法中的注释中就能看出答案来
Note:假如你给一个LinearLayout调用scrollTo()方法,并不是LinearLayout滚动,而是LinearLayout里面的内容进行滚动,比如你想对一个按钮进行滚动,直接用Button调用scrollTo()一定达不到你的需求,大家可以试一试,如果真要对某个按钮进行scrollTo()滚动的话,我们可以在Button外面包裹一层Layout,然后对Layout调用scrollTo()方法。
- computeScrollOffset 方法
public class ScrollerLayout extends ViewGroup {
...
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
View滚动的实现原理,我们先调用Scroller的startScroll()方法来进行一些滚动的初始化设置,然后迫使View进行绘制,我们调用View的invalidate()或postInvalidate()就可以重新绘制View,绘制View的时候会触发computeScroll()方法,我们重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动有没有结束,如果滚动没有结束我们就调用scrollTo()方法来进行滚动,该scrollTo()方法虽然会重新绘制View,但是我们还是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,重新绘制View又触发computeScroll(),所以就进入一个循环阶段,这样子就实现了在某个时间段里面滚动某段距离的一个平滑的滚动效果也许有人会问,干嘛还要调用来调用去最后在调用scrollTo()方法,还不如直接调用scrollTo()方法来实现滚动,其实直接调用是可以,只不过scrollTo()是瞬间滚动的,给人的用户体验不太好,所以Android提供了Scroller类实现平滑滚动的效果。为了方面大家理解,我画了一个简单的scroll实现滚动的原理图
- Scroller 使用
Scroller使用基本步骤:
- 创建Scroller的实例
- 判断刷新时机并调用startScroll()方法来初始化滚动数据并刷新界面
- (可选)重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
package com.example.qwe;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
public class ScrollerLayout extends ViewGroup {
//滚动实例
private Scroller mScroller;
//判定为移动的最小像素
private int mTouchSlop;
//手机按下的时候屏幕坐标
private float mXDown;
//按住移动的时候屏幕坐标
private float mXMove;
//上次触发ACTION_MOVE事件时的屏幕坐标
private float mXLastMove;
//界面可滚动的左边界
private int mLeftBorder;
//界面可滚动的右边界
private int mRightBorder;
public ScrollerLayout(Context ctx, AttributeSet attrs){
super(ctx,attrs);
// 第一步,创建Scroller的实例
mScroller = new Scroller(ctx);
ViewConfiguration viewConfiguration = ViewConfiguration.get(ctx);
// 获取TouchSlop值
mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
}
//view绘制三部曲:OnMeasure,OnLayout,OnDraw
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int nChildCount = getChildCount();
for (int i = 0;i < nChildCount;++i){
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed){
int nChildCount = getChildCount();
for (int i = 0;i < nChildCount;++i){
View childView = getChildAt(i);
//水平布局这三个控件
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
if(nChildCount > 0){
mLeftBorder = getChildAt(0).getLeft();
mRightBorder = getChildAt(getChildCount() - 1).getRight();
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mXLastMove = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
//如果达到最小移动单位,则拦截ACTION_MOVE事件
if(diff > mTouchSlop)
return true;
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
int scrolledX = (int)(mXLastMove - mXMove);
if(getScrollX() + scrolledX < mLeftBorder){
scrollTo(mLeftBorder,0);
return true;
}
else if(getScrollX() + getWidth() + scrolledX > mRightBorder){
scrollTo(mRightBorder - getWidth(),0);
return true;
}
scrollBy(scrolledX,0);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
//这里为什么没用使用scrollTo是为了实现缓冲效果,而不是一下子跳跃滚动
// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
//为了配合调用startScroll需要重写computeScroll方法
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
super.computeScroll();
// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
// 返回false表示滚动完成
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<com.example.guolin.scrollertest.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is first child view"/>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is second child view"/>
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="This is third child view"/>
</com.example.guolin.scrollertest.ScrollerLayout>
参考博客:
Scroller原理: https://www.jianshu.com/p/543b88fa609c
Scroller使用:https://blog.csdn.net/guolin_blog/article/details/48719871
上一篇: 初学opencv/边缘检测
推荐阅读