Android進階之自定義ViewGroup—帶你一步步輕鬆實現ViewPager

weixin_33850890發表於2018-08-17
本文導語:

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的效果圖:


3828835-d7acfdaf63356cc3.gif
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()方法,按下圖排列方式,確定它們各自的擺放位置。


3828835-f70ba628a90b593b.png
示意圖.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;
    }

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


3828835-1b851d783c8b6a31.gif
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所標定的座標,可以看到下面的執行效果很遲鈍。


3828835-45227b5441d270a9.gif
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的效果就出來了,看下效果圖:


3828835-dc9fea2614f37c5d.gif
image3.gif
《三》給ViewPager新增指示器

看一下實現的效果:指示器在右下角,這裡處理成了讓指示器的小點點,隨著滑動的等比距離移動,當然也可以簡單的處理成滑動到某個位置後,再移動小點點,這裡只是提供一個新增指示器的思路。


3828835-90afbb8b5b8804d4.gif
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,看如下圖,會發現上下能滑動,左右滑動失效了。


3828835-c51db009cd491c25.gif
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或者留言。

相關文章