程序员人生 网站导航

Android自定义ViewPager(一)――自定义Scroller模拟动画过程

栏目:综合技术时间:2014-12-10 08:32:22

       转载请注明出处:http://blog.csdn.net/allen315410/article/details/41575831

       相信Android SDK提供的ViewPager组件,大家实在是熟习不过了,但是ViewPager存在于support.v4包下的,说明ViewPager其实不存在于初期的android版本中,那末如何在初期的android版本中也一样使用类似于ViewPager1样的滑动效果呢?这里,我们还是继续探讨1下andrid的自定义组件好了,并且这篇博文只探讨android的1些知识,其实不是刻意去构建1个自定义的ViewPager去使用,这个是没有必要的,请将注意力集中在实现这个效果的知识点上,方便以后“举1反3”。


       好了,我们先来简单分析1下ViewPager。ViewPager可以看作是1个“容器”,在这个“容器”里可以摆放各种各样的View类型,例如ViewPager每一个分页上可以放置TextView,ImageView,ListView、GridView等等1系列View组件,实际上这些View在ViewPager上的摆放我们可以看作是在ViewGroup上Layout各种View(实际上,这个实现是比较复杂的,这里做个比喻意义而已),所以我们就能够抽象理解为,ViewPager相当于ViewGroup,并且在这个ViewGroup上Layout各种View,所以接下来的代码中,我们主要需要1个自定义的ViewGroup来实现到达这样的效果。另外,还需要在这个ViewGroup上给每一个分页上的View添加1个左右滑动的效果,以求摹拟出ViewPager上的动态效果。

       关于自定义ViewGroup的结构,我们有必要仔细探讨1下,某些概念还是值得去加深理解的,为了理解方便,请参看下面的“草图”:


         从上面的草图可以看到,红色的边框代表装备屏幕,即我们可以用肉眼看见的地方,全部灰色的大边框代表全部效果,这里称为“视图”,每一个视图又分为3个View,这个3个或多个View组成1张很大的视图。我们要弄清楚,这3者的关系,装备屏幕代表的显示区域,即我们在装备上能看见的范围,View代表的是单个的组件,1个屏幕上可以显示1个或多个View,但是视图是最容易混淆的东西,视图理论上是很大的1块区域,它不但包括装备屏幕上能被肉眼看见的1部份,还包括装备屏幕之外肉眼看不见的地方,就如上图所示的,子View2和子View3也是视图的1部份,但是在装备屏幕以外,就是肉眼看不见的区域了。视图里可以寄存很多的View,视图被用来管理View的显示效果。而且,视图是可以自由活动的,通过控制视图的活动,控制视图在装备屏幕上的显示范围,就能够切换不同的分页了。       


       所以接下来,我们主要去做的就是如何去自定义1个视图,如何让视图展现不同的View在装备屏幕上,在Android上管理多个View的显示可以通过自定义的ViewGroup,实现onLayout给View进行排版,初始化排版的时候,我1共向ViewGroup里添加了6个子View,这6个子View呈水平横向排版,如上图所示的那样,每一个View显示的宽度和高度跟父View(ViewGroup)相同,首次排版显现出第1个子View在屏幕上,其他5个子View以次添加进来,以父View的宽度的N倍数排版,都被隐藏在装备屏幕的右侧区域。下面是自定义ViewGroup的实现代码:

package com.example.myviewpager; import android.content.Context; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; public class MyViewPager extends ViewGroup { /** 手势辨认器 */ private GestureDetector detector; /** 上下文 */ private Context ctx; /** 第1次按下的X轴的坐标 */ private int firstDownX; /** 记录当前View的id */ private int currId = 0; /** 摹拟动画工具 */ private MyScroller myScroller; public MyViewPager(Context context, AttributeSet attrs) { super(context, attrs); this.ctx = context; init(); } private void init() { myScroller = new MyScroller(ctx); detector = new GestureDetector(ctx, new GestureDetector.OnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 手指滑动 scrollBy((int) distanceX, 0); return false; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } @Override public boolean onDown(MotionEvent e) { return false; } }); } /** * 对子View进行布局,肯定子View的位置 changed 若为true, * 说明布局产生了变化 l  指当前View位于父View的位置 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 指定子View的位置 ,左、上、右、下,是指在ViewGroup坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { detector.onTouchEvent(event); // 指定手势辨认器去处理滑动事件 // 还是得自己处理1些逻辑 switch (event.getAction()) { case MotionEvent.ACTION_DOWN : // 按下 firstDownX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE : // 移动 break; case MotionEvent.ACTION_UP : // 抬起 int nextId = 0; // 记录下1个View的id if (event.getX() - firstDownX > getWidth() / 2) { // 手指离开点的X轴坐标-firstDownX > 屏幕宽度的1半,左移 nextId = (currId - 1) <= 0 ? 0 : currId - 1; } else if (firstDownX - event.getX() > getWidth() / 2) { // 手指离开点的X轴坐标 - firstDownX < 屏幕宽度的1半,右移 nextId = currId + 1; } else { nextId = currId; } moveToDest(nextId); break; default : break; } return true; } /** * 控制视图的移动 * * @param nextId */ private void moveToDest(int nextId) { // nextId的公道范围是,nextId >=0 && nextId <= getChildCount()⑴ currId = (nextId >= 0) ? nextId : 0; currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); // 视图移动,太直接了,没有动态进程 // scrollTo(currId * getWidth(), 0); // 要移动的距离 = 终究的位置 - 现在的位置 int distanceX = currId * getWidth() - getScrollX(); // 设置运行的时间 myScroller.startScroll(getScrollX(), 0, distanceX, 0); // 刷新视图 invalidate(); } /** * invalidate();会致使这个方法的履行 */ @Override public void computeScroll() { if (myScroller.computeOffset()) { int newX = (int) myScroller.getCurrX(); System.out.println("newX::" + newX); scrollTo(newX, 0); invalidate(); } } }

        1,上面是自定义ViewGroup的所有源码,接下来我们渐渐分析1下实现进程,首先是初始化各个子View的排版,上面已说过了,主要代码在onLayout()方法中已体现,比较简单。


        2,实现手势滑动效果。尽人皆知,ViewPager可以随着手指在屏幕上滑动而改变不同的分页,为了实现一样的效果,我在自定义ViewGroup中重写了父类的onTouchEvent(MotionEvent event)方法,该方法被用来处理滑动事件的逻辑。但是为了简便起见,我用了手势辨认器GestureDetector,用这个手指辨认器来处理手指在屏幕上移动时,视图随着手指1起移动的效果,简单在GestureDetector的onScroll()方法中,将移动的距离传递给ScrollBy(int)作为参数便可。


        3,处理比较复杂的手指按下到抬起时,视图切换。这是1个具体分析的进程,下面是这个进程中触及的"草图":


这里,我们以子View2这个View做示例来分析1下3种情况:

(1),手指离开点的X轴坐标 - 手指按下点的X轴坐标 > 屏幕宽度的1半,左移,屏幕显示下1个View

(2),手指离开点的X轴坐标 - 手指按下点的X轴坐标 < 屏幕宽度的1半,右移,屏幕显示上1个View

(3),以上两种条件都不满足,那就停留在当前View上,不切换前后View


       4,通过(3)的进程,我们就知道当前视图向哪个View方向上移动了,得到下1个需要显示View的id,将这个id置为当前View的id,然后将下1个需要显示的View的id*View的宽度,传递给ScrollTo(int,0)作为参数,来控制视图的移动。


       5,通过以上步骤,View视图的切换就已完成了,但是有个问题,在View的左右切换时使用了ScrollTo(int,int)方法,这个方法将View直接移动到指定的位置,但是全部移动的进程太过于迅速,1瞬间就完成了View的切换,这样的体验效果非常差,那末我们怎样提升体验效果呢?对了,是在这个View的切换给1个慢速的进程,让View切换的进程缓慢或匀速的进行,这样体验效果就提生上去了,那末怎样在切换的进程中增加1个匀速的切换的效果呢?我们无妨先举下面1个小例子,方便理解:


       假设,有个人小A要走完1个100米的小路,他自己可以渐渐的走过去,用时很多,也能够1下子跑过去,用时极短,但是他想不紧不慢的匀速走完这段小路,该怎样办呢?这时候候他找来了1位工程师小B,让工程师小B在旁边帮他计算路程,小A在前进前询问1下工程师小B,接下来5秒钟,我要走多少米啊?工程师小B就开始计算出结果,并且告知小A,你先前进10米好了;当小A走完这个10米的路程时,小A又问小B,接下来5秒钟我要前进多少米的距离?小B1顿计算,告知小A前进20米好了,因而小A继续前进20米,停下来接着问小B......反复此进程,知道小A走完这100米的小路为止。


       上面的例子不难理解吧!因而,在View的切换进程中,我们也需要这样的1位“工程师”时刻计算每定时间间隔内的位移,传递给View视图,视图得到这个位移,就立马移动到相应的位置,再次要求“工程师”计算下,下1时间间隔内前进的位移,以此类推。下面,是我们自定义的1个计算位移的工具类源码:

package com.example.myviewpager; import android.content.Context; import android.os.SystemClock; /** * 计算视图偏移的工具类 * * @author Administrator * */ public class MyScroller { /** 开始时的X坐标 */ private int startX; /** 开始时的Y坐标 */ private int startY; /** X方向上要移动的距离 */ private int distanceX; /** Y方向上要移动的距离 */ private int distanceY; /** 开始的时间 */ private long startTime; /** 移动是不是结束 */ private boolean isFinish; /** 当前X轴的坐标 */ private long currX; /** 当前Y轴的坐标 */ private long currY; /** 默许的时间间隔 */ private int duration = 500; public MyScroller(Context ctx) { } /** * 开始移动 * * @param startX * 开始时的X坐标 * @param startY * 开始时的Y坐标 * @param distanceX * X方向上要移动的距离 * @param distanceY * Y方向上要移动的距离 */ public void startScroll(int startX, int startY, int distanceX, int distanceY) { this.startX = startX; this.startY = startY; this.distanceX = distanceX; this.distanceY = distanceY; this.startTime = SystemClock.uptimeMillis(); this.isFinish = false; } /** * 判断当前运行状态 * * @return */ public boolean computeOffset() { if (isFinish) { return false; } // 取得所用的时间 long passTime = SystemClock.uptimeMillis() - startTime; System.out.println("passTime::" + passTime); // 如果时间还在允许的范围内 if (passTime < duration) { currX = startX + distanceX * passTime / duration; currY = startY + distanceY * passTime / duration; } else { currX = startX + distanceX; currY = startY + distanceY; isFinish = true; } return true; } /** * 获得当前X的值 * * @return */ public long getCurrX() { return currX; } public void setCurrX(long currX) { this.currX = currX; } /** * 获得当前Y的值 * * @return */ public long getCurrY() { return currY; } public void setCurrY(long currY) { this.currY = currY; } }

分析1下,这个进程。


       当我们在计算出切换到下1个View的id时,就能够得到切换的距离了,公式:要移动的距离 = 终究的位置 - 现在的位置;得到这个移动距离以后,拿到这个距离和初始位置,告知“工程师”――工具类MyScroller,这时候候可以开始计算了,初始化代码以下:

// 要移动的距离 = 终究的位置 - 现在的位置 int distanceX = currId * getWidth() - getScrollX(); // 设置运行的时间 myScroller.startScroll(getScrollX(), 0, distanceX, 0); // 刷新视图 invalidate();
       初始化完计算工具类以后,需要刷新当前视图了,调用invalidate()方法,这个方法会经过1系列连锁反应,事实上刷新视图是个很复杂的进程,这里不讲授了,1直直到触发computeScroll()方法,此时,我们需要重写父类的computeScroll()方法,在这个方法中,完成自己的1些操作:

/** * invalidate();会致使这个方法的履行 */ @Override public void computeScroll() { if (myScroller.computeOffset()) { int newX = (int) myScroller.getCurrX(); System.out.println("newX::" + newX); scrollTo(newX, 0); invalidate(); } }

       

       在这个方法里,首先调用1下工具类计算位移的方法computeOffset()方法,该方法首先判断1下视图移动是不是完成,若完成返回false,若没有完成,先获得运动的时间间隔,如果当前运动的时间间隔在总时间间隔duration以内,那末通过时间间隔计算出这段时间间隔以后,视图实际移动到的位置,公式是:开始位置+总的距离/总的时间*本段移动时间间隔,如果当前运动的时间间隔超越了总的时间间隔,那末直接算出最后1次位置,公式:开始位置+移动距离。通过getCurrX得到本次位移的距离,即最新的位移距离,调用scrollTo(int,int)方法,移动视图到新的位置。最后再次递归调用invalidate()刷新当前视图,然后触发computeScroll()方法,继续上述步骤,直至超越规定的时间间隔,返回false后,视图的位移进程结束。


      在布局文件中这样援用:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.myviewpager.MyViewPager android:id="@+id/myviewpager" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>
     在MainActivity里需要给这个自定义的组件初始化几个View,为了方便起见,我全部初始化了ImageView,每一个ImageView设置不同的背景图片:

package com.example.myviewpager; import android.os.Bundle; import android.widget.ImageView; import android.app.Activity; public class MainActivity extends Activity { private MyViewPager myViewPager; // 图片资源 private int[] imageRes = new int[]{R.drawable.a1, R.drawable.a2, R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myViewPager = (MyViewPager) findViewById(R.id.myviewpager); ImageView view; for (int i = 0; i < imageRes.length; i++) { view = new ImageView(this); view.setBackgroundResource(imageRes[i]); myViewPager.addView(view); } } }



        另外,在这个例子程序中我自定义了1个MyScroller工具类来计算位移大小了,感觉费时费力,作为学习原理可行,但是实际开发中,可使用Android为我们提供了类似的、极为简便的Helper类,可使用这个Helper类来计算位移,这个类就是

android.widget.Scroller;

以下是Scroller类的相干方法:  

mScroller.getCurrX()    //获得mScroller当前水平转动的位置  
mScroller.getCurrY()    //获得mScroller当前竖直转动的位置  
mScroller.getFinalX()   //获得mScroller终究停止的水平位置  
mScroller.getFinalY()     //获得mScroller终究停止的竖直位置  
mScroller.setFinalX(int newX)    //设置mScroller终究停留的水平位置,没有动画效果,直接跳到目标位置  
mScroller.setFinalY(int newY)    //设置mScroller终究停留的竖直位置,没有动画效果,直接跳到目标位置  
mScroller.startScroll(int startX, int startY, int dx, int dy)   //转动,startX, startY为开始转动的位置,dx,dy为转动的偏移量  
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)    //转动,startX, startY为开始转动的位置,dx,dy为转动的偏移量, duration为完成转动的时间
mScroller.computeScrollOffset()   //返回值为boolean,true说明转动还没有完成,false说明转动已完成。这是1个很重要的方法,通常放在View.computeScroll()中,用来判断是不是转动是不是结束。

       Scroller的具体使用实践在我的前面博文中有用过,请移步Android自定义控件――侧滑菜单查看相干源码。


源码请在这里下载


------分隔线----------------------------
------分隔线----------------------------

最新技术推荐