关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 安卓开发 »Android ViewPager 指示器控件的最佳实现

    Android ViewPager 指示器控件的最佳实现

    2015-02-14 00:00:00 出处:Mr.Simple的专栏
    分享

    为什么我说它是最实用的 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 中一些方法的使用。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    SQL汉字转换为拼音的函数

    windows 7系统无法运行Photoshop CS3的解决方法

    巧用MySQL加密函数对Web网站敏感数据进行保护

    MySQL基础知识简介

    Windows7和WinXP下如何实现不输密码自动登录系统的设置方法介绍

    windows 7系统ip地址冲突怎么办?windows 7系统IP地址冲突问题的

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。