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

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


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

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


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


所以接下来,我们主要去做的就是如何去自定义一个视图,如何让视图展示不同的View在设备屏幕上,在Android上管理多个View的显示可以通过自定义的ViewGroup,实现onLayout给View进行排版,初始化排版的时候,我一共向ViewGroup里添加了6个子View,这6个子View呈水平横向排版,如上图所示的那样,每个View显示的宽度和高度跟父View(ViewGroup)相同,首次排版呈现出第一个子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;/** 第一次按下的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() {@Overridepublic boolean onSingleTapUp(MotionEvent e) {return false;}@Overridepublic void onShowPress(MotionEvent e) {}@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY) {// 手指滑动scrollBy((int) distanceX, 0);return false;}@Overridepublic void onLongPress(MotionEvent e) {}@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2,float velocityX, float velocityY) {return false;}@Overridepublic boolean onDown(MotionEvent e) {return false;}});}/** * 对子View进行布局,确定子View的位置 changed 若为true, * 说明布局发生了变化 l\t\r\b 指当前View位于父View的位置 */@Overrideprotected 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());}}@Overridepublic boolean onTouchEvent(MotionEvent event) {detector.onTouchEvent(event); // 指定手势识别器去处理滑动事件// 还是得自己处理一些逻辑switch (event.getAction()) {case MotionEvent.ACTION_DOWN : // 按下firstDownX = (int) event.getX();break;case MotionEvent.ACTION_MOVE : // 移动break;case MotionEvent.ACTION_UP : // 抬起int nextId = 0; // 记录下一个View的idif (event.getX() - firstDownX > getWidth() / 2) {// 手指离开点的X轴坐标-firstDownX > 屏幕宽度的一半,左移nextId = (currId - 1) <= 0 ? 0 : currId - 1;} else if (firstDownX - event.getX() > getWidth() / 2) {// 手指离开点的X轴坐标 - firstDownX < 屏幕宽度的一半,右移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()-1currId = (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();会导致这个方法的执行 */@Overridepublic void computeScroll() {if (myScroller.computeOffset()) {int newX = (int) myScroller.getCurrX();System.out.println("newX::" + newX);scrollTo(newX, 0);invalidate();}}}

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


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


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


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

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

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

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


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


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


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


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

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;}}

分析一下,这个过程。


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

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

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

在这个方法里,首先调用一下工具类计算位移的方法computeOffset()方法,该方法首先判断一下视图移动是否完成,若完成返回false,若没有完成,先获取运动的时间间隔,如果当前运动的时间间隔在总时间间隔duration之内,那么通过时间间隔计算出这段时间间隔之后,视图实际移动到的位置,公式是:开始位置+总的距离/总的时间*本段移动时间间隔,如果当前运动的时间间隔超出了总的时间间隔,那么直接算出最后一次位置,公式:开始位置+移动距离。通过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};@Overrideprotected 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);}}}



此外,在这个例子程序中我自定义了一个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说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。

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


源码请在这里下载


更多相关文章

  1. Android解析HTML+android爬虫框架jsoup
  2. Android(安卓)ART invoke 代码生成
  3. android 判断字符串是否为空的最优方法
  4. 使用线程执行堆栈StackTraceElement设计Android日志模块
  5. android APP字体大小,不随系统的字体大小变化而变化的方法
  6. Android实现判断手机未接来电及处理方法
  7. android中webview接收favicon总是获取为null解决方案
  8. 【笔记】Android上ROS开发——android_core创建一个android应用
  9. Android(安卓)编译时注解 —— 语法详解

随机推荐

  1. 《Android之大话设计模式》--设计模式 创
  2. Android(安卓)“退一步”的布局加载优化
  3. android 无法生成R文件的原因剖析
  4. 【Android】Handler使用入门
  5. 火线扫描Android静态代码
  6. Android(安卓)为何比 iOS 卡?
  7. android中json解析及使用(上)
  8. Android中使用MD5对密码进行加密
  9. 使用Jenkins进行Android自动打包
  10. [Android]如何做一个崩溃率少于千分之三