关闭 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 中一些方法的使用。

    上一篇返回首页 下一篇

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

    别人在看

    电脑屏幕不小心竖起来了?别慌,快捷键搞定

    Destoon 模板存放规则及语法参考

    Destoon系统常量与变量

    Destoon系统目录文件结构说明

    Destoon 系统安装指南

    Destoon会员公司主页模板风格添加方法

    Destoon 二次开发入门

    Microsoft 将于 2026 年 10 月终止对 Windows 11 SE 的支持

    Windows 11 存储感知如何设置?了解Windows 11 存储感知开启的好处

    Windows 11 24H2 更新灾难:系统升级了,SSD固态盘不见了...

    IT头条

    Synology 更新 ActiveProtect Manager 1.1 以增强企业网络弹性和合规性

    00:43

    新的 Rubrik Agent Cloud 加速了可信的企业 AI 代理部署

    00:34

    宇树科技 G1人形机器人,拉动一辆重达1.4吨的汽车

    00:21

    Cloudera 调查发现,96% 的企业已将 AI 集成到核心业务流程中,这表明 AI 已从竞争优势转变为强制性实践

    02:05

    投资者反对马斯克 1 万亿美元薪酬方案,要求重组特斯拉董事会

    01:18

    技术热点

    大型网站的 HTTPS 实践(三):基于协议和配置的优化

    ubuntu下右键菜单添加新建word、excel文档等快捷方式

    Sublime Text 简明教程

    用户定义SQL Server函数的描述

    怎么在windows 7开始菜单中添加下载选项?

    SQL Server 2016将有哪些功能改进?

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

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