从头至尾一点点实现自己的ViewPager效果
对于ViewPager,应该没有人在项目中没使用过它,效果非常的赞,使用也非常简单,但是如果自己来实现这样的效果,我想并非三下五除二的事了,这里涉及到怎么自定义ViewGroup了,它相比自定义View还要复杂一些,所以这次从头自尾一点点实现这样的效果来对自定义ViewGoup有深刻的认识,知其原理才能做到随心所欲,下面开始:
先预览一下要实现的效果图:
下面则新建一个工程慢慢来实现它:
首先需要用到几张效果图,这里将这些图分别放到两个文件夹中,如下:
下面新建一个自定义的ViewGroup,将会一步步实现我们需要的效果:
MyScrollView.java:
public class MyScrollView extends ViewGroup { public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { } }
其中需要实现两个必实现的方法,接下来会一点点进行填充,接下来在布局文件中进行声明:
activity_main.xml:
<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:gravity="center_horizontal" android:orientation="vertical" tools:context=".MainActivity" > <com.example.myviewpager.MyScrollView android:id="@+id/myscroll_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
首先第一步先将六张图片添加到ViewGroup中,具体的如何排版先不用管:
MainActivity.java:
public class MainActivity extends Activity { // 图片资源ID 数组 private int[] ids = new int[] { R.drawable.a1, R.drawable.a2, R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6 }; private MyScrollView myscroll_view; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myscroll_view = (MyScrollView) findViewById(R.id.myscroll_view); for (int i = 0; i < ids.length; i++) { ImageView image = new ImageView(this); image.setBackgroundResource(ids[i]); myscroll_view.addView(image); } } }
将元素添加进去之后,接下来就得对其布局进行控制,到底是怎么来显示这些图片呢?四大布局都有自己的布局规则,我们也得有我们自己的,这里就得去在MyScrollView的onLayout()做文章了,先来看下该方法:
接下来应该怎么来布局呢?有一些基础概念可以参考博文:http://www.cnblogs.com/webor2006/p/3596728.html,这里就直接把我们要布局的样子画出来:
上面是我们希望的布局效果,所以下面来实现一下:
这时来看下效果,应该就只显示第一张图,而且铺满整个屏幕,其它的图片都是在屏幕区域之外了:
下面则要实现通过的手指滑动来切换不同的图片,所以需要响应触摸事件,重写onTouchEvent方法,然后对事件进行解析,对于判断是否是移动、点击、长按等这些事件的逻辑代码几乎是一样的,所以对于这些事件的解析有必要抽象出来,所以google就提供了一个手势识别的工具类---GestureDetector,所以这次用它,可以省一些解析代码,而怎么自己来解析实际在上次的自定义滑动开机按钮上已经说明过,可参考:http://www.cnblogs.com/webor2006/p/4625461.html,下面来用它:
public class MyScrollView extends ViewGroup { private Context context; /** * 手势识别的工具类 */ private GestureDetector detector; public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; initView(); } private void initView() { detector = new GestureDetector(context, new 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) { 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; } }); } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置,一般在排列自己的位置时,基本上没啥用 */ protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 取得下标为I的子view /** * 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ // 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);//将手势的识别交由google的工具类完成了 return true; } }
接下来我们只要去实现相应的事件回调既可,大大简化工作量,首要的工作就是来响应手指的滑动,怎么让ViewGroup中的内容进行移动,这里需要用到一个新的方法:scrollBy(),直接上代码,超简单:
public class MyScrollView extends ViewGroup { private Context context; /** * 手势识别的工具类 */ private GestureDetector detector; public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; initView(); } private void initView() { detector = new GestureDetector(context, new 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) { /** * 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 disY * Y方向移动的距离 */ 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; } }); } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置,一般在排列自己的位置时,基本上没啥用 */ protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 取得下标为I的子view /** * 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ // 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);// 将手势的识别交由google的工具类完成了 return true; } }
运行看下效果:
就用一句话就实现了滑动效果,挺强大滴,在继续实现之前,来看一个细节问题,也是之前提出来的一个问题:为什么要将六张图片分两个文件夹来存放,先来对比下两个文件夹下的图片效果:
对比下原图:
发现第二张图变模糊了,这是由于第一张图a1是放在mdpi中,a5放在hdpi中:
这是为什么呢?为什么放在高分辨率里面的图片反而变模糊了?这是由于当前模拟器是mdpi分辨率的,所以a1图片直接使用,不进行压缩,所以图片是清晰的;而当使用a5这张图时,由于它是高分辨率下的图片,当使用时发现模拟器不支持这么高的,所以系统对图片进行的压缩,然后再进行使用,所以这就是为什么第二张图片模糊的原因,这个知识点在实际的开发中肯定会碰到,所以单独将图片分开存放的原因也就是为了说明这个问题,好了,回到正题。
接着再对滑动的scrollBy方法进行说明一下,先看下它的系统实现:
所以需要对scrollTo进行一个了解:
关于scrollBy与scrollTo方法的区别,http://www.cnblogs.com/webor2006/p/4625461.html也有说明,这里贴出关键点:
public void scrollTo(int x, int y)
说明:在当前视图内容偏移至(x , y)坐标处,即显示(可视)区域位于(x , y)坐标处。
方法原型为: View.java类中
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { //偏移位置发生了改变 if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; //赋新值,保存当前便宜量 mScrollY = y; //回调onScrollChanged方法 onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { invalidate(); //一般都引起重绘 } } }
public void scrollBy(int x, int y)
说明:在当前视图内容继续偏移(x , y)个单位,显示(可视)区域也跟着偏移(x,y)个单位。
方法原型为: View.java类中
/** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ // 看出原因了吧 。。 mScrollX 与 mScrollY 代表我们当前偏移的位置 , 在当前位置继续偏移(x ,y)个单位 public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
下面继续完善功能,当我们滑过屏幕一半的位置时松手则切换下一张图片,否则还是回到当前图片,效果如下:
所以还需单独对触摸事件进行进一步处理,这里一步步来实现这样的效果。
首先在这里先只对UP事件写上一句这个代码:
看下效果:
有一点点这个效果,但是还需要接着细化,做一些判断。具体代码如下:
public class MyScrollView extends ViewGroup { private Context context; /** * 手势识别的工具类 */ private GestureDetector detector; /** * 当前的ID值 显示在屏幕上的子View的下标 */ private int currId = 0; /** * down 事件时的x坐标 */ private int firstX = 0; private int firstY = 0; public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; initView(); } private void initView() { detector = new GestureDetector(context, new 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) { /** * 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 disY * Y方向移动的距离 */ scrollBy((int) distanceX, 0); /** * 将当前视图的基准点移动到某个点 坐标点 x 水平方向X坐标 Y 竖直方向Y坐标 scrollTo(x, y); */ 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; } }); } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置,一般在排列自己的位置时,基本上没啥用 */ protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 取得下标为I的子view /** * 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ // 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);// 将手势的识别交由google的工具类完成了 // 添加自己的事件解析 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: int nextId = 0; if (event.getX() - firstX > getWidth() / 2) { // 手指向右滑动,超过屏幕的1/2 // 当前的currid - 1 nextId = currId - 1; } else if (firstX - event.getX() > getWidth() / 2) { // 手指向左滑动,超过屏幕的1/2 // 当前的currid // + 1 nextId = currId + 1; } else { nextId = currId; } moveToDest(nextId); break; } return true; } /** * 移动到指定的屏幕上 * * @param nextId * 屏幕 的下标 */ public void moveToDest(int nextId) { /* * 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1 */ // 确保 currId>=0 currId = (nextId >= 0) ? nextId : 0; // 确保 currId<=getChildCount()-1 currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); scrollTo(currId * getWidth(), 0); /* * 刷新当前view onDraw()方法 的执行 */ invalidate(); } }
这时来看下效果:
现在的效果已经很接近ViewPager了,但上图中发现一个BUG,就是向右滑动第一张图时,居然不可以切换,下面来解决下:
再次运行:
BUG成功修复,现在已经可以正常的滑动切换了,但是其中还是有一些细节是需要进一步完善的,所以接下来继续进行细化,首先细化的切换的动画,如下:
而目前我们“scrollTo(currId * getWidth(), 0);”就是瞬间移动,没有任何的过渡,所以接下来要改良它,实际上要让动画平滑的过渡,可以在这段距离上多来一些scrollTo,所以先得到这段要移动的距离:
接下来,需要在这段距离中不断的进行计算并scrollTo,这里新建一个类用来计算位移:
public class MyScroller { private int startX; private int startY; private int distanceX; private int distanceY; /** * 开始执行动画的时间 */ private long startTime; /** * 判断是否正在执行动画 true 是还在运行 false 已经停止 */ private boolean isFinish; public MyScroller(Context ctx) { } /** * 开移移动 * * @param startX * 开始时的X坐标 * @param startY * 开始时的Y坐标 * @param disX * X方向 要移动的距离 * @param disY * Y方向 要移动的距离 */ public void startScroll(int startX, int startY, int disX, int disY) { this.startX = startX; this.startY = startY; this.distanceX = disX; this.distanceY = disY; this.startTime = SystemClock.uptimeMillis();// 为什么不用"System.currentTimeMillis()",因为这个值太大了,是从1970算起的, // 而SystemClock.uptimeMillis()是指开机算起,效率要大大高于前者,计算位移足够了 this.isFinish = false; } }
MyScrollView.java:
public class MyScrollView extends ViewGroup { private Context context; /** * 手势识别的工具类 */ private GestureDetector detector; /** * 当前的ID值 显示在屏幕上的子View的下标 */ private int currId = 0; /** * down 事件时的x坐标 */ private int firstX = 0; private int firstY = 0; private MyScroller myScroller; public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; initView(); } private void initView() { myScroller = new MyScroller(context); detector = new GestureDetector(context, new 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) { /** * 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 disY * Y方向移动的距离 */ scrollBy((int) distanceX, 0); /** * 将当前视图的基准点移动到某个点 坐标点 x 水平方向X坐标 Y 竖直方向Y坐标 scrollTo(x, y); */ 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; } }); } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置,一般在排列自己的位置时,基本上没啥用 */ protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 取得下标为I的子view /** * 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ // 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);// 将手势的识别交由google的工具类完成了 // 添加自己的事件解析 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: int nextId = 0; if (event.getX() - firstX > getWidth() / 2) { // 手指向右滑动,超过屏幕的1/2 // 当前的currid - 1 nextId = currId - 1; } else if (firstX - event.getX() > getWidth() / 2) { // 手指向左滑动,超过屏幕的1/2 // 当前的currid // + 1 nextId = currId + 1; } else { nextId = currId; } moveToDest(nextId); break; } return true; } /** * 移动到指定的屏幕上 * * @param nextId * 屏幕 的下标 */ public void moveToDest(int nextId) { /* * 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1 */ if (nextId < 0) nextId = 0; // 确保 currId>=0 currId = (nextId >= 0) ? nextId : 0; // 确保 currId<=getChildCount()-1 currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); // 瞬间移动 // scrollTo(currId * getWidth(), 0); int distance = currId * getWidth() - getScrollX(); // 最终的位置 - 现在的位置 = // 要移动的距离 // 设置运行的时间 myScroller.startScroll(getScrollX(), 0, distance, 0); /* * 刷新当前view onDraw()方法 的执行 */ invalidate(); } }
接下来要实现平滑的过渡,需要用到一个核心方法:computeScroll():
接下来它的实现代码如下:
MyScroller.java:
public class MyScroller { private int startX; private int startY; private int distanceX; private int distanceY; /** * 开始执行动画的时间 */ private long startTime; /** * 判断是否正在执行动画 true 是还在运行 false 已经停止 */ private boolean isFinish; /** * 默认运行的时间 毫秒值 */ private int duration = 500; /** * 当前的X值 */ private long currX; /** * 当前的Y值 */ private long currY; public long getCurrX() { return currX; } public MyScroller(Context ctx) { } /** * 开移移动 * * @param startX * 开始时的X坐标 * @param startY * 开始时的Y坐标 * @param disX * X方向 要移动的距离 * @param disY * Y方向 要移动的距离 */ public void startScroll(int startX, int startY, int disX, int disY) { this.startX = startX; this.startY = startY; this.distanceX = disX; this.distanceY = disY; this.startTime = SystemClock.uptimeMillis();// 为什么不用"System.currentTimeMillis()",因为这个值太大了,是从1970算起的, // 而SystemClock.uptimeMillis()是指开机算起,效率要大大高于前者,计算位移足够了 this.isFinish = false; } /** * 计算一下当前的运行状况 返回值: true 还在运行 false 运行结束 */ public boolean computeScrollOffset() { if (isFinish) { return false; } // 获得所用的时间 long passTime = SystemClock.uptimeMillis() - startTime; // 如果时间还在允许的范围内 if (passTime < duration) { // 当前的位置 = 开始的位置 + 移动的距离(距离 = 速度*时间) currX = startX + distanceX * passTime / duration; currY = startY + distanceY * passTime / duration; } else { currX = startX + distanceX; currY = startY + distanceY; isFinish = true; } return true; } }
以上的算法还是很容易理解,这里就不多解释,接下来运行看一下效果:
从结果中可以看到切换是慢慢过渡的,上面由于截图的原因可能看的不是很清楚,自己运行来观察就很明显,下面来打一下log,来观察一下computeScroll()方法会执行多少次:
可以发现切换由多个平移动作组成,而且这个方法还跟手机性能有关,如果手机性能好,这个方法执行的次数也更多,关于这个平滑移动的效果其实还不是太好,没用像ViewPager那样的带有加速度效果,要实现跟它一样的该怎么办呢?其实很简单,可以采用系统的android.widget.Scroller,我们为啥要自己实现MyScroller,也就是为了引出它,它的原理就跟咱们自己实现的差不多,只是系统的更加复杂,考虑的东西比较多,所以下面改用系统的来替换:
public class MyScrollView extends ViewGroup { private Context context; /** * 手势识别的工具类 */ private GestureDetector detector; /** * 当前的ID值 显示在屏幕上的子View的下标 */ private int currId = 0; /** * down 事件时的x坐标 */ private int firstX = 0; private int firstY = 0; // private MyScroller myScroller; private Scroller myScroller; public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; initView(); } private void initView() { // myScroller = new MyScroller(context); myScroller = new Scroller(context); detector = new GestureDetector(context, new 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) { /** * 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 disY * Y方向移动的距离 */ scrollBy((int) distanceX, 0); /** * 将当前视图的基准点移动到某个点 坐标点 x 水平方向X坐标 Y 竖直方向Y坐标 scrollTo(x, y); */ 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; } }); } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置,一般在排列自己的位置时,基本上没啥用 */ protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 取得下标为I的子view /** * 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ // 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);// 将手势的识别交由google的工具类完成了 // 添加自己的事件解析 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: int nextId = 0; if (event.getX() - firstX > getWidth() / 2) { // 手指向右滑动,超过屏幕的1/2 // 当前的currid - 1 nextId = currId - 1; } else if (firstX - event.getX() > getWidth() / 2) { // 手指向左滑动,超过屏幕的1/2 // 当前的currid // + 1 nextId = currId + 1; } else { nextId = currId; } moveToDest(nextId); break; } return true; } /** * 移动到指定的屏幕上 * * @param nextId * 屏幕 的下标 */ public void moveToDest(int nextId) { /* * 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1 */ if (nextId < 0) nextId = 0; // 确保 currId>=0 currId = (nextId >= 0) ? nextId : 0; // 确保 currId<=getChildCount()-1 currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); // 瞬间移动 // scrollTo(currId * getWidth(), 0); int distance = currId * getWidth() - getScrollX(); // 最终的位置 - 现在的位置 = // 要移动的距离 // 设置运行的时间 myScroller.startScroll(getScrollX(), 0, distance, 0); /* * 刷新当前view onDraw()方法 的执行 */ invalidate(); } /** * invalidate(); 会导致 computeScroll()这个方法的执行 */ @Override public void computeScroll() { if (myScroller.computeScrollOffset()) { int newX = (int) myScroller.getCurrX(); scrollTo(newX, 0); invalidate(); } } }
其它的调用跟咱们的一模一样,这时看到的效果就会跟ViewPager一样,有个加速度,由于截屏看的不是很清楚,这里就不贴了,自行运行就知道了。
接下来关于滑动切换还有一个细节需要进行处理,就是目前我们必须要滑动到屏幕中间才会进行切换,而ViewPager要比这个任性,当快速滑动而没有过屏幕中间时也会进行切换,像这样的效果该如何实现呢?对于手势的解析我们已经用过了GestureDetector这个类了,实际上快速滑动的它也已经有现成的了,我们只要去实现相应的逻辑既可,这就是这个手势工具类的方便之处,如下:
public class MyScrollView extends ViewGroup { private Context context; /** * 手势识别的工具类 */ private GestureDetector detector; /** * 当前的ID值 显示在屏幕上的子View的下标 */ private int currId = 0; /** * down 事件时的x坐标 */ private int firstX = 0; private int firstY = 0; // private MyScroller myScroller; private Scroller myScroller; /** * 判断是否发生快速滑动 */ protected boolean isFling; public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; initView(); } private void initView() { // myScroller = new MyScroller(context); myScroller = new Scroller(context); detector = new GestureDetector(context, new 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) { /** * 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 disY * Y方向移动的距离 */ scrollBy((int) distanceX, 0); /** * 将当前视图的基准点移动到某个点 坐标点 x 水平方向X坐标 Y 竖直方向Y坐标 scrollTo(x, y); */ return false; } @Override public void onLongPress(MotionEvent e) { } @Override /** * 发生快速滑动时的回调,这里主要关注velocityX,当它>0时表示向右滑动,<0时表示向左滑动 */ 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; } @Override public boolean onDown(MotionEvent e) { return false; } }); } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置,一般在排列自己的位置时,基本上没啥用 */ protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 取得下标为I的子view /** * 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ // 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);// 将手势的识别交由google的工具类完成了 // 添加自己的事件解析 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: if (!isFling) {// 在没有发生快速滑动的时候,才执行按位置判断currid int nextId = 0; if (event.getX() - firstX > getWidth() / 2) { // 手指向右滑动,超过屏幕的1/2 // 当前的currid - 1 nextId = currId - 1; } else if (firstX - event.getX() > getWidth() / 2) { // 手指向左滑动,超过屏幕的1/2 // 当前的currid // + 1 nextId = currId + 1; } else { nextId = currId; } moveToDest(nextId); } isFling = false; break; } return true; } /** * 移动到指定的屏幕上 * * @param nextId * 屏幕 的下标 */ public void moveToDest(int nextId) { /* * 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1 */ if (nextId < 0) nextId = 0; // 确保 currId>=0 currId = (nextId >= 0) ? nextId : 0; // 确保 currId<=getChildCount()-1 currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); // 瞬间移动 // scrollTo(currId * getWidth(), 0); int distance = currId * getWidth() - getScrollX(); // 最终的位置 - 现在的位置 = // 要移动的距离 // 设置运行的时间 myScroller.startScroll(getScrollX(), 0, distance, 0); /* * 刷新当前view onDraw()方法 的执行 */ invalidate(); } /** * invalidate(); 会导致 computeScroll()这个方法的执行 */ @Override public void computeScroll() { if (myScroller.computeScrollOffset()) { int newX = (int) myScroller.getCurrX(); scrollTo(newX, 0); invalidate(); } } }
这时再看下效果:
这时整个滑动效果就跟ViewPager的一模一样了,效果非常得赞,至此一个完整的滑动效果就实现了,接下来添加一些导航的效果,如:
所以先在布局中添加一个单选按钮:
activity_main.xml:
<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:gravity="center_horizontal" android:orientation="vertical" tools:context=".MainActivity" > <RadioGroup android:id="@+id/radioGroup" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > </RadioGroup> <com.example.myviewpager.MyScrollView android:id="@+id/myscroll_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
MainActivity.java:
public class MainActivity extends Activity { // 图片资源ID 数组 private int[] ids = new int[] { R.drawable.a1, R.drawable.a2, R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6 }; private MyScrollView myscroll_view; private RadioGroup radioGroup; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myscroll_view = (MyScrollView) findViewById(R.id.myscroll_view); radioGroup = (RadioGroup) findViewById(R.id.radioGroup); for (int i = 0; i < ids.length; i++) { ImageView image = new ImageView(this); image.setBackgroundResource(ids[i]); myscroll_view.addView(image); // 添加radioButton RadioButton rbtn = new RadioButton(this); rbtn.setId(i); radioGroup.addView(rbtn); if (i == 0) { rbtn.setChecked(true); } } } }
这时则需要给MyScrollView添加相应的监听事件:
public class MyScrollView extends ViewGroup { private Context context; /** * 手势识别的工具类 */ private GestureDetector detector; /** * 当前的ID值 显示在屏幕上的子View的下标 */ private int currId = 0; /** * down 事件时的x坐标 */ private int firstX = 0; private int firstY = 0; // private MyScroller myScroller; private Scroller myScroller; /** * 判断是否发生快速滑动 */ protected boolean isFling; private MyPageChangedListener pageChangedListener; public void setPageChangedListener(MyPageChangedListener pageChangedListener) { this.pageChangedListener = pageChangedListener; } public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; initView(); } private void initView() { // myScroller = new MyScroller(context); myScroller = new Scroller(context); detector = new GestureDetector(context, new 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) { /** * 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 disY * Y方向移动的距离 */ scrollBy((int) distanceX, 0); /** * 将当前视图的基准点移动到某个点 坐标点 x 水平方向X坐标 Y 竖直方向Y坐标 scrollTo(x, y); */ return false; } @Override public void onLongPress(MotionEvent e) { } @Override /** * 发生快速滑动时的回调,这里主要关注velocityX,当它>0时表示向右滑动,<0时表示向左滑动 */ 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; } @Override public boolean onDown(MotionEvent e) { return false; } }); } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置,一般在排列自己的位置时,基本上没啥用 */ protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); // 取得下标为I的子view /** * 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ // 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置 view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);// 将手势的识别交由google的工具类完成了 // 添加自己的事件解析 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: if (!isFling) {// 在没有发生快速滑动的时候,才执行按位置判断currid int nextId = 0; if (event.getX() - firstX > getWidth() / 2) { // 手指向右滑动,超过屏幕的1/2 // 当前的currid - 1 nextId = currId - 1; } else if (firstX - event.getX() > getWidth() / 2) { // 手指向左滑动,超过屏幕的1/2 // 当前的currid // + 1 nextId = currId + 1; } else { nextId = currId; } moveToDest(nextId); } isFling = false; break; } return true; } /** * 移动到指定的屏幕上 * * @param nextId * 屏幕 的下标 */ public void moveToDest(int nextId) { /* * 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1 */ if (nextId < 0) nextId = 0; // 确保 currId>=0 currId = (nextId >= 0) ? nextId : 0; // 确保 currId<=getChildCount()-1 currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); // 瞬间移动 // scrollTo(currId * getWidth(), 0); // 触发listener事件 if (pageChangedListener != null) { pageChangedListener.moveToDest(currId); } int distance = currId * getWidth() - getScrollX(); // 最终的位置 - 现在的位置 = // 要移动的距离 // 设置运行的时间 myScroller.startScroll(getScrollX(), 0, distance, 0); /* * 刷新当前view onDraw()方法 的执行 */ invalidate(); } /** * invalidate(); 会导致 computeScroll()这个方法的执行 */ @Override public void computeScroll() { if (myScroller.computeScrollOffset()) { int newX = (int) myScroller.getCurrX(); scrollTo(newX, 0); invalidate(); } } /** * 页面改时时的监听接口 */ public interface MyPageChangedListener { void moveToDest(int currid); } }
接下来则注册监听,当滑动时相应的选项按钮也会进行更新:
public class MainActivity extends Activity { // 图片资源ID 数组 private int[] ids = new int[] { R.drawable.a1, R.drawable.a2, R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6 }; private MyScrollView myscroll_view; private RadioGroup radioGroup; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myscroll_view = (MyScrollView) findViewById(R.id.myscroll_view); radioGroup = (RadioGroup) findViewById(R.id.radioGroup); for (int i = 0; i < ids.length; i++) { ImageView image = new ImageView(this); image.setBackgroundResource(ids[i]); myscroll_view.addView(image); // 添加radioButton RadioButton rbtn = new RadioButton(this); rbtn.setId(i); radioGroup.addView(rbtn); if (i == 0) { rbtn.setChecked(true); } } myscroll_view.setPageChangedListener(new MyPageChangedListener() { @Override public void moveToDest(int currid) { ((RadioButton) radioGroup.getChildAt(currid)).setChecked(true); } }); radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { myscroll_view.moveToDest(checkedId); } }); } }
这时看下效果:
这样就实现了事件的监听了,只是发现切换的速度有点快,比如我从第一个切到最后一次,希望有一个过渡,要实现它其实很简单,稍加修改一下参数既可:
/** * 移动到指定的屏幕上 * * @param nextId * 屏幕 的下标 */ public void moveToDest(int nextId) { /* * 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1 */ if (nextId < 0) nextId = 0; // 确保 currId>=0 currId = (nextId >= 0) ? nextId : 0; // 确保 currId<=getChildCount()-1 currId = (nextId <= getChildCount() - 1) ? nextId : (getChildCount() - 1); // 瞬间移动 // scrollTo(currId * getWidth(), 0); // 触发listener事件 if (pageChangedListener != null) { pageChangedListener.moveToDest(currId); } int distance = currId * getWidth() - getScrollX(); // 最终的位置 - 现在的位置 = // 要移动的距离 // myScroller.startScroll(getScrollX(), 0, distance, 0); // 设置运行的时间 myScroller .startScroll(getScrollX(), 0, distance, 0, Math.abs(distance)); /* * 刷新当前view onDraw()方法 的执行 */ invalidate(); }
这时再看效果:
这样切换就会有一定的时间过渡,上面截图效果不是很流畅,可以真实运行查看一下。
而对于ViewPager而言,每个页面的内容肯定不只是一张图片,而是可以是复杂的界面,所以接下来我们添加一个ViewGroup,准备布局:
temp.xml:
<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:background="@android:color/darker_gray" android:gravity="center_horizontal" android:orientation="vertical" tools:context=".MainActivity" > <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <ProgressBar android:id="@+id/progressBar1" style="?android:attr/progressBarStyleLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ScrollView android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="wrap_content" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large TextLarge TextLarge TextLarge TextLarge TextLarge TextLarge Text" android:textAppearance="?android:attr/textAppearanceLarge" /> </LinearLayout> </ScrollView> </LinearLayout>
它的内容预览如下:
其中为了说明一个滑动冲突的问题,这里故意弄了个ScrollView,这时添加到MyScrollView中:
public class MainActivity extends Activity { // 图片资源ID 数组 private int[] ids = new int[] { R.drawable.a1, R.drawable.a2, R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6 }; private MyScrollView myscroll_view; private RadioGroup radioGroup; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myscroll_view = (MyScrollView) findViewById(R.id.myscroll_view); radioGroup = (RadioGroup) findViewById(R.id.radioGroup); for (int i = 0; i < ids.length; i++) { ImageView image = new ImageView(this); image.setBackgroundResource(ids[i]); myscroll_view.addView(image); } myscroll_view.setPageChangedListener(new MyPageChangedListener() { @Override public void moveToDest(int currid) { ((RadioButton) radioGroup.getChildAt(currid)).setChecked(true); } }); radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { myscroll_view.moveToDest(checkedId); } }); // 给自定义viewGroup添加测试的布局 View temp = getLayoutInflater().inflate(R.layout.temp, null); myscroll_view.addView(temp, 2); for (int i = 0; i < myscroll_view.getChildCount(); i++) { //添加radioButton RadioButton rbtn = new RadioButton(this); rbtn.setId(i); radioGroup.addView(rbtn); if(i == 0){ rbtn.setChecked(true); } } } }
这时看下效果:
发现其中添加的内容只看到了一个背景,里面的内容为什么没有显示出来呢?这是由于里面的内容没有计算大小,所以这里涉及到ViewGroup的另外一个重要方法:onMeasure(),这个方法在自定义View中有接触过,具体写法如下:
这里再看下我们添加的ViewGroup内容有没有显示出来:
这是为啥呢?实际上ViewGroup不单只是测量自己的大小,还得测量它子View的大小:
但是为啥没添加ViewGroup之前,添加的几个ImageView却能正常显示呢?ViewGroup也没有重写onMeasure方法呀,原因是由于在onLayout中强行指定了位置:
说到这两个方法,需要谈一下view.getMeasuredWidth()和view.getWidth()了:
说到view.getWidth()方法,在实际开发中可能经常会碰到在onCreate()去获得View.getWdith()=0的情况,原因就是如此,因为该view还没有执行onLayout方法确定位置,通过查看这个方法的源码也很容易理解:
另外还需解释一下onMeasure方法中的参数:
只拿widhMeasureSpec来进行说明,由于这是一个整型,总共有32位,而在测量时这个数值肯定是用不完的,所以android工程师将这个数表示了多层函义:
而上面这个规则则就是在super.onMeasure来指定的,看源码如下:
而这时看下MeasureSpec.getSize()和MeasureSpec.getMode的源码实现,就是位操作:
现在添加的ViewGroup内容正常的显示出来了,但是还存在一个问题:
其中用ScrollView包裹的内容上下可以滑动,但是左右没法切换,这就是ScrollView与触摸事件冲突的问题了,这个在实际开发中也是经常会碰到的,接下来解决它:
对于触摸事件我们已经用了onTouchEvent(),接下来先重写另外一个相关的事件:
这时将它返回值改为true:
这时直观看一下这时的效果:
这时发现新添加的ViewGroup不支持上下滑动了,而且界面中的Button也不响应点击事件了,这里就涉及到Android的事件传递机制了,理解好它也就很容易的解决滑动冲突问题了,如下图:
这时如果点击Button,它的整个事件传递机制会是如下:
a、首先ViewGroup A先收到这个事件,然后遍历它里面的子View,也就是ViewGroup B、ViewGroup C;
b、接着判断当前的触摸的区域是在B上面还是在C上面,经过判断是在C上面,接着把事件交给ViewGroup C进行处理;
c、同理,ViewGroup C里面也有两个孩子,也就是ViewGroup D、ViewGroup E,最终把事件会交给ViewGroup D处理;
d、最终事件会到达Button,然后由它消费掉;
以上是一个大致的事件传递机制,关于这些网上有大量的文章进行介绍,下面用一张图对其进行描述:
而默认情况下是会一级级往下传递事件,但是事件是可以中断掉的,也就是onInterceptTouchEvent()这个方法,传不传给下一个由它来决定,上面当它返回true的时候,则自定义的ViewGroup就收不到事件了,所以里面的按钮,ScrollView的滑动事件都无法响应了;而如果返回false,则事件会一级级传递下去,最终会传递到自定义的ViewGroup,这时就不会响应MyScrollView的触摸事件了,所以就造成了可以上下滑,而不能左右滑了。用一个图来将事件的传递机制描述一下:
理解了事件传递机制之后,解决ScrollView的滑动冲突就比较简单了,如果检测当前的手势是上下滑的,则不拦截事件,由本身ViewGroup来处理;如果是左右滑动时,则拦截事件,由我们自己的MyScrollView来处理事件,具体代码如下:
运行看下效果:
这样就成功的解决了滑动冲突,但是目前程序还存在一个BUG,就是滑动的时候会跳动:
这是为什么呢?这个BUG隐藏的很深,先来打LOG来分析一下:
/** * 是否中断事件的传递 * 返回true的时候中断事件,执行自己的onTouchEvent方法 * 返回false的时候,默认处理,不中断,也不会执行自己的onTouchEvent方法 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean result = false; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: Log.d("cexo", "onInterceptTouchEvent ACTION_DOWN"); firstX = (int) ev.getX(); firstY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: Log.d("cexo", "onInterceptTouchEvent ACTION_MOVE"); // 手指在屏幕上水平移的绝对值 int disX = (int) Math.abs(ev.getX() - firstX); // 手指在屏幕上竖直移的绝对值 int disY = (int) Math.abs(ev.getY() - firstY); if (disX > disY && disX > 10)// disX > 10是为了防止手指抖动,需要满足一定距离才可以 result = true; else result = false; break; case MotionEvent.ACTION_UP: Log.d("cexo", "onInterceptTouchEvent ACTION_UP"); break; } return result; } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); detector.onTouchEvent(event);// 将手势的识别交由google的工具类完成了 // 添加自己的事件解析 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.d("cexo", "onTouchEvent ACTION_DOWN"); firstX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: Log.d("cexo", "onTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.d("cexo", "onTouchEvent ACTION_UP"); if (!isFling) {// 在没有发生快速滑动的时候,才执行按位置判断currid int nextId = 0; if (event.getX() - firstX > getWidth() / 2) { // 手指向右滑动,超过屏幕的1/2 // 当前的currid - 1 nextId = currId - 1; } else if (firstX - event.getX() > getWidth() / 2) { // 手指向左滑动,超过屏幕的1/2 // 当前的currid // + 1 nextId = currId + 1; } else { nextId = currId; } moveToDest(nextId); } isFling = false; break; } return true; }
运行看日志:
这样肯定在滑动监听时就会出现逻辑问题,如下:
所以解决这个BUG的代码如下:
再编译运行:
至此,这里就一步步实现了跟ViewPager类似的效果,里面涉及到的知识点还不少,需好好消化,自定义控件,下次继续走起~~