Android 自定義ViewPager

項目地址

概述

  1. 處理滑動到左邊界和右邊界時,不允許滑動。
  2. 頁面滑動一半回彈,滑動一半以上自動切換下一界面。
  3. 當頁面內存在ScrollView這類子控件,事件要正常分發,不允許自定義ViewPager攔截事件。
  4. 回彈與切換動畫處理。

源碼分析

  1. 初始化
public ViewPagerY(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mContext = context;
    // Scroller設置的是一個勻速插值器
    myScroll = new Scroller(context, new LinearInterpolator());
    // 初始化ImageLoader
    ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(mContext));
}
  1. 設置資源,如圖片id,佈局,圖片鏈接等
/**
 * 爲ViewPagerY設置資源集合
 *
 * @param res
 */
public void setRes(ArrayList<ResType> res) {
    for (ResType mResType : res) {
        if (mResType.getmType() == ResType.Type.IAMG) {
            // 如果是資源圖片id,創建ImageView對象
            ImageView imageView = new ImageView(mContext);
            imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            imageView.setImageResource((Integer) mResType.getRes());
            // 將設置好的ImageView添加進ViewPagerY控件中
            this.addView(imageView);
        }
        if (mResType.getmType() == ResType.Type.URL) {
            // 如果資源是圖片URL,創建ImageView對象.
            ImageView imageView = new ImageView(mContext);
            imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            // 使用ImageLoader將圖片從網絡獲取設置到ImageView中.
            ImageLoader.getInstance().displayImage((String) mResType.getRes(), imageView);
            // 將設置好的ImageView添加進ViewPagerY控件中
            this.addView(imageView);
        }
        if (mResType.getmType() == ResType.Type.LAYOUT) {
            // 如果資源是自己寫的佈局文件,就獲取該佈局文件對應的View
            View view = LayoutInflater.from(mContext).inflate((Integer) mResType.getRes(), null);
            // 將獲取到的View添加進ViewPagerY控件中
            this.addView(view);
        }
    }
}
// 資源類型類
public class ResType<T> {
    public enum Type {
        IAMG,
        LAYOUT,
        URL
    }
    private T mRes;
    private Type mType;
    public ResType(T res, Type mType) {
        this.mRes = res;
        this.mType = mType;
    }
    public T getRes() {
        return mRes;
    }

    public Type getmType() {
        return mType;
    }
}
  1. ViewPagerY對子View的測量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 設置ViewPagerY尺寸
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 獲取ViewPagerY寬高
    height = getMeasuredHeight();
    widht = getMeasuredWidth();
    // 創建子View的MeasureSpec, 創建MeasureSpec是有規律的,可以看這個筆記: https://blog.csdn.net/MoLiao2046/article/details/105708819
    int wMeasureSpec = MeasureSpec.makeMeasureSpec(widht, MeasureSpec.EXACTLY);
    int hMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    for (int i = 0; i < getChildCount(); i++) {
        // 遍歷子View開始測量子View.
        getChildAt(i).measure(wMeasureSpec, hMeasureSpec);
    }
}
  1. ViewPagerY對子View進行佈局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (int i = 0; i < getChildCount(); i++) {
        // 資源集合中每一個資源對應一個頁面.
        // 分別設置這些View的左上角與右下角座標.
        this.getChildAt(i).layout(i * widht, 0, i * widht + widht, height);
    }
}
  1. 判斷手勢,如果手勢爲水平滑動就攔截, 否則就正常將事件分發給子View處理.
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    // 默認交給ViewGroup攔截事件,ViewGroup一般是不會攔截事件.
    boolean interceptChildeEvent = super.onInterceptTouchEvent(event);
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            // mLastX與mDownX是手指按下時候的座標,這兩個值會在ViewPagerY中onTouchEvent中用到,處理頁面滑動用的.
            // onTouchEvent()中也能獲取ACTION_DOWN事件,但是在onTouchEvent中獲取手指按下座標再進行相應移動處理會出現頁面跳動的問題.
            mLastX = mDownX = interceptLastX = event.getX();
            interceptLastY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            // 獲取當前手指座標
            float moveX = event.getX();
            float moveY = event.getY();
            // 移動後,計算出滑動後與上一個座標點之間的距離.
            float slopX = moveX - interceptLastX;
            float slopY = moveY - interceptLastY;
            // 得到手指滑動距離絕對值
            float slopAbsX = Math.abs(slopX);
            float slopAbsY = Math.abs(slopY);
            if ((slopAbsX > 0 || slopAbsY > 0) && (slopAbsX - slopAbsY) >= 6) {
                // 如果手指移動距離大於0,且橫向移動距離減去縱向移動距離大於6像素
                // 那麼ViewPagerY就將該事件攔截, 不分發給它的子View使用,留給自己使用了.
                // 這樣會導致mFirstTouchTarget=null,之後子View就再也接收不到事件組的其他事件了.
                interceptChildeEvent = true;
            }
            interceptLastX = moveX;
            interceptLastY = moveY;
            break;
    }
    return interceptChildeEvent;
}
  1. 處理頁面滑動回彈的邏輯
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_MOVE:
            float mMoveX = event.getX();
            // mLastX:是在onInterceptTouchEvent()中得到的值
            float mDiffX = mMoveX - mLastX;
            mLastX = mMoveX;
            if (event.getPointerId(event.getActionIndex()) == 0 && event.getPointerCount() == 1) {// 這個條件可以控制只追蹤屏幕中的一個手指的滑動.
                int scrollX = getScrollX();
                if (currentIndex == 0) {
                    // 第一頁
                    if (mDiffX < 0) {
                        // 如果向左滑動
                        ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                    } else {
                        // 處理先向左滑動,然後又向右滑動.
                        float mDiffMargin = scrollX - mDiffX;
                        if (mDiffMargin >= 0) {
                            // 內容左邊距離控件左邊的距離減去向右滑動距離,如果大於0,說明內容左邊距離控件左邊還有間隔距離,滑動距離取手指移動距離.
                            ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                        } else {
                            // 內容左邊距離控件左邊的距離減去向右滑動距離,如果小於0,說明內容左邊與控件左邊需要重合,滑動距離取getScrollX().
                            ViewPagerY.this.scrollBy(-scrollX, 0);
                        }
                    }
                }
                if (currentIndex == getChildCount() - 1) {
                    // 最後一頁
                    if (mDiffX > 0) {
                        // 如果向右滑動
                        ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                    } else {
                        // 處理先向右滑動,然後又向左滑動.
                        // (((getChildCount() - 1) * widht) - scrollX): 表示內容右邊距離控件右邊的距離
                        float mDiffMargin = (((getChildCount() - 1) * widht) - scrollX) + mDiffX;
                        if (mDiffMargin >= 0) {
                            // 說明內容右邊與控件右邊還有距離,滑動距離取手指移動距離.
                            ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                        } else {
                            // 說明內容右邊與控件右邊需要重合,滑動距離取內容右邊與控件右邊的距離.
                            ViewPagerY.this.scrollBy(-(((getChildCount() - 1) * widht) - scrollX), 0);
                        }
                    }
                }
                if (currentIndex != 0 && currentIndex != getChildCount() - 1) {
                    // scrollBy總是和移動的相反
                    ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            float mUpX = event.getX();
            // mDownX:是在onInterceptTouchEvent()中得到的值
            if (mUpX - mDownX > getWidth() / 2) {
                // 移動到上一個
                moveTo(currentIndex - 1);
            } else if (mUpX - mDownX < -getWidth() / 2) {
                // 移動到下一個
                moveTo(currentIndex + 1);
            } else {
                // 移動到當前頁面
                moveTo(currentIndex);
            }
            break;
    }
    return true;
}
  1. 頁面切換邏輯
/**
 * 頁面切換
 * @param index
 */
public void moveTo(int index) {
    int duration = 0;
    if (index < 0) {
        index = 0;
    } else if (index > getChildCount() - 1) {
        index = getChildCount() - 1;
    }
    // 中間經歷幾個界面, 每個頁面切換時長是固定的.
    int count = currentIndex - index;
    if (count != 0) {
        duration = mDuration * count;
    } else {
        duration = mDuration;
    }
    currentIndex = index;
    if (onPageChangeListener != null) {
        // 頁面切換的監聽.
        onPageChangeListener.onPageSelect(currentIndex);
    }
    // getScrollX(): 內容左邊與ViewPager控件左邊距離
    // currentIndex * getWidth(): 切換到currentIndex界面時的getScrollX()值.
    // 得到需要移動的距離.
    int distanceX = currentIndex * getWidth() - getScrollX();
    //給MyScroll 計算的類賦初始值
    myScroll.startScroll(getScrollX(), 0, distanceX, 0, Math.abs(duration));
    invalidate();
}
// 想要緩慢滑動這個也很重要.
@Override
public void computeScroll() {
    //如果爲true說明移動還沒結束
    if (myScroll.computeScrollOffset()) {
        //得到計算的位置,然後移動
        float currX = myScroll.getCurrX();
        scrollTo((int) currX, 0);
        invalidate();
    }
}

效果圖

效果圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章