从头至尾一点点实现自己的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,这里新建一个类用来计算位移:

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

posted on 2015-07-26 10:02  cexo  阅读(802)  评论(0编辑  收藏  举报

导航