Android自定義輪播圖指示器

batmanlf 7年前發布 | 10K 次閱讀 游戲開發 Android開發 移動開發

輪播圖在項目中一般會使用VeiwPager來實現,同時還會關聯輪播指示器。

輪播指示器效果gif

改造ViewPager的OnPageChangedListener,添加自定義的ViewPager滾動監聽器(直接將上一篇文章的代碼貼過來了)

/**
 * ViewPager輔助類
 */
public class ViewPagerHelper implements ViewPager.OnPageChangeListener {

    private double mLastPositionOffsetSum;  // 上一次滑動總的偏移量
    private OnPageScrollListener mOnPageScrollListener;

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        // 當前總的偏移量
        float currentPositionOffsetSum = position + positionOffset;
        // 上次滑動的總偏移量大于此次滑動的總偏移量,頁面從右向左進入(手指從右向左滑動)
        boolean rightToLeft = mLastPositionOffsetSum <= currentPositionOffsetSum;
        if (currentPositionOffsetSum == mLastPositionOffsetSum) return;
        int enterPosition;
        int leavePosition;
        float percent;
        if (rightToLeft) {  // 從右向左滑
            enterPosition = (positionOffset == 0.0f) ? position : position + 1;
            leavePosition = enterPosition - 1;
            percent = (positionOffset == 0.0f) ? 1.0f : positionOffset;
        } else {            // 從左向右滑
            enterPosition = position;
            leavePosition = position + 1;
            percent = 1 - positionOffset;
        }
        if (mOnPageScrollListener != null) {
            mOnPageScrollListener.onPageScroll(enterPosition, leavePosition, percent);
        }
        mLastPositionOffsetSum = currentPositionOffsetSum;
    }

    @Override
    public void onPageSelected(int position) {
        if (mOnPageScrollListener != null) {
            mOnPageScrollListener.onPageSelected(position);
        }
    }

    /**
     * @param state 當前滑動狀態
     *              ViewPager.SCROLL_STATE_IDLE     頁面處于閑置、穩定狀態,即沒被拖動也沒慣性滑動
     *              ViewPager.SCROLL_STATE_DRAGGING 頁面正在被用戶拖動,即手指正在拖動狀態
     *              Viewpager.SCROLL_STATE_SETTLING 頁面處于即將到達最終狀態的過程,即手指松開后慣性滑動狀態
     */
    @Override
    public void onPageScrollStateChanged(int state) {
        if (mOnPageScrollListener != null) {
            mOnPageScrollListener.onPageScrollStateChanged(state);
        }
    }

    public void bindScrollListener(ViewPager viewPager, OnPageScrollListener onPageScrollListener) {
        mOnPageScrollListener = onPageScrollListener;
        viewPager.addOnPageChangeListener(this);
    }
}
/**
 * ViewPage的頁面滾動監聽器
 */
public interface OnPageScrollListener {
    /**
     * 頁面滾動時調用
     *
     * @param enterPosition 進入頁面的位置
     * @param leavePosition 離開的頁面的位置
     * @param percent       滑動百分比
     */
    void onPageScroll(int enterPosition, int leavePosition, float percent);

    /**
     * 頁面選中時調用
     *
     * @param position 選中頁面的位置
     */
    void onPageSelected(int position);

    /**
     * 頁面滾動狀態變化時調用
     *
     * @param state 頁面的滾動狀態
     */
    void onPageScrollStateChanged(int state);
}

根據兩種指示器的效果分析,都是通過回調onPageScroll方法中不斷變化的enterPositon、leavePosition和percent來實現。

NumberIndicator(數字指示器)

當ViewPager頁面從右向左滑動時,指示器中對應頁面的數字從下往上滾動,頁面停止,數字停止在中間位置。

當ViewPager頁面從左向右滑動時,指示器中對應頁面的數字從下往上滾動,頁面停止,數字停止在中間位置。

最終指示器效果可以采用繪制View的方式來實現(當然也可以采用組合控件的方式來實現)。ViewPager頁面滑動時,不斷重繪View,達到指示數字上下滑動的效果。

/**
 * 數字指示器
 */
public class NumberIndicater extends View implements OnPageScrollListener {

    private Context mContext;

    private int mCircleColor;
    private int mCircleSize;
    private int mNumberColor;
    private int mNumberSize;

    private int mCount;
    private int mCurrent;

    private Paint mCirclePaint;
    private Paint mTextPaint;

    private float offset;       // 頁面偏移百分比
    private boolean isUp;       // 指示器數字是否向上滑動

    public NumberIndicater(Context context) {
        this(context, null);
    }

    public NumberIndicater(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NumberIndicater(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttrs(context, attrs);
        initPaint();
    }

    /**
     * 初始化屬性
     *
     * @param context
     * @param attrs
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        mContext = context;
        mCircleSize = dp2px(48f);
        mCircleColor = 0xfffdd63b;
        mNumberSize = sp2px(14f);
        mNumberColor = 0xff353535;
        // 自定義屬性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NumberIndicater);
        mCircleColor = ta.getColor(R.styleable.NumberIndicater_circle_color, mCircleColor);
        mCircleSize = (int) ta.getDimension(R.styleable.NumberIndicater_circle_size, mCircleSize);
        mNumberColor = ta.getColor(R.styleable.NumberIndicater_number_color, mNumberColor);
        mNumberSize = (int) ta.getDimension(R.styleable.NumberIndicater_number_size, mNumberSize);
        ta.recycle();
    }

    private void initPaint() {
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(mCircleColor);
        mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE);

        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextAlign(Paint.Align.LEFT);
        mTextPaint.setColor(mNumberColor);
        mTextPaint.setTextSize(mNumberSize);

        offset = 1;
        mCount = 3;
        mCurrent = 1;
        isUp = true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 設置測量后的尺寸
        setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec));
    }

    private int measure(int measureSpec) {
        int size = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.EXACTLY:
                size = specSize;
                break;
            case MeasureSpec.AT_MOST:
                size = mCircleSize;
                break;
        }
        return size;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 繪制圓形底圖
        canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, mCircleSize / 2f, mCirclePaint);
        // 繪制分割線
        drawSplit(canvas);
        // 繪制右邊總數數字
        drawTotleNumber(canvas);
        // 繪制左邊指示數字
        drawIndicatNumber(canvas);
    }

    private void drawSplit(Canvas canvas) {
        String text = "/";
        float width = mTextPaint.measureText(text);
        float x = (getWidth() - width) / 2f;
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        float y = getHeight() / 2f + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2f;
        // x為繪制文本左邊緣距離X軸的距離,y為繪制文本基線距離Y軸的位置
        canvas.drawText(text, x, y, mTextPaint);
    }

    private void drawTotleNumber(Canvas canvas) {
        String text = String.valueOf(mCount);
        float x = getWidth() / 2f + mTextPaint.measureText("/") / 2f + 3;
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        float y = getHeight() / 2f + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2f;
        canvas.drawText(text, x, y, mTextPaint);
    }

    private void drawIndicatNumber(Canvas canvas) {
        mTextPaint.setTextSize(mNumberSize * 1.3f);
        String text = String.valueOf(mCurrent);
        Rect rect = new Rect();
        // 獲取文本的寬度
        float width = mTextPaint.measureText(text);
        // 獲取文本的高度
        mTextPaint.getTextBounds(text, 0, text.length(), rect);
        float height = rect.height();
        // 文本左邊緣距離X軸的距離
        float x = getWidth() / 2f - mTextPaint.measureText("/") / 2f - 3 - width;
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        // 文本基線位置距離Y軸的距離
        float y = getHeight() / 2f + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2f;
        if (isUp) { // 指示數字向上滑動
            y = offset * y + (1 - offset) * (getHeight() / 2f - mCircleSize / 2f + mCircleSize + height);
        } else {    // 指示數字向下滑動
            y = offset * y + (1 - offset) * (getHeight() / 2f - mCircleSize / 2f);
        }
        canvas.drawText(text, x, y, mTextPaint);
        mTextPaint.setTextSize(mNumberSize);
    }

    /**
     * 將指示器綁定到ViewPager
     *
     * @param viewPager view pager
     */
    public void bindViewPager(ViewPager viewPager) {
        if (viewPager != null && viewPager.getAdapter() != null) {
            mCount = viewPager.getAdapter().getCount();
            new ViewPagerHelper().bindScrollListener(viewPager, this);
            invalidate();    // 綁定ViewPager后指示器重繪,因為指示器的數字與初始的可能不同
        }
    }

    @Override
    public void onPageScroll(int enterPosition, int leavePosition, float percent) {
        mCurrent = enterPosition + 1;
        offset = percent;
        isUp = enterPosition > leavePosition;
        postInvalidate();       // 滑動過程中不斷重繪
    }

    @Override
    public void onPageSelected(int position) {

    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    private int dp2px(float dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpValue,
                mContext.getResources().getDisplayMetrics());
    }

    private int sp2px(float dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                dpValue,
                mContext.getResources().getDisplayMetrics());
    }
}

PointIndicator(圓點指示器)

根據ViewPager滑動的位置和百分比,動態繪制指示小圓點。通過Viewpger滑動過程中的enterPosition和leavePosition以及滑動百分比percent來計算出滑動小圓點的左邊位置即可。

/**
 * 圓點指示器
 */
public class PointIndicator extends View implements OnPageScrollListener {

    private Context mContext;

    private int mNormalColor;
    private int mSelectColor;
    private int mPointSize;
    private int mPointSpace;

    private Paint mNormalPaint;
    private Paint mSelectPaint;

    private int mCount;
    private int enterPosition;
    private int leavePosition;
    private float percent;

    public PointIndicator(Context context) {
        this(context, null);
    }

    public PointIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PointIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttrs(context, attrs);
        initPaint();
    }

    private void initPaint() {
        mNormalPaint = new Paint();
        mNormalPaint.setColor(mNormalColor);
        mNormalPaint.setAntiAlias(true);

        mSelectPaint = new Paint();
        mSelectPaint.setColor(mSelectColor);
        mSelectPaint.setAntiAlias(true);

        mCount = 4;
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        mContext = context;

        mNormalColor = 0x66cccccc;
        mSelectColor = 0xfffdd63b;
        mPointSize = dp2px(3f);
        mPointSpace = dp2px(3f);

        // 自定義屬性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PointIndicator);
        mNormalColor = ta.getColor(R.styleable.PointIndicator_normal_color, mNormalColor);
        mSelectColor = ta.getColor(R.styleable.PointIndicator_select_color, mSelectColor);
        mPointSize = (int) ta.getDimension(R.styleable.PointIndicator_point_size, mPointSize);
        mPointSpace = (int) ta.getDimension(R.styleable.PointIndicator_point_space, mPointSpace);
        ta.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

    private int measureWidth(int measureSpec) {
        int size = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.EXACTLY:
                size = specSize;
                break;
            case MeasureSpec.AT_MOST:
                size = mCount * mPointSize + (mCount - 1) * mPointSpace;
                break;
        }
        return size;
    }

    private int measureHeight(int measureSpec) {
        int size = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.EXACTLY:
                size = specSize;
                break;
            case MeasureSpec.AT_MOST:
                size = mPointSize;
                break;
        }
        return size;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 繪制normalPoint
        drawNormalPoint(canvas);
        // 繪制selectPoint
        drawSelectPoint(canvas);

    }

    private void drawSelectPoint(Canvas canvas) {
        float cx;
        if (enterPosition > leavePosition) {
            cx = (leavePosition + 0.5f) * mPointSize
                    + leavePosition * mPointSpace
                    + (mPointSize + mPointSpace) * percent;
        } else {
            cx = (leavePosition + 0.5f) * mPointSize
                    + leavePosition * mPointSpace
                    - (mPointSize + mPointSpace) * percent;
        }
        float cy = getHeight() / 2;
        float radius = mPointSize / 2f;
        canvas.drawCircle(cx, cy, radius, mSelectPaint);
    }

    private void drawNormalPoint(Canvas canvas) {
        for (int i = 0; i < mCount; i++) {
            float cx = mPointSize / 2f + (mPointSize + mPointSpace) * i;
            float cy = getHeight() / 2;
            float radius = mPointSize / 2f;
            canvas.drawCircle(cx, cy, radius, mNormalPaint);
        }
    }

    public void bindViewPager(ViewPager viewPager) {
        if (viewPager != null) {
            if (viewPager.getAdapter() != null) {
                mCount = viewPager.getAdapter().getCount();
                new ViewPagerHelper().bindScrollListener(viewPager, this);
                requestLayout(); // 綁定ViewPager后指示器重新布局,因為指示器的數量和寬度可能有變化
            }
        }
    }

    @Override
    public void onPageScroll(int enterPosition, int leavePosition, float percent) {
        this.enterPosition = enterPosition;
        this.leavePosition = leavePosition;
        this.percent = percent;
        postInvalidate();
    }

    @Override
    public void onPageSelected(int position) {

    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    private int dp2px(float dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpValue,
                mContext.getResources().getDisplayMetrics());
    }

}

涉及到的自定義屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="NumberIndicater">
        <attr name="circle_color" format="color"/>
        <attr name="circle_size" format="dimension"/>
        <attr name="number_size" format="dimension"/>
        <attr name="number_color" format="color"/>
    </declare-styleable>

    <declare-styleable name="PointIndicator">
        <attr name="point_size" format="dimension"/>
        <attr name="point_space" format="dimension"/>
        <attr name="normal_color" format="color"/>
        <attr name="select_color" format="color"/>
    </declare-styleable>
</resources>

以上兩種指示器都是通過自繪View的方式來實現。通過自定義的OnPageScrollListener還可以實現更多效果炫酷的指示器。

 

 

來自:http://www.jianshu.com/p/60be684d897d

 

 本文由用戶 batmanlf 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!