欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android Scroller大揭秘

程序员文章站 2022-06-06 17:25:39
在学习使用scroller之前,需要明白scrollto()、scrollby()方法。 一、view的scrollto()、scrollby() scrollto、s...

在学习使用scroller之前,需要明白scrollto()、scrollby()方法。

一、view的scrollto()、scrollby()

scrollto、scrollby方法是view中的,因此任何的view都可以通过这两种方法进行移动。首先要明白的是,scrollto、scrollby滑动的是view中的内容(而且还是整体滑动),而不是view本身。我们的滑动控件如srollview可以限定宽、高大小,以及在布局中的位置,但是滑动控件中的内容(或者里面的childview)可以是无限长、宽的,我们调用view的scrollto、scrollby方法,相当于是移动滑动控件中的画布canvas,然后进行重绘,屏幕上也就显示相应的内容。如下:

Android Scroller大揭秘

1、getscrollx()、getscrolly()

在学习scrollto()、scrollby()之前,先来了解一下getscrollx()、getscrolly()方法。

getscrollx()、getscrolly()得到的是偏移量,是相对自己初始位置的滑动偏移距离,只有当有scroll事件发生时,这两个方法才能有值,否则getscrollx()、getscrolly()都是初始时的值0,而不管你这个滑动控件在哪里。所谓自己初始位置是指,控件在刚开始显示时、没有滑动前的位置。以getscrollx()为例,其源码如下:

 public final int getscrollx() {
 return mscrollx;
}

可以看到getscrollx()直接返回的就是mscrollx,代表水平方向上的偏移量,getscrolly()也类似。偏移量mscrollx的正、负代表着,滑动控件中的内容相对于初始位置在水平方向上偏移情况,mscrollx为正代表着当前内容相对于初始位置向左偏移了mscrollx的距离,mscrollx为负表示当前内容相对于初始位置向右偏移了mscrollx的距离。

这里的坐标系和我们平常的认知正好相反。为了以后更方便的处理滑动相关坐标和偏移,在处理偏移、滑动相关的功能时,我们就可以把坐标反过来看,如下图:

Android Scroller大揭秘

因为滑动控件中的内容是整体进行滑动的,同时也是相对于自己显示时的初始位置的偏移,对于view中内容在偏移时的参考坐标原点(注意是内容视图的坐标原点,不是图中说的滑动控件的原点),可以选择初始位置的某一个地方,因为滑动时整体行为,在进行滑动的时候从这个选择的原点出进行分析即可。

2、scrollto()、scrollby()

scrollto(int x,int y)移动的是view中的内容,而滑动控件中的内容都是整体移动的,scrollto(int x,int y)中的参数表示view中的内容要相对于内容初始位置移动x和y的距离,即将内容移动到距离内容初始位置x和y的位置。正如前面所说,在处理偏移、滑动问题时坐标系和平常认知的坐标系是相反的。以一个例子说明scrollto():

说明:图中黄色矩形区域表示的是一个可滑动的view控件,绿色虚线矩形为滑动控件中的滑动内容。注意这里的坐标是相反的。(例子来源于:)

(1)调用scrollto(100,0)表示将view中的内容移动到距离内容初始显示位置的x=100,y=0的地方,效果如下图:

Android Scroller大揭秘

(2)调用scrollto(0,100)效果如下图:

Android Scroller大揭秘

(3)调用scrollto(100,100)效果如下图:

Android Scroller大揭秘

(4)调用scrollto(-100,0)效果如下图:

Android Scroller大揭秘

通过上面几个图,可以清楚看到scrollto的作用和滑动坐标系的关系。在实际使用中,我们一般是在ontouchevent()方法中处理滑动事件,在motionevent.action_move时调用scrollto(int x,int y)进行滑动,在调用scrollto(int x,int y)前,我们先要计算出两个参数值,即水平和垂直方向需要滑动的距离,如下:

@override 
public boolean ontouchevent(motionevent event) { 
 int y = (int) event.gety(); 
 int action = event.getaction(); 
 switch (action){ 
 case motionevent.action_down: 
 mlasty = y; 
 break; 
 case motionevent.action_move: 
 int dy = mlasty - y;//本次手势滑动了多大距离 
 int oldscrolly = getscrolly();//先计算之前已经偏移了多少距离 
 int scrolly = oldscrolly + dy;//本次需要偏移的距离=之前已经偏移的距离+本次手势滑动了多大距离 
 if(scrolly < 0){ 
 scrolly = 0; 
 } 
 if(scrolly > getheight() - mscreenheight){ 
 scrolly = getheight() - mscreenheight; 
 } 
 scrollto(getscrollx(),scrolly); 
 mlasty = y; 
 break; 
 } 
 return true; 
} 

上面在计算参数时,分为了三步。第一是,通过int dy = mlasty - y;得到本次手势在屏幕上滑动了多少距离,这里要特别注意这个相减顺序,因为这里的坐标与平常是相反的,因此,手势滑动距离是按下时的坐标mlasty - 当前的坐标y;第二是,通过oldscrolly = getscrolly();获得滑动内容之前已经距初始位置便宜了多少;第三是,计算本次需要偏移的参数int scrolly = oldscrolly + dy; 后面通过两个if条件进行了边界处理,然后调用scrollto进行滑动。调用完scrollto后,新的偏移量又重新产生了。从scrollto源码中可以看到:

public void scrollto(int x, int y) { 
 if (mscrollx != x || mscrolly != y) { 
 int oldx = mscrollx; 
 int oldy = mscrolly; 
 mscrollx = x;//赋值新的x偏移量 
 mscrolly = y;//赋值新的y偏移量 
 invalidateparentcaches(); 
 onscrollchanged(mscrollx, mscrolly, oldx, oldy); 
 if (!awakenscrollbars()) { 
 postinvalidateonanimation(); 
 } 
 } 
 } 

scrollto是相对于初始位置来进行移动的,而scrollby(int x ,int y)则是相对于上一次移动的距离来进行本次移动。scrollby其实还是依赖于scrollto的,如下源码:

public void scrollby(int x, int y) { 
 scrollto(mscrollx + x, mscrolly + y); 
 } 

可以看到,使用scrollby其实就是省略了我们在计算scrollto参数时的第三步而已,因为scrollby内部已经自己帮我加上了第三步的计算。因此scrollby的作用就是相当于在上一次的偏移情况下进行本次的偏移。

一个完整的水平方向滑动的例子:

public class myviewpager extends viewgroup { 
 private int mlastx; 
 public myviewpager(context context) { 
 super(context); 
 init(context); 
 } 
 public myviewpager(context context, attributeset attrs) { 
 super(context, attrs); 
 init(context); 
 } 
 public myviewpager(context context, attributeset attrs, int defstyleattr) { 
 super(context, attrs, defstyleattr); 
 init(context); 
 } 
 private void init(context context) { 
 } 
 @override 
 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { 
 super.onmeasure(widthmeasurespec, heightmeasurespec); 
 int count = getchildcount(); 
 for(int i = 0; i < count; i++){ 
 view child = getchildat(i); 
 child.measure(widthmeasurespec,heightmeasurespec); 
 } 
 } 
 @override 
 protected void onlayout(boolean changed, int l, int t, int r, int b) { 
 int count = getchildcount(); 
 log.d("tag","--l-->"+l+",--t-->"+t+",-->r-->"+r+",--b-->"+b); 
 for(int i = 0; i < count; i++){ 
 view child = getchildat(i); 
 child.layout(i * getwidth(), t, (i+1) * getwidth(), b); 
 } 
 } 
 @override 
 public boolean ontouchevent(motionevent ev) { 
 int x = (int) ev.getx(); 
 switch (ev.getaction()){ 
 case motionevent.action_down: 
 mlastx = x; 
 break; 
 case motionevent.action_move: 
 int dx = mlastx - x; 
 int oldscrollx = getscrollx();//原来的偏移量 
 int prescrollx = oldscrollx + dx;//本次滑动后形成的偏移量 
 if(prescrollx > (getchildcount() - 1) * getwidth()){ 
  prescrollx = (getchildcount() - 1) * getwidth(); 
 } 
 if(prescrollx < 0){ 
  prescrollx = 0; 
 } 
 scrollto(prescrollx,getscrolly()); 
 mlastx = x; 
 break; 
 } 
 return true; 
 } 
} 

布局文件:

<?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"> 

 <com.scu.lly.viewtest.view.myviewpager 
android:layout_width="match_parent" 
android:layout_height="300dp" 
> 
 <imageview 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
android:scaletype="fitxy" 
android:src="@drawable/test1" /> 

 <imageview 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
android:scaletype="fitxy" 
android:src="@drawable/test2" /> 

 <imageview 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
android:scaletype="fitxy" 
android:src="@drawable/test3" /> 

 <imageview 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
android:scaletype="fitxy" 
android:src="@drawable/test4" /> 
 </com.scu.lly.viewtest.view.myviewpager> 
</linearlayout> 

效果如图:

Android Scroller大揭秘

二、scroller滑动辅助类

根据我们上面的分析,可知view的scrollto()、scrollby()是瞬间完成的,当我们的手指在屏幕上移动时,内容会跟着手指滑动,但是当我们手指一抬起时,滑动就会停止,如果我们想要有一种惯性的滚动过程效果和回弹效果,此时就需要使用scroller辅助类。

但是注意的是,scroller本身不会去移动view,它只是一个移动计算辅助类,用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过view的scrollto、scrollby方法完成view的移动的。

在使用scroller类之前,先了解其重要的两个方法:

(1)startscroll()

public void startscroll(int startx, int starty, int dx, int dy, int duration)

开始一个动画控制,由(startx , starty)在duration时间内前进(dx,dy)个单位,即到达偏移坐标为(startx+dx , starty+dy)处。

(2)computescrolloffset()

public boolean computescrolloffset()

滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mcurrx和mcurry值中。

上面两个方法的源码如下:

public class scroller {
private int mstartx;//水平方向,滑动时的起点偏移坐标
private int mstarty;//垂直方向,滑动时的起点偏移坐标
private int mfinalx;//滑动完成后的偏移坐标,水平方向
private int mfinaly;//滑动完成后的偏移坐标,垂直方向
private int mcurrx;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,水平方向
private int mcurry;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,垂直方向
private int mduration; //本次滑动的动画时间
private float mdeltax;//滑动过程中,在达到mfinalx前还需要滑动的距离,水平方向
private float mdeltay;//滑动过程中,在达到mfinalx前还需要滑动的距离,垂直方向
public void startscroll(int startx, int starty, int dx, int dy) {
 startscroll(startx, starty, dx, dy, default_duration);
}
/**
 * 开始一个动画控制,由(startx , starty)在duration时间内前进(dx,dy)个单位,即到达偏移坐标为(startx+dx , starty+dy)处
*/
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;
}
/**
 * 滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mcurrx和mcurry值中
 * @return
*/
public boolean computescrolloffset() {
 if (mfinished) {//已经完成了本次动画控制,直接返回为false
 return false;
 }
 int timepassed = (int)(animationutils.currentanimationtimemillis() - mstarttime);
 if (timepassed < mduration) {
 switch (mmode) {
 case scroll_mode:
 final float x = minterpolator.getinterpolation(timepassed * mdurationreciprocal);
 mcurrx = mstartx + math.round(x * mdeltax);//计算出当前的滑动偏移位置,x轴
 mcurry = mstarty + math.round(x * mdeltay);//计算出当前的滑动偏移位置,y轴
 break;
 ...
 }
 }else {
 mcurrx = mfinalx;
 mcurry = mfinaly;
 mfinished = true;
 }
 return true;
 }
 ...
}

scroller类中最重要的两个方法就是startscroll()和computescrolloffset(),但是scroller类只是一个滑动计算辅助类,它的startscroll()和computescrolloffset()方法中也只是对一些轨迹参数进行设置和计算,真正需要进行滑动还是得通过view的scrollto()、scrollby()方法。为此,view中提供了computescroll()方法来控制这个滑动流程。computescroll()方法会在绘制子视图的时候进行调用。其源码如下:

/** 
 * called by a parent to request that a child update its values for mscrollx 
 * and mscrolly if necessary. this will typically be done if the child is 
 * animating a scroll using a {@link android.widget.scroller scroller} 
 * object. 
 * 由父视图调用用来请求子视图根据偏移值 mscrollx,mscrolly重新绘制 
 */ 
public void computescroll() { //空方法 ,自定义滑动功能的viewgroup必须实现方法体 
 
} 

因此scroller类的基本使用流程可以总结如下:

(1)首先通过scroller类的startscroll()开始一个滑动动画控制,里面进行了一些轨迹参数的设置和计算;

(2)在调用startscroll()的后面调用invalidate();引起视图的重绘操作,从而触发viewgroup中的computescroll()被调用;

(3)在computescroll()方法中,先调用scroller类中的computescrolloffset()方法,里面根据当前消耗时间进行轨迹坐标的计算,然后取得计算出的当前滑动的偏移坐标,调用view的scrollto()方法进行滑动控制,最后也需要调用invalidate();进行重绘。

如下的一个简单代码示例:  

@override 
 public boolean ontouchevent(motionevent ev) { 
 initvelocitytrackerifnotexists(); 
 mvelocitytracker.addmovement(ev); 
 int x = (int) ev.getx(); 
 switch (ev.getaction()){ 
 case motionevent.action_down: 
 if(!mscroller.isfinished()){ 
  mscroller.abortanimation(); 
 } 
 mlastx = x; 
 break; 
 case motionevent.action_move: 
 int dx = mlastx - x; 
 int oldscrollx = getscrollx();//原来的偏移量 
 int prescrollx = oldscrollx + dx;//本次滑动后形成的偏移量 
 if(prescrollx > (getchildcount() - 1) * getwidth()){ 
  prescrollx = (getchildcount() - 1) * getwidth(); 
 } 
 if(prescrollx < 0){ 
  prescrollx = 0; 
 } 
 //开始滑动动画 
 mscroller.startscroll(mscroller.getfinalx(),mscroller.getfinaly(),dx,0);//第一步 
 //注意,一定要进行invalidate刷新界面,触发computescroll()方法,因为单纯的startscroll()是属于scroller的,只是一个辅助类,并不会触发界面的绘制 
 invalidate(); 
 mlastx = x; 
 break; 
 } 
 return true; 
 } 
 @override 
 public void computescroll() { 
 super.computescroll(); 
 if(mscroller.computescrolloffset()){//第二步 
 scrollto(mscroller.getcurrx(),mscroller.getcurry());//第三步 
 invalidate(); 
 } 
 } 

下面是一个完整的例子:一个类似viewpager的demo,效果图如下:

Android Scroller大揭秘

代码如下:

public class myviewpager3 extends viewgroup { 

 private int mlastx; 

 private scroller mscroller; 
 private velocitytracker mvelocitytracker; 
 private int mtouchslop; 
 private int mmaxvelocity; 
 /** 
 * 当前显示的是第几个屏幕 
 */ 
 private int mcurrentpage = 0; 

 public myviewpager3(context context) { 
 super(context); 
 init(context); 
 } 

 public myviewpager3(context context, attributeset attrs) { 
 super(context, attrs); 
 init(context); 
 } 

 public myviewpager3(context context, attributeset attrs, int defstyleattr) { 
 super(context, attrs, defstyleattr); 
 init(context); 
 } 

 private void init(context context) { 
 mscroller = new scroller(context); 
 viewconfiguration config = viewconfiguration.get(context); 
 mtouchslop = config.getscaledpagingtouchslop(); 
 mmaxvelocity = config.getscaledminimumflingvelocity(); 
 } 

 @override 
 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { 
 super.onmeasure(widthmeasurespec, heightmeasurespec); 
 int count = getchildcount(); 
 for(int i = 0; i < count; i++){ 
 view child = getchildat(i); 
 child.measure(widthmeasurespec, heightmeasurespec); 
 } 
 } 

 @override 
 protected void onlayout(boolean changed, int l, int t, int r, int b) { 
 int count = getchildcount(); 
 log.d("tag","--l-->"+l+",--t-->"+t+",-->r-->"+r+",--b-->"+b); 
 for(int i = 0; i < count; i++){ 
 view child = getchildat(i); 
 child.layout(i * getwidth(), t, (i + 1) * getwidth(), b); 
 } 
 } 

 @override 
 public boolean ontouchevent(motionevent ev) { 
 initvelocitytrackerifnotexists(); 
 mvelocitytracker.addmovement(ev); 
 int x = (int) ev.getx(); 
 switch (ev.getaction()){ 
 case motionevent.action_down: 
 if(!mscroller.isfinished()){ 
  mscroller.abortanimation(); 
 } 
 mlastx = x; 
 break; 
 case motionevent.action_move: 
 int dx = mlastx - x; 
 /* 注释的里面是使用startscroll()来进行滑动的 
 int oldscrollx = getscrollx();//原来的偏移量 
 int prescrollx = oldscrollx + dx;//本次滑动后形成的偏移量 
 if (prescrollx > (getchildcount() - 1) * getwidth()) { 
  prescrollx = (getchildcount() - 1) * getwidth(); 
  dx = prescrollx - oldscrollx; 
 } 
 if (prescrollx < 0) { 
  prescrollx = 0; 
  dx = prescrollx - oldscrollx; 
 } 
 mscroller.startscroll(mscroller.getfinalx(), mscroller.getfinaly(), dx, 0); 
 //注意,使用startscroll后面一定要进行invalidate刷新界面,触发computescroll()方法,因为单纯的startscroll()是属于scroller的,只是一个辅助类,并不会触发界面的绘制 
 invalidate(); 
 */ 
 //但是一般在action_move中我们直接使用scrollto或者scrollby更加方便 
 scrollby(dx,0); 
 mlastx = x; 
 break; 
 case motionevent.action_up: 
 final velocitytracker velocitytracker = mvelocitytracker; 
 velocitytracker.computecurrentvelocity(1000); 
 int initvelocity = (int) velocitytracker.getxvelocity(); 
 if(initvelocity > mmaxvelocity && mcurrentpage > 0){//如果是快速的向右滑,则需要显示上一个屏幕 
  log.d("tag","----------------快速的向右滑--------------------"); 
  scrolltopage(mcurrentpage - 1); 
 }else if(initvelocity < -mmaxvelocity && mcurrentpage < (getchildcount() - 1)){//如果是快速向左滑动,则需要显示下一个屏幕 
  log.d("tag","----------------快速的向左滑--------------------"); 
  scrolltopage(mcurrentpage + 1); 
 }else{//不是快速滑动的情况,此时需要计算是滑动到 
  log.d("tag","----------------慢慢的滑动--------------------"); 
  slowscrolltopage(); 
 } 
 recyclevelocitytracker(); 
 break; 
 } 
 return true; 
 } 

 /** 
 * 缓慢滑动抬起手指的情形,需要判断是停留在本page还是往前、往后滑动 
 */ 
 private void slowscrolltopage() { 
 //当前的偏移位置 
 int scrollx = getscrollx(); 
 int scrolly = getscrolly(); 
 //判断是停留在本page还是往前一个page滑动或者是往后一个page滑动 
 int whichpage = (getscrollx() + getwidth() / 2 ) / getwidth() ; 
 scrolltopage(whichpage); 
 } 

 /** 
 * 滑动到指定屏幕 
 * @param indexpage 
 */ 
 private void scrolltopage(int indexpage) { 
 mcurrentpage = indexpage; 
 if(mcurrentpage > getchildcount() - 1){ 
 mcurrentpage = getchildcount() - 1; 
 } 
 //计算滑动到指定page还需要滑动的距离 
 int dx = mcurrentpage * getwidth() - getscrollx(); 
 mscroller.startscroll(getscrollx(),0,dx,0,math.abs(dx) * 2);//动画时间设置为math.abs(dx) * 2 ms 
 //记住,使用scroller类需要手动invalidate 
 invalidate(); 
 } 

 @override 
 public void computescroll() { 
 log.d("tag", "---------computescrollcomputescrollcomputescroll--------------"); 
 super.computescroll(); 
 if(mscroller.computescrolloffset()){ 
 scrollto(mscroller.getcurrx(),mscroller.getcurry()); 
 invalidate(); 
 } 
 } 

 private void recyclevelocitytracker() { 
 if (mvelocitytracker != null) { 
 mvelocitytracker.recycle(); 
 mvelocitytracker = null; 
 } 
 } 

 private void initvelocitytrackerifnotexists() { 
 if(mvelocitytracker == null){ 
 mvelocitytracker = velocitytracker.obtain(); 
 } 
 } 
} 

布局文件如下:

<?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">

 <com.lusheep.viewtest.view.myviewpager3
 android:layout_width="match_parent"
 android:layout_height="200dp"
 android:background="#999" >
 <imageview
 android:layout_width="300dp"
 android:layout_height="match_parent"
 android:scaletype="fitxy"
 android:src="@drawable/test1" />

 <imageview
 android:layout_width="300dp"
 android:layout_height="match_parent"
 android:scaletype="fitxy"
 android:src="@drawable/test2" />

 <imageview
 android:layout_width="300dp"
 android:layout_height="match_parent"
 android:scaletype="fitxy"
 android:src="@drawable/test3" />

 <imageview
 android:layout_width="300dp"
 android:layout_height="match_parent"
 android:scaletype="fitxy"
 android:src="@drawable/test4" />
 </com.lusheep.viewtest.view.myviewpager3>
</linearlayout>

点此下载

简单总结:

(1)scroller类能够帮助我们实现高级的滑动功能,如手指抬起后的惯性滑动功能。使用流程为,首先通过scroller类的startscroll()+invalidate()触发view的computescroll(),在computescroll()中让scroller类去计算最新的坐标信息,拿到最新的坐标偏移信息后还是要调用view的scrollto来实现滑动。可以看到,使用scroller的整个流程比较简单,关键的是控制滑动的一些逻辑计算,比如上面例子中的计算什么时候该往哪一页滑动...

(2)android后面推出了overscroller类,overscroller在整体功能上和scroller类似,使用也相同。overscroller类可以完全代替scroller,相比scroller,overscroller主要是增加了对滑动到边界的一些控制,如增加一些回弹效果等,功能更加强大。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!