zoukankan      html  css  js  c++  java
  • 从头至尾一点点实现自己的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	
      是指当前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	
      是指当前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	
      是指当前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	
      是指当前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	
      是指当前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	
      是指当前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	
      是指当前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类似的效果,里面涉及到的知识点还不少,需好好消化,自定义控件,下次继续走起~~

  • 相关阅读:
    前台 图片上传 上传预览 调用上传服务(多张图片展示)
    正则表达式验证,只能输入数字
    点击文本框搜索,出现在下拉列表中
    keycode 锁键盘按键(只能输入数字)
    Dubbo与Zookeeper、SpringMVC整合和使用(负载均衡、容错)
    map 理解
    mybatis 关联关系查询 java
    mybatis 批量插入值的sql
    EJB
    JPA概要
  • 原文地址:https://www.cnblogs.com/webor2006/p/4677181.html
Copyright © 2011-2022 走看看