Android进阶之自定义ViewGroup—带你一步步轻松实现ViewPager

本文导语:

ViewPager相信读者们都用得很多了,在项目中的使用场景可以说是相当的多了,例如:
(1)项目框架的搭建,可以使用ViewPager+Fragment
(2)App引导页
(3)banner轮播图
(4)多张图片的浏览等等
可能根据不同的需求,还有其他的一些使用场景,在这里就不逐一列举了。今天就带大家一起来手写实现一下ViewPager的基本功能,不用畏惧,灰常简单。千万不要认为重复造轮子是没有意义的,可能写了最后也是用系统的,但是我们的目的主要是学习其中的思想和解决问题的思路。

学习本篇文章你能收获到:

1、自定义ViewGroup的基本流程
2、手势识别器和Scroller的使用
3、自定义实现ViewPager
4、给原生ViewPager添加指示器和给自定义的ViewPager添加指示器
5、处理ViewPager中的ListView和ScrollView的滑动冲突

《一》了解一下ViewGroup和View:

1、ViewGroup相当于一个放置View的容器,主要负责给childView计算出建议的宽高和测量模式;决定childView的位置。主要用到的方法有:

  • onMesure() ——计算childView的测量值以及模式,以及设置自己的宽和高。
  • onLayout()——通过getChildCount()获取子view数量,getChildAt获取所有子View,分别调用layout(int l, int t, int r, int b)确定每个子View的摆放位置。
  • onSizeChanged——在onMeasure()后执行,只有大小发生了变化才会执行onSizeChange。
  • onDraw——默认不会触发,需要手动触发。

2、View的职责:是根据测量模式和ViewGroup给出的建议的宽和高,在ViewGroup为其指定的区域内绘制出自己的形态。一般常用的方法有:

  • onMesure()
  • onDraw()
《二》自定义ViewPager实现步骤:

先看一下最终实现的MyViewPager的效果图:


final

1、创建一个MyViewPager extends ViewGroup,为该自定的ViewGroup添加几个childView。

ublic class MyViewPager extends ViewGroup {
    private Context mContext;
    private int[] images = {R.mipmap.bg_guide_one, R.mipmap.bg_guide_two, R.mipmap.bg_guide_three, R.mipmap.bg_guide_four};

    public MyViewPager(Context context) {
        super(context);
        this.mContext = context;
        init();
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        init();
    }

    public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        init();
    }

    private void init() {
        for (int i = 0; i < images.length; i++) {
            ImageView iv = new ImageView(getContext());
            iv.setBackgroundResource(images[i]);
            this.addView(iv);
        }
    }
}

2、重写onLayout()方法,获取所有的子View,各自调用layout()方法,按下图排列方式,确定它们各自的摆放位置。


示意图.png
  @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
        }
    }

3、到此,我们已经处理好了子View的摆放位置,接下来就是处理如何让ViewGroup中的元素,跟着手的滑动而滑动了。view可以通过onTouch事件来获取基本的触摸操作,但是对于较为复杂的手势,则需要手势识别器Gesturedetector来实现,在此,我们使用它来处理滑动事件。

(1) 创建一个手势识别器:这里主要就是靠 scrollBy()方法,来实现View跟随手的滑动而滑动。

  mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                //相对滑动:X方向滑动多少距离,view就跟着滑动多少距离
                scrollBy((int) distanceX, 0);
                return super.onScroll(e1, e2, distanceX, distanceY);
            }
        });

(2)重写onTouchEvent(),将触摸事件传递给手势识别器处理,并返回true,让该控件消费该事件。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
      
        return true;
    }

到此步,运行的效果如下:


image1.gif

可以看到,现在View已经可以跟随我们的手势滑动了,但离我们预期的效果,还差两个小问题待解决:边界情况的处理和平滑的回弹到指定位置。

(3)边界情况的处理。
我们期望的效果是:手指松开时,当滑动偏移的距离超出图片1/2时,自动切换到下个图片;小于1/2,回弹到初始位置。这里我们需要在onTouchEvent()中处理触摸事件,具体代码实现如下:

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
                scrollX = getScrollX();//相对于初始位置滑动的距离
                //你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
                position = (getScrollX() + getWidth() / 2) / getWidth();
                //滑到最后一张的时候,不能出边界
                if (position >= images.length) {
                    position = images.length - 1;
                }
                if (position < 0) {
                    position = 0;
                }
                break;
            case MotionEvent.ACTION_UP:

                //绝对滑动,直接滑到指定的x,y的位置,较迟钝
             scrollTo(position * getWidth(), 0);
              
                break;
        }
        return true;
    }

这里暂时我们使用的scrollTo(int x,int y)这个方法:让它到某个临界值时,滑动到指定位置,由于它是让view直接滚动到参数x和y所标定的坐标,可以看到下面的运行效果很迟钝。


image2.gif

(4) 如何实现平滑的回弹到指定位置呢?这里就要用到Scroller这个类了。
Android里Scroller类是为了实现View平滑滚动的一个Helper类。通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。设置mScroller滚动的位置时,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动。

Scroller mScroller = new Scroller(mContext);

在onTouchEvent()中的up事件中将scrollTo()方法替换为:mScroller.startScroll();

    @Override
    public boolean onTouchEvent(MotionEvent event) {
          //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
                scrollX = getScrollX();//相对于初始位置滑动的距离
                //你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
                position = (getScrollX() + getWidth() / 2) / getWidth();
                //屏蔽边界值:postion在0~images.length-1范围内
                if (position >= images.length) {
                    position = images.length - 1;
                }
                if (position < 0) {
                    position = 0;
                }
                break;
            case MotionEvent.ACTION_UP:
                //scrollTo(position * getWidth(), 0);
                //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量
                mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
                invalidate();//使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法
                break;
        }
    }

其实Scroller的原理就是用ScrollTo()来一段一段的进行,最后看上去跟自然的一样,必须使用postInvalidate(),这样才会一直回调computeScroll()这个方法,直到滑动结束。

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            Log.e("CurrX", "mScroller.getCurrX()=" + mScroller.getCurrX());
            postInvalidate();
        }
    }

基本上ViewPager的效果就出来了,看下效果图:


image3.gif
《三》给ViewPager添加指示器

看一下实现的效果:指示器在右下角,这里处理成了让指示器的小点点,随着滑动的等比距离移动,当然也可以简单的处理成滑动到某个位置后,再移动小点点,这里只是提供一个添加指示器的思路。


image5.gif

我们模仿系统的ViewPager,写一个接口,将滑动事件的偏移距离比和当前滑动到哪个页面的position提供出去。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
                scrollX = getScrollX();//相对于初始位置滑动的距离
                //你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
                position = (getScrollX() + getWidth() / 2) / getWidth();
                //屏蔽边界值:postion在0~images.length-1范围内
                if (position >= images.length) {
                    position = images.length - 1 + 1;
                }
                if (position < 0) {
                    position = 0;
                }

                if (mOnPageScrollListener != null) {
                    Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / ((1) * getWidth())));
                    mOnPageScrollListener.onPageScrolled((float) (getScrollX() * 1.0 / (getWidth())), position);
                }
                break;
            case MotionEvent.ACTION_UP:
                mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
                invalidate();//使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法

                if (mOnPageScrollListener != null) {
                    mOnPageScrollListener.onPageSelected(position);
                }
                break;
        }
        return true;
    }

    /**
     * 其实Scroller的原理就是用ScrollTo来一段一段的进行,最后看上去跟自然的一样,必须使用postInvalidate,
     * 这样才会一直回调computeScroll这个方法,直到滑动结束。
     */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            Log.e("CurrX", "mScroller.getCurrX()=" + mScroller.getCurrX());
            postInvalidate();
            if (mOnPageScrollListener != null) {
                Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / (getWidth())));
                mOnPageScrollListener.onPageScrolled((float) (mScroller.getCurrX() * 1.0 / ((1) * getWidth())), position);
            }
        }
    }
    public interface OnPageScrollListener {
        /**
         * @param offsetPercent offsetPercent:getScrollX滑动的距离占屏幕宽度的百分比
         * @param position
         */
        void onPageScrolled(float offsetPercent, int position);

        void onPageSelected(int position);
    }

    private OnPageScrollListener mOnPageScrollListener;

    public void setOnPageScrollListener(OnPageScrollListener onPageScrollListener) {
        this.mOnPageScrollListener = onPageScrollListener;
    }
}

Activity中布局中,我们在ViewPager上面放一个LinearLayout,通过addView()动态添加小点点。

public class TestActivity extends AppCompatActivity {
    private MyViewPager myviewpager;
          
    private LinearLayout llPointList;
    private List<Integer> mData = new ArrayList<>();
    private LinearLayout.LayoutParams params;
    private View viewDot;
    private int dotDistance = 30;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        myviewpager = findViewById(R.id.myviewpager);
        viewDot = findViewById(R.id.view_dot);
        llPointList = findViewById(R.id.ll_point_list);
        initCirclePoint();

        myviewpager.setOnPageScrollListener(new MyViewPager.OnPageScrollListener() {
            @Override
            public void onPageScrolled(float offsetPercent, int position) {
                //效果一:滑动页面过程中小圆点跟随移动
                //offsetPercent:0-0.5-1-1.5-...
                float leftMargin = offsetPercent * dotDistance;
                //如果使用系统的ViewPager也可以使用这种方法添加指示器,只需修改成如下即可: 
                //float leftMargin = positionOffset * dotDistance + position * dotDistance;
                FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewDot.getLayoutParams();
                params.leftMargin = (int) leftMargin; //滑动后更新距离
//                Elog.e("Offset", "params.leftMargin=" + params.leftMargin);
                viewDot.setLayoutParams(params);
            }

            @Override
            public void onPageSelected(int position) {
                 //效果二:滑动页面过程中小圆点不跟随移动,到某个指定位置才切换小圆点
                 Log.e("TAG", "position=" + position);
//                float leftMargin = position * dotDistance;
//                FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewDot.getLayoutParams();
//                params.leftMargin = (int) leftMargin; //滑动后更新距离
////                Elog.e("Offset", "params.leftMargin=" + params.leftMargin);
//                viewDot.setLayoutParams(params);

            }
        });
    }

    private void initCirclePoint() {
        for (int i = 0; i < 4; i++) {
            mData.add(i);
        }
        for (int i = 0; i < mData.size(); i++) {
            View point = new View(this);
            point.setBackgroundResource(R.drawable.bg_point_selector);
            params = new LinearLayout.LayoutParams(20, 20);
            if (i != 0) {
                params.leftMargin = 10;
            }
            point.setEnabled(false);
            point.setLayoutParams(params);
            llPointList.addView(point);
        }
    }
}

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 <com.example.jojo.learn.customview.MyViewPager
        android:id="@+id/myviewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.jojo.learn.customview.MyViewPager>


    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_marginBottom="64px"
        android:layout_marginRight="30px">

        <LinearLayout
            android:id="@+id/ll_point_list"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="30dp"
            android:layout_marginRight="40dp"
            android:orientation="horizontal"></LinearLayout>

        <View
            android:id="@+id/view_dot"
            android:layout_width="20px"
            android:layout_height="20px"
            android:background="@drawable/bg_shape_white_point"></View>
    </FrameLayout>
</RelativeLayout>

bg_shape_white_point.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="@color/color_a9c6d6"></solid>
</shape>

bg_shape_grey_point.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="@color/color_918f8e"></solid>
</shape>

bg_point_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/bg_shape_white_point" android:state_enabled="true"/>
    <item android:drawable="@drawable/bg_shape_grey_point" android:state_enabled="false"/>
</selector>
《四》滑动冲突的处理(在这里以ScrollView为例讲解滑动冲突,ListView的处理方式跟其一样)

往MyViewPager添加一个ScrollView的子view,看如下图,会发现上下能滑动,左右滑动失效了。


image4.gif
滑动冲突原因分析:

MyViewPager是左右滑动,子View(ScrollView)是上下滑动。事件传递的过程中,如果父View无拦截无消耗,那么当事件传递到子View时,默认会被子View(ScrollView)消费,那么事件在ScrollView中就传递结束了,所以父View(MyViewPager)的左右滑动就失效了。

解决冲突的办法:

就是重写父View的onInterceptTouchEvent()事件,在合适的时候,拦截该事件。

onInterceptTouchEvent()方法返回值的含义:

1、 如果return true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
2、 如果return false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;

根据我们的期望的效果:左右滑动时,让父View消费该事件;上下滑动时,直接放行,让子View(ScrollView)自己处理。代码如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 如果左右滑动, 就需要拦截, 上下滑动,不需要拦截
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) ev.getX();
                startY = (int) ev.getY();
                //这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件还是要给手势识别器处理,否则会丢失事件,滑动的时候会存在bug。
                mGestureDetector.onTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                int endX = (int) ev.getX();
                int endY = (int) ev.getY();
                int dx = endX - startX;
                int dy = endY - startY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    // 左右滑动
                    return true;// 中断事件传递, 不允许孩子响应事件了, 由父控件处理
                }
                break;
            default:
                break;
        }
        return false;// 不拦截事件,优先传递给孩子(也就是ScrollView,让它正常处理上下滑动事件)处理

    }
《五》MyViewPager完整代码:
/**
 * Created by JoJo on 2018/8/14.
 * wechat:18510829974
 * description:自定义ViewPager
 */

public class MyViewPager extends ViewGroup {
    private Context mContext;
    private int[] images = {R.mipmap.bg_subject_detail_default, R.mipmap.bg_subject_default, R.mipmap.bg_guide_one, R.mipmap.bg_guide_two};
    private GestureDetector mGestureDetector;
    private Scroller mScroller;
    private int position;

    private int scrollX;
    private int startX;
    private int startY;

    public MyViewPager(Context context) {
        super(context);
        this.mContext = context;
        init();
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        init();
    }

    public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        init();
    }

    private void init() {
        for (int i = 0; i < images.length; i++) {
            ImageView iv = new ImageView(getContext());
            iv.setBackgroundResource(images[i]);
            this.addView(iv);
        }
        //由于ViewGroup默认只测量下面一层的子View(所以我们直接在ViewGroup里面添加ImageView是可以直接显示出来的),所以基本自定义ViewGroup都会要重写onMeasure方法,否则无法测量第一层View(这里是ScrollView)中的view,无法正常显示里面的内容。
        View testView = View.inflate(mContext, R.layout.test_viewpager_scrollview, null);
        addView(testView, 2);

        mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                //相对滑动:X方向滑动多少距离,view就跟着滑动多少距离
                scrollBy((int) distanceX, 0);
                return super.onScroll(e1, e2, distanceX, distanceY);
            }
        });
        mScroller = new Scroller(mContext);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            //如果是view:触发view的测量;如果是ViewGroup,触发测量ViewGroup中的子view
            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
        }
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 如果左右滑动, 就需要拦截, 上下滑动,不需要拦截
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) ev.getX();
                startY = (int) ev.getY();
                mGestureDetector.onTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                int endX = (int) ev.getX();
                int endY = (int) ev.getY();
                int dx = endX - startX;
                int dy = endY - startY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    // 左右滑动
                    return true;// 中断事件传递, 不允许孩子响应事件了, 由父控件处理
                }
                break;
            default:
                break;
        }
        return false;// 不拦截事件,优先传递给孩子(也就是ScrollView,让它正常处理上下滑动事件)处理

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("ACTION_MOVE", "scrollX=" + getScrollX());
                scrollX = getScrollX();//相对于初始位置滑动的距离
                //你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
                position = (getScrollX() + getWidth() / 2) / getWidth();
                //屏蔽边界值:postion在0~images.length-1范围内
                if (position >= images.length) {
                    position = images.length - 1 + 1;
                }
                if (position < 0) {
                    position = 0;
                }

                if (mOnPageScrollListener != null) {
                    Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / ((1) * getWidth())));
                    mOnPageScrollListener.onPageScrolled((float) (getScrollX() * 1.0 / (getWidth())), position);
                }
                break;
            case MotionEvent.ACTION_UP:

                //绝对滑动,直接滑到指定的x,y的位置,较迟钝
//                scrollTo(position * getWidth(), 0);
//                Log.e("TAG", "水平方向回弹滑动的距离=" + (-(scrollX - position * getWidth())));
                //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量
                mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
                invalidate();//使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法

                if (mOnPageScrollListener != null) {
                    mOnPageScrollListener.onPageSelected(position);
                }
                break;
        }
        return true;
    }

    /**
     * 其实Scroller的原理就是用ScrollTo来一段一段的进行,最后看上去跟自然的一样,必须使用postInvalidate,
     * 这样才会一直回调computeScroll这个方法,直到滑动结束。
     */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            Log.e("CurrX", "mScroller.getCurrX()=" + mScroller.getCurrX());
            postInvalidate();
            if (mOnPageScrollListener != null) {
                Log.e("TAG", "offset=" + (float) (getScrollX() * 1.0 / (getWidth())));
                mOnPageScrollListener.onPageScrolled((float) (mScroller.getCurrX() * 1.0 / ((1) * getWidth())), position);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);

        }
    }

    public interface OnPageScrollListener {
        /**
         * @param offsetPercent offsetPercent:getScrollX滑动的距离占屏幕宽度的百分比
         * @param position
         */
        void onPageScrolled(float offsetPercent, int position);

        void onPageSelected(int position);
    }

    private OnPageScrollListener mOnPageScrollListener;

    public void setOnPageScrollListener(OnPageScrollListener onPageScrollListener) {
        this.mOnPageScrollListener = onPageScrollListener;
    }
}

存在待解决的问题:
1、当快速切换时,页面会无法切换。有兴趣朋友的欢迎一起交流学习。

本文参考学习:

Android 手把手教您自定义ViewGroup
Android手势检测——GestureDetector全面分析
触摸事件的处理和传递dispatchTouchEvent、onInterceptTouchEvent
Android分析View的scrollBy()和scrollTo()的参数正负问题原理分析
scrollTo/scrollBy 使用详解

最后,附上我的一个Kotlin编写+组件化开发的开源项目Designer

Kotlin+组件化开发实践—开源项目Designer-App

Designer项目算是倾注了我蛮多心血了,每个页面和功能都当成是上线的App来做,App的logo还特地做了UI设计😃力求做到精致和完善,其中还包括了很多自己项目开发中的经验汇总和对新技术的探索和整合,希望对各位读者有所帮助,欢迎点个star,follow,或者给个小心心,嘻嘻😝也可以分享给你更多的朋友一起学习,您的支持是我不断前进的动力。如果有任何问题,欢迎在GitHub上给我提issue或者留言。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容