对于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,这里新建一个类用来计算位移:

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; 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类似的效果,里面涉及到的知识点还不少,需好好消化,自定义控件,下次继续走起~~

从头至尾一点点实现自己的ViewPager效果的更多相关文章

  1. android自定义控件(5)-实现ViewPager效果

    对于系统的ViewGroup我们已经是十分熟悉了,最常用的LinearLayout和RelativeLayout几乎是天天要打交道,下面我们就来看看,如何一步一步将其实现: 一.首先当然也是最通常的新 ...

  2. Android 自定义View修炼-自定义HorizontalScrollView视图实现仿ViewPager效果

    开发过程中,需要达到 HorizontalScrollView和ViewPager的效果,于是直接重写了HorizontalScrollView来达到实现ViewPager的效果. 实际效果图如下: ...

  3. RecyclerView实现ViewPager效果

    RecyclerView实现ViewPager效果,以及横向的ListView效果.效果图如下: Github: https://github.com/hpu-spring87/recyclervie ...

  4. ViewPagerWithRecyclerDemo【RecyclerView+ViewPager实现类似TabLayout+ViewPager效果】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 使用RecyclerView+ViewPager实现类似TabLayout+ViewPager效果. 效果图 使用步骤 一.项目组织 ...

  5. 自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

    1. 这里我们继承已有ViewGroup实现自定义控件,模拟出来ViewPager的效果,如下: (1)实现的效果图如下: (2)实现步骤: • 自定义view继承viewGroup • 重写onLa ...

  6. RecyclerView实现ViewPager效果;

    看代码就好了,RecyclerView实现Viewpager的效果,添加了界面的改变监听,用法和普通的RecyclerView一样,还可以设置一次滑动多个界面: public class VpRecy ...

  7. Android重写HorizontalScrollView仿ViewPager效果

    Android提供的ViewPager类太复杂,有时候没有必要使用,所以重写一个HorizontalScrollView来实现类似的效果,也可以当做Gallery来用 思路很简单,就是重写onTouc ...

  8. Android 使用RecyclerView实现多行水平分页的GridView效果和ViewPager效果

    前些天看到有人在论坛上问这种效果怎么实现,没写过也没用过这个功能,网上查了一下,大多是使用ViewPager+GridView或者HorizontalScrollView+GridView实现,不过貌 ...

  9. Android重写HorizontalScrollView模仿ViewPager效果

    Android提供的ViewPager类太复杂,有时候没有必要使用,所以重写一个HorizontalScrollView来实现类似的效果,也可以当做Gallery来用 思路很简单,就是重写onTouc ...

随机推荐

  1. webkit浏览器下改变滚动条样式

    /*定义滚动条轨道 内阴影+圆角*/ ::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); ba ...

  2. jquery获得 当前页面url的变量

    (function($){ $.extend({ urlGet:function () { var aQuery = window.location.href.split("?") ...

  3. mvp设计模式

    一.设计模式的简单介绍 MVP的 V 层是由UIViewController 和UIView 共同组成view 将委托presenter 对它自己的操作,(简单来说就是presenter发命令来控制v ...

  4. windows下图形学视觉基本库安装不完全指南

    安装各种库东奔西走...... GLUT(英文全写:OpenGL Utility Toolkit)是一个处理OpenGL程式的工具库,负责处理和底层操作系统的呼叫以及I/O,并包括了以下常见的功能: ...

  5. 【转】spring基础:@ResponseBody,PrintWriter用法

    理解:很多情况我们需要在controller接收请求然后返回一些message. 1.在springmvc中当返回值是String时,如果不加@ResponseBody的话,返回的字符串就会找这个St ...

  6. Postman和jmeter的区别

    1.创建接口用例集(没区别) Postman是Collections,Jmeter是线程组,没什么区别. 2.步骤的实现(有区别) Postman和jmeter都是创建http请求 区别1:postm ...

  7. 剑指offer30:连续子数组的最大和

    1 题目描述 HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学.今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决.但是,如果 ...

  8. 2019CCPC网络赛——array(权值线段树)

    题目链接http://acm.hdu.edu.cn/showproblem.php?pid=6703 题目大意: 给出一个n(n<1e5)个元素的数组A,A中所有元素都是不重复的[1,n]. 有 ...

  9. Python面试题集合带答案

    目录 Python基础篇 1:为什么学习Python 2:通过什么途径学习Python 3:谈谈对Python和其他语言的区别 Python的优势: 4:简述解释型和编译型编程语言 5:Python的 ...

  10. SAS学习笔记19 SAS删除空格函数(left、right、trim、strip、compress、compbl函数)