标签:viewpager viewgroup view scrollto scrollby
转载请注明出处: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() { @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\t\r\b 指当前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); // 指定手势识别器去处理滑动事件 // 还是得自己处理一些逻辑 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的id if (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()-1 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的所有源码,接下来我们慢慢分析一下实现过程,首先是初始化各个子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();会导致这个方法的执行 */ @Override public 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后,视图的位移过程结束。
此外,在这个例子程序中我自定义了一个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自定义控件——侧滑菜单查看相关源码。
源码请在这里下载
Android自定义ViewPager(一)——自定义Scroller模拟动画过程
标签:viewpager viewgroup view scrollto scrollby
原文地址:http://blog.csdn.net/allen315410/article/details/41575831