从头至尾一点点实现自己的ViewPager效果
阅读原文时间:2023年07月13日阅读:1

对于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:

<com.example.myviewpager.MyScrollView  
    android:id="@+id/myscroll\_view"  
    android:layout\_width="match\_parent"  
    android:layout\_height="match\_parent" />

首先第一步先将六张图片添加到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:

<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" />

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:

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

它的内容预览如下:

其中为了说明一个滑动冲突的问题,这里故意弄了个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类似的效果,里面涉及到的知识点还不少,需好好消化,自定义控件,下次继续走起~~