自定义ViewPager轮播图指示器

作者: xiaoyanger | 来源:发表于2017-01-13 15:00 被阅读818次

    轮播图在项目中一般会使用VeiwPager来实现,同时还会关联轮播指示器。上一篇文章给ViewPager添加一个自定义的滚动监听器实现了一个自定义的滚动监听器,现在通过这个监听器的关联可以很方便的实现一个自定义的指示器。

    轮播指示器效果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:
                case MeasureSpec.UNSPECIFIED:
                    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:
                case MeasureSpec.UNSPECIFIED:
                    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:
                case MeasureSpec.UNSPECIFIED:
                    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还可以实现更多效果炫酷的指示器。

    源码:https://github.com/xiaoyanger0825/Indicator

    相关文章

      网友评论

        本文标题:自定义ViewPager轮播图指示器

        本文链接:https://www.haomeiwen.com/subject/vuerbttx.html