美文网首页安卓View学习
自定义ViewPager指示器

自定义ViewPager指示器

作者: sankemao | 来源:发表于2018-05-09 17:15 被阅读17次

    app的引导页和banner功能,通常采用ViewPager实现,并且往往都会有指示器,显示当前所选页。本文用自定义view的方式实现一个通用的圆形指示器,继承自View。
    一、效果预览

    image.png
    二、效果分析
    上图中显示两个小圆点,表示  ViewPager有两页(不考虑无限轮播)
    红色表示选中页,灰色表示未选中页
    实际上红色小圆点下方也有灰色小圆点,只是被覆盖了
    所以图上有两个灰色小圆点,它们统称为背景小圆点
    红色小圆点称为移动小圆点
    

    三、自定义View的属性

        <declare-styleable name="CirclePagerIndicator">
            <!--是否水平居中-->
            <attr name="indicator_centerHorizontal" format="boolean" />
            <!--滑动小圆点是否跟随-->      
            <attr name="indicator_follow" format="boolean" />
            <!--背景小圆点颜色-->    
            <attr name="indicator_color" format="color" />
            <!--背景小圆点描边颜色-->    
            <attr name="indicator_stroke_color" format="color" />
            <!--背景小圆点描边宽度-->    
            <attr name="indicator_stroke_width" format="dimension" />
            <!--移动小圆点颜色-->   
            <attr name="indicator_move_color" format="color" />
            <!--背景小圆点半径-->   
            <attr name="indicator_radius" format="dimension" />
            <!--移动小圆点半径-->   
            <attr name="indicator_move_radius" format="dimension" />
            <!--小圆点间距-->   
            <attr name="indicator_space" format="dimension" />
        </declare-styleable>
    

    四、自定义View套路代码

    public class CirclePagerIndicator extends View implements PagerIndicator {
        public CirclePagerIndicator(Context context) {
            this(context, null);
        }
    
        public CirclePagerIndicator(Context context, AttributeSet attrs) {
            super(context, attrs);
            //获取上面自定义的属性,并初始化画笔。
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePagerIndicator);
    
            mCenterHorizontal = a.getBoolean(R.styleable.CirclePagerIndicator_indicator_centerHorizontal, true);
            //背景小圆点画笔
            mBgCirclePaint.setStyle(Paint.Style.FILL);
            mBgCirclePaint.setColor(a.getColor(R.styleable.CirclePagerIndicator_indicator_color, 0x0000ff));
            //背景小圆点描边画笔
            mBgStrokePaint.setStyle(Paint.Style.STROKE);
            mBgStrokePaint.setColor(a.getColor(R.styleable.CirclePagerIndicator_indicator_stroke_color, 0x000000));
            mBgStrokePaint.setStrokeWidth(a.getDimension(R.styleable.CirclePagerIndicator_indicator_stroke_width, 0));
            //移动小圆点画笔
            mMoveCirclePaint.setStyle(Paint.Style.FILL);
            mMoveCirclePaint.setColor(a.getColor(R.styleable.CirclePagerIndicator_indicator_move_color, 0x0000ff));
            //背景小圆点半径
            mBgCircleRadius = a.getDimension(R.styleable.CirclePagerIndicator_indicator_radius, 10);
            //移动小圆点半径
            mMoveCircleRadius = a.getDimension(R.styleable.CirclePagerIndicator_indicator_move_radius, 10);
            //小圆点间距
            mIndicatorSpace = a.getDimension(R.styleable.CirclePagerIndicator_indicator_space, 20);
            //移动小圆点是否随viewpager移动跟随
            mIsFollow = a.getBoolean(R.styleable.CirclePagerIndicator_indicator_follow, true);
            if (mMoveCircleRadius < mBgCircleRadius) mMoveCircleRadius = mBgCircleRadius;
    
            a.recycle();
        }
    }
    

    五、测量

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
        }
    
        private int measureWidth(int measureSpec) {
            int width;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
                width = specSize;
            } else {
                final int count = mViewPager.getAdapter().getCount();
                width = (int) (getPaddingLeft() + getPaddingRight()
                        + (count * 2 * mBgCircleRadius) + (mMoveCircleRadius - mBgCircleRadius) * 2 + (count - 1) * mIndicatorSpace);
                if (specMode == MeasureSpec.AT_MOST) {
                    width = Math.min(width, specSize);
                }
            }
            return width;
        }
    
        private int measureHeight(int measureSpec) {
            int height;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            if (specMode == MeasureSpec.EXACTLY) {
                height = specSize;
            } else {
                height = (int) (2 * mBgCircleRadius + getPaddingTop() + getPaddingBottom() + 1);
                if (specMode == MeasureSpec.AT_MOST) {
                    height = Math.min(height, specSize);
                }
            }
            return height;
        }
    

    六、获取自定义View尺寸
    只有在onMeasure之后才能获得正确的控件宽高。所以在onSizeChanged中获取,同时当控件尺寸变化后该方法会再次执行,确保了尺寸获取的正确性。

    @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            if (w != oldw || h != oldh) {
                //控件总宽度
                mWidth = getWidth();
                //控件左边距
                mPaddingLeft = getPaddingLeft();
                //控件右边距
                mPaddingRight = getPaddingRight();
                //控件顶边距
                mPaddingTop = getPaddingTop();
            }
        }
    

    七、绑定ViewPager
    小圆点需要随着ViewPager的切换而移动,而我们都知道ViewPager中有个监听页面切换的api:addOnPageChangeListener(OnPageChangeListener listener);。所以直接将ViewPager绑定给指示器,定义绑定方法:

    @Override
        public void bindViewPager(ViewPager viewPager, int initialPosition, int realSize) {
            bindViewPager(viewPager, initialPosition);
            this.mRealSize = realSize;
        }
    
        public void bindViewPager(ViewPager viewPager, int initialPosition) {
            bindViewPager(viewPager);
            setCurrentItem(initialPosition);
        }
    
        public void bindViewPager(ViewPager viewPager) {
            if (mViewPager == viewPager) {
                return;
            }
            if (viewPager.getAdapter() == null) {
                throw new IllegalStateException("ViewPager does not set adapter");
            }
            mViewPager = viewPager;
            mViewPager.addOnPageChangeListener(this);
            mViewPager.getAdapter().registerDataSetObserver(mObserver);
            invalidate();
        }
    

    八、处理Viewpager的页面切换监听

    //*********************************OnPageChangeListener*************************************************
        @Override
        public void onPageScrollStateChanged(int state) {
    
        }
    
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            if (mRealSize == 0) {
                mCurrentPosition = position;
            } else {
                mCurrentPosition = position % mRealSize;
            }
            mPositionOffset = positionOffset;
            //如果指示器跟随ViewPager缓慢滑动,那么滚动时一直绘制界面
            if (mIsFollow) {
                invalidate(); //实时绘制小圆点.
            }
        }
    
        @Override
        public void onPageSelected(int position) {
            if (mRealSize == 0) {
                mFollowPage = mCurrentPosition = position;
            } else {
                mFollowPage = mCurrentPosition = position % mRealSize;    //轮播图无限, 记录的当前页.
            }
            invalidate();
        }
        //******************************************************************************************
    

    九、重头戏,小圆点绘制

        @Override
        protected void onDraw(Canvas canvas) {
    
            super.onDraw(canvas);
    
            if (mViewPager == null) {
                return;
            }
            //轮播图实际页面数(小圆点个数)
            int count = mRealSize == 0 ? mViewPager.getAdapter().getCount() : mRealSize;
    
            if (count == 0) {
                return;
            }
    
            if (mCurrentPosition >= count) {
                setCurrentItem(count - 1);
                return;
            }
            //直径+间隔距离
            final float circleAndSpace = 2 * mBgCircleRadius + mIndicatorSpace; 
            final float circleCenterY = mPaddingTop + mBgCircleRadius;
            //第一个小圆点的圆心x坐标
            float circleCenterXFirst = mPaddingLeft + mBgCircleRadius;
    
            if (mCenterHorizontal) {
                //(总长度 - 绘制圆点所占空间) / 2,居中
                circleCenterXFirst += ((mWidth - mPaddingLeft - mPaddingRight) - (count * circleAndSpace - mIndicatorSpace)) / 2.0f;
            }
    
            float cX;
            float cY;
    
            float strokeRadius = mBgCircleRadius;
    
            //如果绘制描边
            if (mBgStrokePaint.getStrokeWidth() > 0) {
                strokeRadius -= mBgStrokePaint.getStrokeWidth() * 1f / 2;
            }
    
            //绘制所有圆点
            for (int i = 0; i < count; i++) {
    
                cX = circleCenterXFirst + (i * circleAndSpace);//计算下个圆绘制起点偏移量
                cY = circleCenterY;
    
                //绘制背景小圆点
                if (mBgCirclePaint.getAlpha() > 0) {
                    canvas.drawCircle(cX, cY, mBgCircleRadius, mBgCirclePaint);
                }
    
                //绘制背景小圆点描边
                if (strokeRadius != mBgCircleRadius) {
                    canvas.drawCircle(cX, cY, strokeRadius, mBgStrokePaint);
                }
            }
    
            //绘制移动的小圆点。
            if (mIsFollow && mCurrentPosition == mRealSize - 1) {
                if (mPositionOffset < 0.5) { //当前为最后一页, 偏移小于一半时
                    canvas.drawCircle(circleCenterXFirst + mCurrentPosition * circleAndSpace, circleCenterY, mMoveCircleRadius, mMoveCirclePaint);
                    return;
                } else {                //当前为最后一页, 偏移大于一半时
                    canvas.drawCircle(circleCenterXFirst, circleCenterY, mMoveCircleRadius, mMoveCirclePaint);
                    return;
                }
            }
    
            float cx = (mIsFollow ? mCurrentPosition + mPositionOffset : mFollowPage) * circleAndSpace;
            cX = circleCenterXFirst + cx;
            cY = circleCenterY;
            canvas.drawCircle(cX, cY, mMoveCircleRadius, mMoveCirclePaint);
        }
    

    十、对外提供一些方法

    1、设置选中小圆点
    2、刷新页面
    
        @Override
        public void setCurrentItem(int item) {
            if (mViewPager == null) {
                throw new IllegalStateException("indicator has not bind ViewPager");
            }
    
            if (mRealSize == 0) {
                mCurrentPosition = item;
            } else {
                mCurrentPosition = item % mRealSize;
            }
            invalidate(); //调用onDraw.
        }
    
        @Override
        public void notifyDataSetChanged() {
            invalidate();
            requestLayout();//当view的宽高不变,不会调用invalidate();
        }
    

    十一、完善,观察者设计模式
    ViewPager的Adapter数据发生改变,调用Adapter的notifyDataSetChanged方法,ViewPager的数据改变,当然,与之关联的指示器控件也要刷新。怎么办?
    通过查看源码,发现ViewPager的adapter中持有一个Observable对象,adapter在这里就相当于被观察者。
    而ViewPager相当于观察者,它里面定义了Observer内部类,并将这个内部类对象注册给Adapter中的Observable。
    很明显,非常典型的观察者模式,都是套路。(我认为这样做的好处就是解耦)

    被观察者的变化,从而引起观察者的变化。不难看出,我们这里的自定义view相当于观察者,所以仿照ViewPager源码,定义一个Observer内部类如下:

        //============================ 观察者设计模式 ======================================
        private class IndicatorObserver extends DataSetObserver {
            @Override
            public void onChanged() {
                notifyDataSetChanged();
            }
        }
    

    那么该Observer内部类什么时候注册给adapter呢?往回看第七步的绑定Viewpager中的这句代码:

      mViewPager.getAdapter().registerDataSetObserver(mObserver);
    

    mObserver是自定义view的成员变量:

    private final DataSetObserver mObserver = new IndicatorObserver
    

    这样,自定义ViewPager指示器就完成了。

    相关文章

      网友评论

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

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