Android中的Scroller类

来源:互联网 发布:行业数据分析 编辑:程序博客网 时间:2024/05/18 09:16

Scroller译为“滚动器”,是ViewGroup类中原生支持的一个功能。在Android中,如果一个控件需要实现滚动的功能,就需要用到Scroller类。在Android系统的控件中,比如ListView、ViewPager等都用到了。这篇博客就来学习一下Scroller类,并使用Scroller类和事件的分发写一个实例。

在说Scroller类之前,先说两组相关的API:

1.invalidate()和postInvalidate()

重载方法invalidate(int l, int t, int r, int b)、invalidate(Rectdirty)和postInvalidate(int left, int top, int right, int bottom)

invalidate()方法用于重绘组件,不带参数表示重绘整个视图区域,带参数表示重绘指定的区域。调用View的invalidate()方法就相当于调用了onDraw()方法,而onDraw()方法中就是我们编写的绘图代码。需要注意的是invalidate()方法只能在UI线程中调用,如果需要在子线程中刷新组件,那就需要调用View类另一组名为postInvalidate()的方法。

了解invalidate()方法实现重新绘制界面的过程,可以查看《invalidate()和requestLayout()方法调用过程》这篇博客

2.scrollTo()和ScrollBy()

这两个方法是在View类中定义的,也就说明了在Android中所有的空间都是可以滚动的,但是这两个方法有什么区别呢?不解释,我们先来看看这两个方法在View类中是怎样实现的。

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();}}}public void scrollBy(int x, int y) {scrollTo(mScrollX + x, mScrollY + y);}

通过对源码的查看,两个方法的区别就很容易发现了:

scrollTo(int x, int y)方法中,参数x、y是目标位置,该方法先判断新的滚动位置是否发生了变化,如果是,先保存上一次的位置,再应用新位置(x,y),接着调用onScrollChanged()方法,并调用postInvalidateOnAnimation()方法(该方法和invalidate()方法效果一样,只是postInvalidateOnAnimation()更加流畅,不会失帧)刷新View组件。scrollTo()方法表示滚动到指定位置。
scrollBy(int x, int y)方法则不同,是要原来的基础上水平方向滚动x个距离,垂直方向滚动y个距离,最终还是调用了scrollTo(int x, int y)方法。scrollBy()方法表示从某一点开始滚动指定距离。

下面用一个实例来展示一下他们两个方法的区别:

首先是布局文件:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:layout_margin="16dp"    android:orientation="vertical">    <LinearLayout        android:id="@+id/view1"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:orientation="vertical">        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="ScrollTo" />        <Button            android:id="@+id/bt_scrollto"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="开始" />    </LinearLayout>    <LinearLayout        android:id="@+id/view2"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_marginTop="10dp"        android:orientation="vertical">        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="ScrollBy" />        <Button            android:id="@+id/bt_scrollby"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="开始" />    </LinearLayout>    <TextView        android:id="@+id/textView"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_marginTop="10dp"        android:background="#80ff0000"        android:padding="5dp"        android:text="text_scrollby" /></LinearLayout>

接着Activity代码:

public class MainActivity extends AppCompatActivity {    private LinearLayout view1;    private LinearLayout view2;    private Button btScrollTo;    private Button btScrollBy;    private TextView textView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        getWindow().requestFeature(Window.FEATURE_NO_TITLE);        setContentView(R.layout.activity_main);        view1 = (LinearLayout) findViewById(R.id.view1);        view2 = (LinearLayout) findViewById(R.id.view2);        btScrollTo = (Button) findViewById(R.id.bt_scrollto);        btScrollBy = (Button) findViewById(R.id.bt_scrollby);        textView = (TextView) findViewById(R.id.textView);        btScrollTo.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                view1.scrollTo(-20,0);            }        });        btScrollBy.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                view2.scrollBy(-20, 0);            }        });        textView.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                textView.scrollBy(-20,0);            }        });    }}

效果如图所示:


从效果图上可以看出,scrollTo()方法当我们点击一次之后,接着不管怎样点击都没有效果,不会再移动了,因为它已经移动到了指定的位置;而scrollBy()不同,不管我们点击多少次,都是每点击一次就会在水平方向向右移动20像素,因为它是在原来的基础上进行偏移。

几点注意:

① 在布局文件中可以看到,前面两个都是用一个线性布局包裹了一个textView和一个button,最后一个是一个TextView;在代码中可以看到,在点击按钮时是调用线性布局的scrollTo()和scrolBy()方法,但最终移动的并不是线性布局,而是线性布局的孩子控件,也就是textView和Button两个控件;在点击TextView的时候,调用的就是TextView的scrollBy()方法,移动的是TextView中显示的文字,也就是TextView的内容。所以,第一点注意,我们在使用scrollTo()和scrollBy()的时候,如果是容器布局调用的话,那么移动的是他的孩子控件,并不是他自身,如果不是容器控件调用的话,那么移动的就是控件中的内容。

② 在图中,我们看到的移动方向是向右,但是在代码中调用时所传的参数是负数,这也就是第二点需要注意的,如果需要向右或者向下(就是沿着坐标轴的正方向移动),那么我们调用这两个方法传的参数是负数,否则就是传正数。

在上面的代码中,我们看到了不管是View,还是ViewGroup,都是可以滚动的。同时,我们也发现了一个问题,如果我需要从位置(0,0)滚动到位置(200,0),如果我们调用scrollTo(200,0),那么他可以直接跳过去,目的达到了,但是对用户好像不太友好,如果我们使用scrollBy()方法一点一点设置,那么我们自己有不好控制每隔多久调用一次以及一次移动多少距离,还要定义一个变量计算移动了多少距离,太麻烦。其实,在Android中,想要实现这样的效果很简单,只需要简单的几步就可以完成。这就是利用这篇博客的主角Scroller类。


Scroller类:

Scroller 类在滚动过程的的几个主要作用如下:
◆启动滚动动作;
◆根据提供的滚动目标位置和持续时间计算出中间的过渡位置;
◆判断滚动是否结束;
◆介入View或ViewGroup的重绘流程,从而形成滚动动画。


Scroller类对于滚动的作用非常重大,但是他定义的方法不是太多,下面列出了Scroller类中比较常见的方法:

// 构造方法,interpolator指定插速器,如果没有指定,// 默认插速器为ViscousFluidInterpolator,flywheel参数为true可以提供类似“飞轮”的行为public Scroller(Context context)public Scroller(Context context, Interpolator interpolator)public Scroller(Context context, Interpolator interpolator, boolean flywheel)// 设置一个摩擦系数,默认为 0.015f, 摩擦系数决定惯性滑行的距离public final void setFriction(float friction)// 返回起始 x 坐标值public final int getStartX()// 返回起始 y 坐标值public final int getStartY()// 返回结束 x 坐标值public final int getFinalX()// 返回结束 y 坐标值public final int getFinalY()// 返回滚动过程中的 x 坐标值,滚动时会提供startX(起始)和finalX(结束),currX根据这两个值计算而来public final int getCurrX()// 返回滚动过程中的 y 坐标值,滚动时会提供 startY(起始)和finalY(结束),currY根据这两个值计算而来public final int getCurrY()// 计算滚动偏移量,必调方法之一。主要负责计算currX和currY两个值,其返回值为true表示滚动尚未完成, 为false表示滚动已结束public boolean computeScrollOffset()// 启动滚动行为,startX和startY表示起始位置,dx、dy表示要滚动的x、y方向的距离,duration表示持续时间,默认时间为 250 毫秒public void startScroll(int startX, int startY, int dx, int dy)public void startScroll(int startX, int startY, int dx, int dy, int duration)// 判断滚动是否已结束,返回true表示已结束public final boolean isFinished()// 强制结束滚动,currX、 currY 即为当前坐标;public final void forceFinished(boolean finished)// 与forceFinished功用类似,停止滚动,但currX、currY设置为终点坐标public void abortAnimation()// 延长滚动时间public void extendDuration(int extend)// 返回滚动已耗费的时间,单位为毫秒public int timePassed()// 设置终止位置的 x 坐标,可能需要调用extendDuration()延长或缩短动画时间public void setFinalX(int newX)// 设置终止位置的 y 坐标,可能需要调用extendDuration()延长或缩短动画时间public void setFinalY(int newY)
上面的方法中, 常用的主要有startScroll()、computeScrollOffset()、getCurrX()、getCurrY()和abortAnimation()等几个方法,下面就通过一个这些方法实现一个简单的案例:

下面直接上代码:

1.自定义的ScrollerTest.java类

public class ScrollerTest extends ViewGroup {    /**定义Scroller对象*/    private Scroller mScroller;    /**定义系统默认滑动系数*/    private int mTapSlop;    /**记录按下时的x方向坐标*/    private int mDownX;    /**控件左边界*/    private int mLeft;    /**控件右边界*/    private int mRight;    /**屏幕宽度*/    private int mScreenWidth;    /**当前显示的页面角标*/    private int mIndex = 0;    public ScrollerTest(Context context) {        this(context, null);    }    public ScrollerTest(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public ScrollerTest(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        // 获取屏幕宽度,为了简单将所有的直接孩子控件的宽度设置成屏幕宽度        WindowManager systemService = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        DisplayMetrics outMetrics = new DisplayMetrics();        systemService.getDefaultDisplay().getMetrics(outMetrics);        mScreenWidth = outMetrics.widthPixels;        // 获取系统默认滑动系数        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);        mTapSlop = viewConfiguration.getScaledDoubleTapSlop();        // 1、初始化Scroller对象        mScroller = new Scroller(context);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        // 测量每一个孩子控件的大小        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            View childView = this.getChildAt(i);            measureChild(childView, widthMeasureSpec, heightMeasureSpec);        }        // getDefaultSize():作用是返回一个默认的值,如果MeasureSpec没有强制限制的话则使用提供的大小.否则在允许范围内可任意指定大小        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));    }    // 继承至ViewGroup,重写onLayout()方法确定每一个孩子控件的位置    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        if (changed) { // 判断是否需要重新布局            int childCount = getChildCount();            for (int i = 0; i < childCount; i++) {                View childView = this.getChildAt(i);                int childViewMeasuredWidth = childView.getMeasuredWidth();                // 指定每一个孩子控件的位置,这里就是直接水平排列每一个孩子控件                childView.layout(i * childViewMeasuredWidth, 0, (i + 1) * childViewMeasuredWidth, childView.getMeasuredHeight());            }            mLeft = this.getChildAt(0).getLeft();            mRight = this.getChildAt(childCount - 1).getRight();        }    }    @Override // 判断是否需要拦截事件的方法    public boolean onInterceptTouchEvent(MotionEvent ev) {        int action = ev.getAction();        switch (action) {            case MotionEvent.ACTION_DOWN:                mDownX = (int) ev.getX();                break;            case MotionEvent.ACTION_MOVE:                int moveX = (int) ev.getX();                int diffX = Math.abs(moveX - mDownX);                mDownX = moveX;                if (diffX > mTapSlop) {                    // 如果移动的距离大于默认滑动系数就拦截事件                    return true;                }                break;            default:                break;        }        return super.onInterceptTouchEvent(ev);    }    @Override // 处理事件的方法    public boolean onTouchEvent(MotionEvent event) {        int action = event.getAction();        switch (action) {            case MotionEvent.ACTION_DOWN:                if(mScroller != null && !mScroller.isFinished()){                    // 如果当前还没有完成滑动,就强制结束滑动状态                    mScroller.abortAnimation();                }                mDownX = (int) event.getX();                break;            case MotionEvent.ACTION_MOVE:                int moveX = (int) event.getX();                int dX = mDownX - moveX;                // 边界处理                if (getScrollX() + dX < mLeft) {                    scrollTo(mLeft, 0);                } else if (getScrollX() + mScreenWidth + dX > mRight) {                    scrollTo(mRight - mScreenWidth, 0);                } else {                    // 非边界,直接移动                    scrollBy(dX, 0);                    mDownX = moveX;                }                break;            case MotionEvent.ACTION_UP:                // 计算应该显示的是第几页                mIndex = (getScrollX() + mScreenWidth / 2) / mScreenWidth;                // 计算需要滑动的距离                int scrollX = mIndex * mScreenWidth - getScrollX();                ///2、开始滑动                mScroller.startScroll(getScrollX(), 0, scrollX, 0, scrollX);                invalidate();                break;            default:                break;        }        return true;    }    // 3、维持滑动状态    @Override    public void computeScroll() {        if (mScroller.computeScrollOffset()) { // 判断是否已经完成滑动            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            postInvalidate();// 不断通过postInvalidate()方法调用draw()方法重绘界面        }    }    // 滑动到指定位置    public void setPosition(int position) {        if (position > getChildCount() - 1 || position < 0) {            throw new ArrayIndexOutOfBoundsException("页面位置角标错误");        }        int dX = position * mScreenWidth - getScrollX();        mScroller.startScroll(getScrollX(), 0, dX, 0, dX);        postInvalidate();    }}
2.布局文件代码:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <renj.customerview.widget.ScrollerTest        android:id="@+id/scroll"        android:layout_width="match_parent"        android:layout_height="0dp"        android:layout_weight="1">        <LinearLayout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:gravity="center"            android:background="#80ff0000">            <TextView                android:textSize="30sp"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="第一页" />        </LinearLayout>        <LinearLayout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:gravity="center"            android:background="#8000ff00">            <TextView                android:textSize="30sp"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="第二页" />        </LinearLayout>        <LinearLayout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:gravity="center"            android:background="#800000ff">            <TextView                android:textSize="30sp"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="第三页" />        </LinearLayout>    </renj.customerview.widget.ScrollerTest>    <Button        android:id="@+id/button"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:padding="5dp"        android:text="跳转到第三页" /></LinearLayout>
3.Activity中的代码:

public class MainActivity extends AppCompatActivity {    private ScrollerTest scrollTest;    private Button button;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        getWindow().requestFeature(Window.FEATURE_NO_TITLE);        setContentView(R.layout.activity_main);        scrollTest = (ScrollerTest) findViewById(R.id.scroll);        button = (Button) findViewById(R.id.button);        button.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                scrollTest.setPosition(2);            }        });    }}
4.最终运行结果展示:


通过上面的实例可以看出,使用Scroller类可以归纳为三步:

① 创建Scroller对象,可以直接通过构造方法创建;

② 通过Scroller类的startScroll()开启动画;

③ 重写View中的computeScroll()方法维持动画直到结束(控件到达指定位置)。

在第三步中的computeScroll()方法中调用Scroller类的computeScrollOffset()方法判断是否已经移动到最终位置,如果返回false,表示移动到最终位置,结束动画;如果返回true,表示没有结束动画,那么就可以调用Scroller类中的getCurrX()/getCurrY()获取下一个移动的位置并调用scrollTo()进行移动,最后调用postInvalidate()方法重新绘制界面实现动画效果。

在文章开篇说道了调用invalidate()就是调用了空间的绘制方法,但是在draw()方法中是怎样调用View中的computeScroll()方法实现不断刷新界面的,可以查看《Android自定义View之View的绘制流程》这篇博客的绘制(draw)部分。

在最后的这一个案例中使用到了事件分发相关的知识,事件的分发在《Android中的事件分发机制》这篇博客当中已经聊过了,这里就不在啰嗦。


0 0
原创粉丝点击