转载请注明出处:http://blog.csdn.net/allen315410/article/details/41628301
上篇文章介绍了自定义ViewPager的简单实现,完成了模拟动画效果的实现,本篇将继续接着上篇的内容进行扩展,阅读本篇前请确保浏览过上篇文章,地址是Android自定义ViewPager(一)——自定义Scroller模拟动画过程。开始之前,先想想要从哪几个方面进行优化呢?先参考一下ViewPager吧!
1,解决在页面上快速滑动的处理。当手指在某个页面上快速滑动的时候,显然这种剧烈的滑动最好不要根据水平位移来决断下一个页面是哪个了,只要手指在页面上“快速”滑动了,我们就立即认为这是一种转换页面的行为,立即让页面跳转。关于页面滑动的逻辑,在上一篇博文中可以看到我们使用的是android封装好的手势识别器GestureDetector处理,在这个GestureDetector类下有个onFling(MotionEvent e1, MotionEvent e2,float velocityX, float velocityY)方法,在这个方法中处理手指“快速”滑动的逻辑。
/** 标记是否快速的滑动 */ private boolean isFling;
@Override public boolean onFling(MotionEvent e1, MotionEvent e2,float velocityX, float velocityY) { isFling = true; if (velocityX > 0 && currId > 0) { // 快速向右滑动 currId--; } else if (velocityX < 0 && currId < getChildCount() - 1) {// 快速向左滑动 currId++; } moveToDest(currId); return false; }解决完“快速”滑动的逻辑之后,就是要处理手指抬起的逻辑,上面定义了boolean isFling用于标记当前操作是否是快速滑动,若是快速滑动,那么我们就不要按照位移来判断页面的切换了,直接走快速滑动的逻辑好了。下面代码修改为:
case MotionEvent.ACTION_UP : // 抬起 if (!isFling) { // 没有快速滑动的情况下,我们按照距离来判断 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); } isFling = false; break;
2,给自定义的ViewPager添加一个分页指示器,让用户通过页面的切换,知道当前滑动了第几个页面,或者通过点击“分页指示器”来直接滑动到指定的分页上去,这个简单起见,我添加RadioGroup配合RadioButton来使用了,在布局文件中如下:
<LinearLayout 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" android:orientation="vertical" > <RadioGroup android:id="@+id/radiogroup" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > </RadioGroup> <com.example.myviewpager.MyViewPager android:id="@+id/myviewpager" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>接下来需要在初始化分页的时候,也同样需要初始化“分页指示器”RadioGroup,就是通过动态的添加RadioButton来实现的:
for (int i = 0; i < imageRes.length; i++) { ... RadioButton rbtn = new RadioButton(this); rbtn.setId(i); radiogroup.addView(rbtn); if (i == 0) { rbtn.setChecked(true); }
这时候页面都初始化完毕了,下面需要做一些更重要的操作——为自定义ViewPager添加回调函数,该回调函数用于返回当前页面的id。自定义ViewPager获取到当前的页面的id后,将根据id设置“页面指示器”指示在哪个RadioButton上,关于回调的写法如下:
/** * 页面改变监听对象 */ private PageChangedListener listener; /** * 对外提供设置监听的方法 * * @param listener */ public void setOnPageChangeListener(PageChangedListener listener) { this.listener = listener; } /** * 页面改变时回调此接口 */ public interface PageChangedListener { /** * 移动到指定的位置 * * @param currId */ void moveToDest(int currId); }定义好回调函数之后,就需要在MoveToDest(int)这个管理页面跳转的方法中,调用这个回调函数,将当前的页面id当作参数返回给调用者:
/** * 控制视图的移动 * * @param nextId */ public void moveToDest(int nextId) { // nextId的合理范围是,nextId >=0 && nextId <= getChildCount()-1 currId = (nextId >= 0) ? nextId : 0; currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); // 触发监听的方法 if (listener != null) { listener.moveToDest(currId); } // 要移动的距离 = 最终的位置 - 现在的位置 int distanceX = currId * getWidth() - getScrollX(); // 设置运行的时间 myScroller.startScroll(getScrollX(), 0, distanceX, 0); // 刷新视图 invalidate(); }在Activity中给自定义ViewPager设置监听,从回调函数中拿到页面的id值,根据id值设置相关的RadioButton的状态。
// 给自定义ViewPager设置监听 myViewPager.setOnPageChangeListener(new PageChangedListener() { @Override public void moveToDest(int currId) { // TODO Auto-generated method stub ((RadioButton) radiogroup.getChildAt(currId)).setChecked(true); } }); // 给RadioGroup设置监听 radiogroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { // TODO Auto-generated method stub myViewPager.moveToDest(checkedId); } });
3,给自定义的ViewPager添加自己的View。上面例子中为了简单方便,我一共使用了6个ImageView这种简单的组件,万一我们需要在自定义ViewPager中添加一些其他的View,例如ListView、ScrollView等等,会怎么样呢?下面添加一个ScrollView页面到自定义的ViewPager中看看效果:
首先需要一个页面的布局,这个布局就是一个ScrollView,宽高都是“填充父元素”,很简单,补贴代码了。布局有了之后,就是需要Activity里动态的添加到自定义ViewPager中
View tempView = LayoutInflater.from(this).inflate(R.layout.temp, null); // 将View添加到自定义ViewPager中 myViewPager.addView(tempView, 2);添加完之后,如果运行之后会发现一个问题,那就是在自定义ViewPager切换到这个ScrollView的页面时,发现页面一片空白,没有看见ScrollView已经ScrollView包含的一系列子元素控件,这个该怎么解决。导致这个现象的原因就是,自定义ViewPager动态添加ScrollView之后并没有对ScrollView进行测量,而是简单的将包含ScrollView的父元素LinearLayout进行了排版,所以在LinearLayout里看不到其子元素ScrollView,为了解决这个问题,我们必须的手动测量一下ScrollView的宽高,操作是在自定义的MyViewPager类下复写ViewGroup的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub super.onMeasure(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); v.measure(widthMeasureSpec, heightMeasureSpec); } }这样的一个操作之后,我们就可以看见ScrollView了。
看看上面的动态图,可以看到我把另外一个布局ScrollView添加进来了,并且ScrollView是可见的,上下滑动ScrollView没有问题的,但是问题是左右滑动就不行了,理论上左右滑动时,ViewPager应该是页面切换啊,怎么在这里就不可以了呢?解决这个问题得明白android下的时间分发机制的原理,了解事件分发机制的流程,具体是怎样的,我在这里不赘述了,请参考Android自定义控件——侧滑菜单,这里有一些相关介绍,看明白之后就很好解决了,下面是我的解决方法:
// 第一次手指按下的X轴坐标 private int firstX = 0; // 第一次手指按下的Y轴坐标 private int firstY = 0; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // TODO Auto-generated method stub boolean result = false; //标记方法返回值 switch (ev.getAction()) { case MotionEvent.ACTION_DOWN : // 手指抬起 // 让手势识别器记录按下事件,防止左右滑动页面跳动的“bug” detector.onTouchEvent(ev); firstX = (int) ev.getX(); firstY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE :// 手指移动 // 手指横向移动的位移绝对值 int diffX = (int) Math.abs(ev.getX() - firstX); // 手指竖直移动的位移绝对值 int diffY = (int) Math.abs(ev.getY() - firstY); if (diffX > diffY && diffX > 10) { result = true; } else { result = false; } break; case MotionEvent.ACTION_UP : // 手指抬起 break; default : break; } return result; }原理是这样的,在onInterceptTouchEvent这个处理是否中断事件传递的方法中,记录下每次按下时XY轴的坐标,每次在屏幕上滑动时,分别计算一下XY轴位移,如果X轴上的位移大于Y轴上的位移并且X轴位移大于10px,我们就认为是手指是左右滑动,该方法返回true表示中断事件,反之则认为是上下滑动,不处理事件,交给ViewGroup处理。好了,下面是新的运行效果。
源码请在这里下载