Android自定义View之 实现一个多功能的IndicatorView

CircleIndicatorView.png

Indicator (指示器) 可能大家都见的比较多了,在一个APP中,有很多场景都会用到Indicator,比如第一次安装APP 时的引导页,首页上面的广告Banner ,又或者是Tab下面的Indicator。Indicator 一般配合ViewPager 使用,它能很直观的反应出ViewPager 中一共有多少页,当前选中的是哪一页,用户能够很容易的看出是否还可以滑动,用户体验较好。那么本篇文章就讲一下如何通过自定义View来实现一个漂亮简洁的IndicatorView。

Indicator的展示形式

�我们使用Indicator时,常见的有三种形式,第一种是根据ViewPager 的总页数展示一排小圆点,选中和未选中分别标为不同的颜色。这也是普遍的使用Indicator的效果,效果如下:

image.png

第二种形式是根据ViewPager 的总页数展示一排圆点,并且用数字标出其顺序(1,2,3,4 ...),大概效果如下:

image.png

第三种形式和第二种形式差不多,同样根据ViewPager总页数展示一片圆点,但是用字母标出顺序(A,B,C,D ....), 比如 :魅族手机屏幕的切换的Indicator。效果如下:

image.png

本篇文章就通过自定义View来实现这三种IndicatorView。

通过自定义View 实现多功能的CircleIndicatorView

首先整理一下思路,CircleIndicatorView ,它是由一组圆 组成的,只不过圆有多种状态,选中、未选中、显示字母、显示数字等等。看着有这么多中状态,感觉挺复杂,但是其实不复杂,只是用不同的变量来控制不同的状态即可。其他的先不考虑,我们只要先把这一排圆画出来,剩下的就比较容易了。要确定一个圆的,需要知道圆的半径和圆心,半径是我们可以配置的,那么,其实核心就是计算这一排的圆点的圆心位置。圆心 y 的位置是固定的(View 高的一般),因此我们只需要计算每个圆的圆心x的位置,看下图,其实很容易就能找到规律,规律如下: 第一个圆的x 就等于圆的半径,从第二个圆开始,当前圆的圆心x 坐标为 上一个圆的x 坐标 + (radius * 2 + mSpace)。 其中mSpace 是圆之间的间距。

圆心x坐标计算公式.png

有了上面的规律,我们只需要一个循环就能找出所有圆的圆心位置。代码如下:

/**
     * 测量每个圆点的位置
     */
    private void measureIndicator(){
        mIndicators.clear();
        float cx = 0;
        for(int i=0;i<mCount;i++){
            Indicator indicator = new Indicator();
            if( i== 0){
                cx = mRadius + mStrokeWidth;
            }else{
                cx += (mRadius + mStrokeWidth) * 2 +mSpace;
            }

            indicator.cx = cx;
            indicator.cy = getMeasuredHeight() / 2;

            mIndicators.add(indicator);
        }
    }

我们用Indicator类记录了每个圆的圆心位置,并且保存在一个列表里面,现在有了所有圆的数据,我们就可以绘制 了。

  @Override
    protected void onDraw(Canvas canvas) {

        for(int i=0;i<mIndicators.size();i++){

            Indicator indicator = mIndicators.get(i);
            float x = indicator.cx;

            float y = indicator.cy;

            if(mSelectPosition == i){
                mCirclePaint.setStyle(Paint.Style.FILL);
                mCirclePaint.setColor(mSelectColor);
            }else{
                mCirclePaint.setColor(mDotNormalColor);
                if(mFillMode != FillMode.NONE){
                    mCirclePaint.setStyle(Paint.Style.STROKE);
                }else{
                    mCirclePaint.setStyle(Paint.Style.FILL);

                }
            }
            canvas.drawCircle(x,y, mRadius, mCirclePaint);

            // 绘制小圆点中的内容
            if(mFillMode != FillMode.NONE){
                String text = "";
                if(mFillMode == FillMode.LETTER){
                    if(i >= 0 && i<LETTER.length){
                        text = LETTER[i];
                    }
                }else{
                    text = String.valueOf(i+1);
                }
                Rect bound = new Rect();
                mTextPaint.getTextBounds(text,0,text.length(),bound);
                int textWidth = bound.width();
                int textHeight = bound.height();

                float textStartX = x - textWidth / 2;
                float textStartY = y + textHeight / 2;
                canvas.drawText(text,textStartX,textStartY, mTextPaint);
            }

        }

    }

绘制的代码很简单,无非就是循环我们保存的列表,拿出每一个Indicator,然后绘制圆就行了,除此之外就是一些状态的判断,比如,是否绘制数字、字母和选中状态等等 。

到此,我们的CircleIndicatorView 的绘制工作算是完成了,并且也能够显示在界面上了,但是它现在还是一个单独的View。我们前面说过,IndicatorView 一般是配合ViewPager使用的,它是随着Viewpager的切换而改变的,因此我们需要将IndicatorView 与ViewPager 进行关联。

要让CircleIndicatorView 与ViewPager 关联,首先需要 CircleIndicatorView 实现 ViewPager.OnPageChangeListener 接口。然后在onPageSelected方法中记录当前页的位置。代码如下:

 @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
        mSelectPosition = position;
        invalidate();
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

第二步,向外提供一个API ,设置CircleIndicatorView 与 ViewPager 关联。

  /**
     *  与ViewPager 关联
     * @param viewPager
     */
    public void setUpWithViewPager(ViewPager viewPager){
        releaseViewPager();
        if(viewPager == null){
            return;
        }
        mViewPager = viewPager;
        mViewPager.addOnPageChangeListener(this);
        int count = mViewPager.getAdapter().getCount();
        setCount(count);
    }

通过如上两步,就建立了CircleIndicatorView 与ViewPager的关联,只需要调用方法setUpWithViewPager 就OK。

到此就完了吗?当然还没有完,其实还有一个小细节,那就是IndicatorView 因该是可以点击的,点击Indicator小圆点就能切换ViewPager 到对应的页面(如 Iphone 和 魅族手机的 屏幕切换,点击indicator小圆点就可以切换 ,以前没有注意的现在可以去试一下 ),那么我们也来实现这样一个功能,其实很简单,重写onTouchEvent方法 ,监听ACTION_DOWN 事件,然后获取点击屏幕的坐标,与所绘制的圆位置比较,如果点击区域在圆的范围内,就点击了该Indicator。点击之后,切换Viewpager到对应页面。代码如下:

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        float xPoint = 0;
        float yPoint = 0;
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                xPoint = event.getX();
                yPoint = event.getY();
                handleActionDown(xPoint,yPoint);
                break;

        }

        return super.onTouchEvent(event);
    }

    private void handleActionDown(float xDis,float yDis){
        for(int i=0;i<mIndicators.size();i++){
            Indicator indicator = mIndicators.get(i);
            if(xDis < (indicator.cx + mRadius+mStrokeWidth)
                    && xDis >=(indicator.cx - (mRadius + mStrokeWidth))
                    && yDis >= (yDis - (indicator.cy+mStrokeWidth))
                    && yDis <(indicator.cy+mRadius+mStrokeWidth)){
                 // 找到了点击的Indicator
                // 切换ViewPager
                 mViewPager.setCurrentItem(i,false);
                 // 回调
                if(mOnIndicatorClickListener!=null){
                    mOnIndicatorClickListener.onSelected(i);
                }
                break;
            }
        }
    }

到此,我们自定义IndicatorView 的工作就差不多完成了,但是现在的IndicatorView 还不是很灵活,我们要让它的可配置性更强,就应该提供更多的API 来让IndicatorView 使用更加灵活方便,因此,最后一步,加上一些自定义属性来提高它的灵活性。自定义了如下一些属性:

属性名 属性意义 取值
indicatorRadius 设置指示器圆点的半径 单位为 dp 的值
indicatorBorderWidth 设置指示器的border 单位为 dp 的值
indicatorSpace 设置指示器之间的距离 单位为 dp 的值
indicatorTextColor 设置指示器中间的文字颜色 颜色值,如:#FFFFFF
indicatorColor 设置指示器圆点的颜色 颜色值
indicatorSelectColor 设置指示器选中的颜色 颜色值
fill_mode 设置指示器的模式 枚举值:有三种,分别是letter,number和none

自定义属性文件如下:

 <declare-styleable name="CircleIndicatorView">
        <attr name="indicatorRadius" format="dimension"/>
        <attr name="indicatorBorderWidth" format="dimension"/>
        <attr name="indicatorSpace" format="dimension"/>
        <attr name="indicatorTextColor" format="color"/>
        <attr name="indicatorColor" format="color"/>
        <attr name="indicatorSelectColor" format="color"/>
        <attr name="fill_mode">
            <enum name="letter" value="0"/>
            <enum name="number" value="1"/>
            <enum name="none" value="2"/>
        </attr>
    </declare-styleable>

通过上面这些属性,我们就可以很好的定制IndicaotorView 了,比如,自定义圆的大小,颜色,border,文字的颜色,选中的颜色和展示的模式等等。

最终的效果如下:

CircleIndicator最终效果.gif

CircleIndicatorView 使用方法

上面讲了自定义IndicatorView 的思路、过程 和一些关键代码,为了使用方便,已经将CircleIndicatorView封装成了一个库,以下介绍一下CircleIndicatorView API的使用。Github 地址:https://github.com/pinguo-zhouwei/CircleIndicatorView

** Dependency**

1, 最外层build.gradle添加如下代码:

   allprojects {
       repositories {
    ...
    maven { url 'https://jitpack.io' }
   }
}

2, app 层buid.gradle dependencies 中 添加如下代码:

compile 'com.github.pinguo-zhouwei:CircleIndicatorView:v1.0.0'
    

使用方法如下:
** 1, xml 添加CircleIndicatorView,配置相关属性 **

 <com.zhouwei.indicatorview.CircleIndicatorView
         android:id="@+id/indicator_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
         android:layout_marginBottom="50dp"
         android:layout_centerHorizontal="true"
         app:indicatorSelectColor="#00A882"
         app:fill_mode="letter"
         app:indicatorBorderWidth="2dp"
         app:indicatorRadius="8dp"
         app:indicatorColor="@color/colorAccent"
         app:indicatorTextColor="@android:color/white"
         />

2, 在代码中与ViewPager 关联

       // 初始化ViewPager 相关
        mViewPager = (ViewPager) findViewById(R.id.viewpager);
        mPagerAdapter = new ViewPagerAdapter();
        mViewPager.setAdapter(mPagerAdapter);

        mIndicatorView = (CircleIndicatorView) findViewById(R.id.indicator_view);
        // 关联ViewPager 
        mIndicatorView.setUpWithViewPager(mViewPager);

上面所有列举的属性,出了在xml 中配置外,也可以在代码中设置,如下:

        // 在代码中设置相关属性
        
        CircleIndicatorView indicatorView = (CircleIndicatorView) findViewById(R.id.indicator_view3);
        // 设置半径
        indicatorView.setRadius(DisplayUtils.dpToPx(15));
        // 设置Border
        indicatorView.setBorderWidth(DisplayUtils.dpToPx(2));

        // 设置文字颜色
        indicatorView.setTextColor(Color.WHITE);
        // 设置选中颜色
        indicatorView.setSelectColor(Color.parseColor("#FEBB50"));
        //
        indicatorView.setDotNormalColor(Color.parseColor("#E38A7C"));
        // 设置指示器间距
        indicatorView.setSpace(DisplayUtils.dpToPx(10));
        // 设置模式
        indicatorView.setFillMode(CircleIndicatorView.FillMode.LETTER);

        // 最重要的一步:关联ViewPager
        indicatorView.setUpWithViewPager(mViewPager);

最后

以上就是通过自定义View的方式实现一个多功能的IndicatorView的全部内容,内容很简单,通过自定义的View方式实现很方便,免得每次还得找设计师要切图啊(能自己动手的,就不找设计师要了。😄)。如有问题欢迎交流,最后,如果你觉得不错的话,欢迎github 赏个start啊!!轮子传送门 https://github.com/pinguo-zhouwei/CircleIndicatorView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容