打造Android 最实用的ViewPager 指示器控件

来源:互联网 发布:车身设计 知乎 编辑:程序博客网 时间:2024/05/18 15:24

为什么我说它是最实用的 ViewPager 指示器控件呢?
它有以下几个特点:
1、通过自定义 View 来实现,代码简单易懂;
2、使用起来非常方便;
3、通用性高,大部分涉及到 ViewPager 指示器的地方都能使用此控件;
4、实现了两种指示器效果,传统版指示器和流行版指示器(具体请看效果图)

一、先来看效果图
传统版指示器的效果图:
这里写图片描述

流行版指示器的效果
这里写图片描述

二、分析
如果单纯的要实现此功能,相信,大家都能实现,而我也不会拿出来这里讲了,这里我是要把它打造成一个控件,通俗一点讲就是,在以后可以直接拿来用,而不需要修改代码。
控件,那就离不开自定义 View,我在前面也讲了一篇关于自定义 View 的文章 Android自定义View,你必须知道的几点 ,虽然讲的很浅,但我觉得还是非常有用处的,有兴趣的可以阅读一下,对理解这篇文章很有帮助。额,跑题了! 回顾下那两张效果图,整个 View 需要的资源其实只有两张图片;唯一的难点,就是对图片绘制的位置如何计算;既然是实现通用型易用的控件,那就不能再 ViewPager 的 OnPagerChangerListener 中来改变指示器的状态,所以这个时候,就得把 ViewPager 传入到这个控件中,到这里,分析的差不多了;

三、编码实现功能
像白饭要一口一口的吃,这里就得先创建一个类,然后让他继承之 View,前期步骤跟我的上一篇 blog 很像,就不累赘了,直接上代码

public class IndicatorView extends View implements ViewPager.OnPageChangeListener{    //指示器图标,这里是一个 drawable,包含两种状态,    //选中和飞选中状态    private Drawable mIndicator;    //指示器图标的大小,根据图标的宽和高来确定,选取较大者    private int mIndicatorSize ;    //整个指示器控件的宽度    private int mWidth ;    /*图标加空格在家 padding 的宽度*/    private int mContextWidth ;    //指示器图标的个数,就是当前ViwPager 的 item 个数    private int mCount ;    /*每个指示器之间的间隔大小*/    private int mMargin ;    /*当前 view 的 item,主要作用,是用于判断当前指示器的选中情况*/    private int mSelectItem ;    /*指示器根据ViewPager 滑动的偏移量*/    private float mOffset ;    /*指示器是否实时刷新*/    private boolean mSmooth ;    /*因为ViewPager 的 pageChangeListener 被占用了,所以需要定义一个    * 以便其他调用    * */    private ViewPager.OnPageChangeListener mPageChangeListener ;    public IndicatorView(Context context) {        this(context, null);    }    public IndicatorView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public IndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        //通过 TypedArray 获取自定义属性        TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.IndicatorView);        //获取自定义属性的个数        int N = typedArray.getIndexCount();        for (int i = 0; i < N; i++) {            int attr = typedArray.getIndex(i);            switch (attr) {                case R.styleable.IndicatorView_indicator_icon:                    //通过自定义属性拿到指示器                    mIndicator = typedArray.getDrawable(attr);                    break;                case R.styleable.IndicatorView_indicator_margin:                    float defaultMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,5,getResources().getDisplayMetrics());                    mMargin = (int) typedArray.getDimension(attr , defaultMargin);                    break ;                case R.styleable.IndicatorView_indicator_smooth:                    mSmooth = typedArray.getBoolean(attr,false) ;                    break;            }        }        //使用完成之后记得回收        typedArray.recycle();        initIndicator() ;    }    private void initIndicator() {        //获取指示器的大小值。一般情况下是正方形的,也是时,你的美工手抖了一下,切出一个长方形来了,        //不用怕,这里做了处理不会变形的        mIndicatorSize = Math.max(mIndicator.getIntrinsicWidth(),mIndicator.getIntrinsicHeight()) ;        /*设置指示器的边框*/        mIndicator.setBounds(0,0,mIndicator.getIntrinsicWidth(),mIndicator.getIntrinsicWidth());    }}

这里需要注意一点的就是 Drawable mIndicator这个成员变量,它是在 drawable 文件夹下定义的一个 drawable 文件,包含了选中和为选中两张图片。

接着是测量工作

    /**     * 测量View 的大小,这个方法我前面的 blog 讲了很多了,     * @param widthMeasureSpec     * @param heightMeasureSpec     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));    }    /**     * 测量宽度,计算当前View 的宽度     * @param widthMeasureSpec     * @return     */    private int measureWidth(int widthMeasureSpec){        int mode = MeasureSpec.getMode(widthMeasureSpec) ;        int size = MeasureSpec.getSize(widthMeasureSpec) ;        int width ;        int desired = getPaddingLeft() + getPaddingRight() + mIndicatorSize*mCount + mMargin*(mCount -1) ;        mContextWidth = desired ;        if(mode == MeasureSpec.EXACTLY){            width = Math.max(desired, size)  ;        }else {            if(mode == MeasureSpec.AT_MOST){                width = Math.min(desired,size) ;            }else {                width = desired ;            }        }        mWidth = width ;        return width ;    }    private int measureHeight(int heightMeasureSpec){        int mode = MeasureSpec.getMode(heightMeasureSpec) ;        int size = MeasureSpec.getSize(heightMeasureSpec) ;        int height ;        if(mode == MeasureSpec.EXACTLY){            height = size ;        }else {            int desired = getPaddingTop() + getPaddingBottom() + mIndicatorSize ;            if(mode == MeasureSpec.AT_MOST){                height = Math.min(desired,size) ;            }else {                height = desired ;            }        }        return height ;    }

测量完了,就到了绘制 View 的阶段了。这里重点看看 onDraw()方法,先说一下,大致流程,
首先,绘制所有为选中的指示器,这里是绘制 Drawable,所以需要用到 Canvas中的某些方法来平移画布,让其顺序的绘制所有的 Drawable,这里特别注意的一点就是 Canvas.restore() 方法,这个方法是在绘制完成之后,想要回到原来的位置和状态调用,但它必须配合Canvas.save()来配套使用。Canvas.save()就是记录当前画布的状态,所以这里,我觉得这个方法的名字应该换成 record()是不是更符合我们的理解呢?这里纯属个人见解,理解了就好,如何命名不妨碍我们的工作,下面是 onDraw()的代码,注释很详细

    /**     * 绘制指示器     * @param canvas     */    @Override    protected void onDraw(Canvas canvas) {        /*        * 首先得保存画布的当前状态,如果位置行这个方法        * 等一下的 restore()将会失效,canvas 不知道恢复到什么状态        * 所以这个 save、restore 都是成对出现的,这样就很好理解了。        * */        canvas.save() ;        /*        * 这里开始就是计算需要绘制的位置,        * 如果不好理解,请按照我说的做,拿起        * 附近的纸和笔,在纸上绘制一下,然后        * 你就一目了然了,        *        * */        int left = mWidth/2 - mContextWidth/2 +getPaddingLeft() ;        canvas.translate(left,getPaddingTop());        for(int i = 0 ; i < mCount ; i++){            /*            * 这里也需要解释一下,            * 因为我们额 drawable 是一个selector 文件            * 所以我们需要设置他的状态,也就是 state            * 来获取相应的图片。            * 这里是获取未选中的图片            * */            mIndicator.setState(EMPTY_STATE_SET) ;            /*绘制 drawable*/            mIndicator.draw(canvas);            /*每绘制一个指示器,向右移动一次*/            canvas.translate(mIndicatorSize+mMargin,0);        }        /*        * 恢复画布的所有设置,也不是所有的啦,        * 根据 google 说法,就是matrix/clip        * 只能恢复到最后调用 save 方法的位置。        * */        canvas.restore();        /*这里又开始计算绘制的位置了*/        float leftDraw = (mIndicatorSize+mMargin)*(mSelectItem + mOffset);        /*        * 计算完了,又来了,平移,为什么要平移两次呢?        * 也是为了好理解。        * */        canvas.translate(left,getPaddingTop());        canvas.translate(leftDraw,0);        /*        * 把Drawable 的状态设为已选中状态        * 这样获取到的Drawable 就是已选中        * 的那张图片。        * */        mIndicator.setState(SELECTED_STATE_SET) ;        /*这里又开始绘图了*/        mIndicator.draw(canvas);    }

现在我们的控件其实就差一步没有实现了,就是在何时何地更新 View,一开始就分析了,这个 View 是需要传入 ViewPager 的,传入 ViewPager 的目的是什么,其实有三个,
1、获取 ViewPager 的 item 的个数,从而来确定指示器的个数;
2、获取当前 ViewPager 选中的 item,也是确定指示器选中的 item;
3、获取 OnPagerChangeListener,来控制 View 什么时候需要刷新;

    /**     * 此ViewPager 一定是先设置了Adapter,     * 并且Adapter 需要所有数据,后续还不能     * 修改数据     * @param viewPager     */    public void setViewPager(ViewPager viewPager){        if(viewPager == null){            return;        }        PagerAdapter pagerAdapter = viewPager.getAdapter() ;        if(pagerAdapter == null){            throw new RuntimeException("请看使用说明");        }        mCount = pagerAdapter.getCount() ;        viewPager.setOnPageChangeListener(this);        mSelectItem = viewPager.getCurrentItem() ;        invalidate();    }    public void setOnPageChangeListener(ViewPager.OnPageChangeListener mPageChangeListener) {        this.mPageChangeListener = mPageChangeListener;    }    @Override    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {        Log.v("zgy","========"+position+",===offset" + positionOffset) ;        if (mSmooth){            mSelectItem = position ;            mOffset = positionOffset ;            invalidate();        }        if(mPageChangeListener != null){            mPageChangeListener.onPageScrolled(position,positionOffset,positionOffsetPixels);        }    }    @Override    public void onPageSelected(int position) {        mSelectItem = position ;        invalidate();        if(mPageChangeListener != null){            mPageChangeListener.onPageSelected(position);        }    }    @Override    public void onPageScrollStateChanged(int state) {        if(mPageChangeListener != null){            mPageChangeListener.onPageScrollStateChanged(state);        }    }

这个位置也有个点需要提一下,就是当 mSmooth 为 true 的时候,这个时候是需要实时刷新的,所以需要在onPageScrolled(int position, float positionOffset, int positionOffsetPixels)调用 invalidate(),并把偏移量保存起来,用于计算绘制指示器的位置。

好了,以上就是指示器控件的实现全过程;

既然是一个控件,接下来看看在 xml 是如何引用的

    <com.gyzhong.viewpagerindicator.IndicatorView        android:id="@+id/id_indicator"        android:layout_centerHorizontal="true"        android:layout_alignParentBottom="true"        android:layout_marginBottom="20dp"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:padding="5dp"        zgy:indicator_icon="@drawable/indicator_selector"        zgy:indicator_margin="5dp"/>

再来看看代码中的引用

        mIndicatorView = (IndicatorView) findViewById(R.id.id_indicator) ;        mIndicatorView.setViewPager(mViewPager);

代码简洁明了。

四、总结
整体来说,不是很难,代码量很少,主要用到的知识点,1、自定义属性,2、如何测量 View,2、Cavans 中一些方法的使用;最后,看了如果觉得有用,请顶一下,谢谢!

源码下载地址:戳我

8 0
原创粉丝点击