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中的事件分发机制》这篇博客当中已经聊过了,这里就不在啰嗦。
- Android中的Scroller类
- Android中的Scroller类讲解
- Android类说明---Scroller
- Android类说明---Scroller
- android之scroller类
- Android 滚动类Scroller
- 【android】Scroller类介绍
- Android类说明---Scroller
- android Scroller类详解
- android scroller类的使用
- android scroller类的使用
- android api Scroller类翻译
- android中Scroller类分析
- android scroller类的使用
- Android Scroller和VelocityTracker类
- android scroller
- android Scroller
- android scroller
- 专题2-我从内部看ARM-ARM工作模式+寄存器详解
- JSP脚本中的9个内置对象---00概述
- C语言 - 三位数组合问题
- 半糖iOS版首页实现与基本原理揭秘
- Pycharm中调整文字大小
- Android中的Scroller类
- bzoj2654 tree 最小生成树+二分验证
- matlab 学习之数组运算
- 大神教你如何在 Linux 中启用 Shell 脚本的调试模式
- 纯CSS手里剑绘制练习优化2
- 正则表达式:字符串中连续重复字符串的识别和替换
- getter ,setter
- 剑指Offer——京东实习笔试题汇总
- HDU 2149 Public Sale(巴什博弈)