博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
安卓自定义View——网易颜色渐变效果指示器
阅读量:6515 次
发布时间:2019-06-24

本文共 12739 字,大约阅读时间需要 42 分钟。

一直想写博客来着,可惜直到现在才真正抽出时间。最近一直在研究网易新闻这个UI框架,发现了一些很值得借鉴的效果,当然,网上也不乏这方面的介绍。本文主要实现的指示器效果为字体颜色和大小渐变,废话不多说献上效果图:

 

实现效果主要包括:

  • 指示器背景可以根据用户自己定制形状
  • 动态判断tab个数是否可滑动和不可滑动
  • 支持tab文本长短不一,指示器也跟随变化
  • tab颜色和字体小大渐变
  • tab宽度动态测量
  • 选中自动居中

自定义LinearLayout,填充tab

要实现这样一种指示器,肯定少不了自定义控件了,这里我选择了水平方向LinearLayout,因为这样可以设置tab的权重(平分tab宽度),当然,最重要的是,可以放入HorizontalScrollView中滑动,嘎嘎~首先,我们要做的是继承LinearLayout,这个不用多说,相信大家都会。不做赘述。其次,填充tab,我这里决定使用Textview来填充,当然,如果你有兴趣做颜色移动特效,可以自动更改textview为你自己的自定义控件。我们先设置好tab的一些必要属性:

tabTextSize和maxTabTextSize:tab默认大小和选中时最大的字体大小,用于做出字体渐变

tabTextColor和tabPressColor:tab的默认颜色和选中时的颜色,用于做出颜色渐变

mTabWidth和defaultHeight:顾名思义,肯定要给tab一个默认宽度和默认高度

totalCount:当然还少不了个数,这个肯定和viewpager绑定的数组一致了

tabLengthArray:tab每个宽度的数组,这个很重要,因为我们的指示器要适应tab的自适应宽度,因为字体变大了,宽度肯定也会发生变化,这里的宽度数组保存了每个tab在设置完tab最大字体后测量出来的宽度。可能语言表达没办法说的清楚,下面上一下tab的构造代码:

    /**     * 创建默认tab(Textview)     *     * @param string 要显示的文本     * @param i  坐标     */    private TextView creatDefaultTab(String string, int i) {        TextView textView = new TextView(getContext());        textView.setGravity(Gravity.CENTER);        textView.setTextColor(tabTextColor);        textView.setTextSize(tabTextSize);        textView.setText(string);        textView.setPadding(tabPaddingLeft, tabPaddingTop, tabPaddingRight,                tabPaddingBottom);        TextPaint mTextPaint;        if (isShowTabSizeChange) {
//设置是否字体变换 TextView dTextView = new TextView(getContext()); dTextView.setTextSize(maxTabTextSize); mTextPaint = dTextView.getPaint();//得到最大尺寸textview的Paint,用于测量宽度 } else { mTextPaint = textView.getPaint(); } if(!isSetTabWidth) { mTabWidth = (int) mTextPaint .measureText(isDeuceTabWidth ? getMaxLengthString(titles) : string) + tabPaddingLeft + tabPaddingRight; } tabLengthArray[i] = mTabWidth; textView.setLayoutParams(new LinearLayout.LayoutParams(mTabWidth, defaultHeight + tabPaddingBottom + tabPaddingTop)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { textView.setAllCaps(true); } return textView; }

代码比较多,咱们挑重点说说,其中有个参数isShowTabSizeChange和isDeuceTabWidth以及isSetTabWidth,都是boolean类型,一个用于控制是否显示字体变换效果,一个是是否平分tab宽度,最后一个是是否被用户指定了宽度,其中测量tab宽度的方法用mTextPaint.measureText可能很多人对这个方法不是很了解,在这里我简单介绍一下,Paint中有一种专门用来Text文本展示的叫做TextPaint,内置了测量文本宽度的方法,即measureText,经过测试发现,textview在设置成Wrap content的时候的宽度恰巧是文本的宽度+Padding,这样我就很容易的计算出每个Textview要显示的宽高了大笑。再回过头看看代码,发现我内部也做了一个操作,判断是否平分宽度,如果平分,则采用取最大字体且字数最长的Textview的宽度作为每一个tab的宽度,这时候tabLengthArray内部每个值都是一样的,如果不平分宽度,则按照每个tab自己的宽度展示,这样我们的指示器就会更接地气有木有~

看的仔细的小伙伴肯定会发现里面有这么个函数getMaxLengthString,当然聪明的你肯定知道这是干嘛用的,是的,这个函数的操作仅仅是获取数组中最长长度所对应的文本,代码简单在这里就不贴出来了!

画圆角背景和内圆指示器

填充完我们的Tab,剩下的该是定制我们的背景和Draw我们的指示器了。定制背景不用多说,肯定是圆角矩形背景,在画背景之前我们按理先罗列一下一些基本参数:

backgroundColor:指示器整体背景颜色

backgroundRadius:指示器背景圆角半径,同时也是指示器圆角半径

strokeWidth、backgroundLineColor:指示器整体背景线条宽度,当然有宽度肯定需要颜色

isShowBackground:是否显示背景

介绍完一些必要的参数,咱们直接看圆角背景的代码实现: 

/**     * 设置背景     */    private void setBackgroundShape() {        // 创建drawable        GradientDrawable gd = new GradientDrawable();        gd.setColor(backgroundColor);        gd.setCornerRadius(backgroundRadius);        gd.setStroke(strokeWidth, backgroundLineColor);        if (isShowBackground) {            setBackground(gd);        } else {            setBackgroundResource(0);        }    }

代码很简单,这里我采用代码形式画drawable,毕竟我们希望我们的指示器可以按照用户来自定义圆角,所以显然用代码来画背景更人性化一点。

画完背景我们来看一下重中之重的指示器:

关于指示器滑动的原理我在这里说明一下,我先画好一个圆角背景指示器,然后通过不断的改变它与0点X轴距离的偏移量来重绘,也就是说,当水平X轴偏移量从一个tab到另一个tab,对应的指示器就是从第一个tab移动到第二个tab。擦,是不是觉得原理特别简单~~当然,有一点得注意到,如果tab设置了自适应宽度,那么我们的指示器宽度也应该随着偏移量的增长而变化。不用说,我们得先得到offset,庆幸的是,Viewpager已经给我们提供了,接下来看一下公式:

tabWidth=tabWidth+(nextTabWidth-tabWidth)*offset;

tabWidth=tabLengthArray[position];

nextTabWidth=tabLengthArray[position+1];

 了解完公式,我们基本上对整个指示器有了不错的了解。接下来,就是动手开始画指示器了,我们先绘制第一个tab的指示器,第一个tab的宽度我们取tabLengthArray[0],高度取默认高度defaultHeight即控件高度,知道了这两个参数,我们就可以来定位指示器的位置了:

mTransitX:指示器距离X轴零点的偏移量:

mTransitX=tabLengthArray[position]*offset+tab[0]+....+tab[position]

根据公式不难理解,应该是当前tab的宽度的offset+tab已经滑动的宽度之和,即下一个tab的起点

那么,我们就可以得到指示器的left、right、top和bottom了:

left=mTransitX;

right=tabWidth+left;

top=0;

bottom=defaultHeight;

知道了左右前后的坐标,就可以开始画指示器了,在这里贴上指示器代码:

@Override    protected void dispatchDraw(Canvas canvas) {        defaultHeight = getMeasuredHeight();        if (mCurrentIndex == 0) {            mTabWidth = tabLengthArray[0];        }        int left = mTransitX + mInitIndex * mTabWidth;// tab左边距离原点的位置        int right = mTabWidth + left;// 整个tab的位置        int top = 0;// tab距离顶端的位置        int bottom = defaultHeight;// 整个tab的高度        if (isShowIndicator) {            if (creator != null) {                creator.drawIndicator(canvas, left, top, right, bottom,                        indicatorPaint, backgroundRadius);            } else {                drawIndicatorWithTransitX(canvas, left, top, right, bottom,                        indicatorPaint);            }        }        if (mInitIndex != 0) {            (getTab(mInitIndex)).setTextColor(backgroundColor);            int centerX = getTransitXByPosition(mInitIndex)                    - (screenWidth - tabLengthArray[mInitIndex]) / 2;            parentScrollto(centerX, 0);        }        mInitIndex = 0;// 清除第一次默认index        super.dispatchDraw(canvas);    }

里面逻辑还是比较复杂的,因为牵扯到viewpager可以自定义默认显示的第几项,所以,我定义了一个mInitIndex,即默认的便宜位置,如果它不为0,则说明用户制定了默认显示项。其中isShowIndicator为是否显示指示器,creator为指示器回调,让用户自己去设置对应的指示器形状。接下来我们看真正的圆角指示器的实现了:

/**     * 默认为圆角矩形指示器,用户可继承重写自定义指示器样式     *     * @param canvas     * @param left   tab左边距离原点的位置     * @param top    整个tab的位置     * @param right  tab距离顶端的位置     * @param bottom 整个tab的高度,既控件高度     * @param paint  指示器画笔     */    public void drawIndicatorWithTransitX(Canvas canvas, int left, int top,                                          int right, int bottom, Paint paint) {        if (backgroundRadius < defaultHeight / 2) {            // 真机运行用这种方式,模拟器圆角会失真            RectF oval = new RectF(left, top, right, bottom);// 设置个新的长方形,扫描测量            canvas.drawRoundRect(oval, backgroundRadius, backgroundRadius,                    paint);        } else {
// 画三段代替圆角矩形,既圆、矩形、圆 RectF oval2 = new RectF(bottom / 2 + left, top, right - bottom / 2, bottom); canvas.drawCircle(oval2.left, bottom / 2, bottom / 2, indicatorPaint); canvas.drawRect(oval2, indicatorPaint); canvas.drawCircle(oval2.right, bottom / 2, bottom / 2, paint); } }

采用了drawRoundRect方法,在一般机型上已经可以完美显示效果,但是在模拟器中,过度圆角会产生偏差,这里,如果设置的圆角小于高度的一般,代表是圆角矩形,因为此时的圆角还不是一个半圆,模拟器可以很完美的呈现圆角,而不是圆形,如果半径大于高度一般,代表左右两边是半圆了,这时候我们采用三段式,即圆、矩形、圆拼凑而成。公用了一个banckgroundRadius的好处是,指示器的边框会随着外围的边框而变化,看起来更贴切,自然。

跟随Viewpager滑动,颜色字体渐变以及停靠中间

跟随滑动的逻辑很简单,抠抠脚就知道肯定要重写Viewpager的OnpageChangeListener的三个方法,即

onPageScrolled、onPageSelected、onPageScrollStateChanged三个方法,其中我们需要滑动偏移量offset,所以我们首先重写onPageScrolled,贴上代码:

@Override    public void onPageScrolled(int position, float positionOffset,                               int positionOffsetPixels) {        if (position + 1 != totalCount && !isClick && position < totalCount) {            if (isShowTabSizeChange) {
// 判断是否变换 setTabSizeChange(position, 1 - positionOffset); setTabSizeChange(position + 1, positionOffset); } setTabColorChange(position, 1 - positionOffset); setTabColorChange(position + 1, positionOffset); } if (positionOffset != 0.0 && position < totalCount - 1) { mTransitX = (int) (tabLengthArray[position] * positionOffset + (getTransitXByPosition(position))); mTabWidth = (int) (tabLengthArray[position] + (tabLengthArray[position + 1] - tabLengthArray[position]) * positionOffset); } invalidate(); // 回调 if (onPageChangeListener != null) { onPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } }

思路很简单,首先控制当前position必须要在tab数量之内,否则不滑动(这就是为什么效果图上上方Indicator只滑动三个就不动了大笑),然后设置颜色变换以及字体大小变化,然后按照上方公式得到mTransitX 和mTabWidth 然后去重绘指示器。思路没什么难的,主要是颜色变换,接下来上颜色变换效果:

/**     * 设置颜色变换     *     * @param position     * @param positionOffset     */    protected void setTabColorChange(int position, float positionOffset) {        getTab(position).setTextColor(                blendColors(tabPressColor, tabTextColor, positionOffset));    }  /**     * 两个颜色渐变转化     *     * @param color1     * @param color2     * @param ratio     * @return     */    private int blendColors(int color1, int color2, float ratio) {        final float inverseRation = 1f - ratio;        float r = (Color.red(color1) * ratio)                + (Color.red(color2) * inverseRation);        float g = (Color.green(color1) * ratio)                + (Color.green(color2) * inverseRation);        float b = (Color.blue(color1) * ratio)                + (Color.blue(color2) * inverseRation);        return Color.rgb((int) r, (int) g, (int) b);    }

呵呵!一个类轻松搞定颜色变换,确实如此,因为我们得到了offset之后,就可以得到当前tab的颜色色值了,会根据offset的变换而呈现出很自然的颜色变化。

当然,细心的人肯定会发现在我们指示器滑动后会默认居中,这种效果看起来还是蛮舒服的,因为人的肉眼第一扫到的就是中间,更清晰明了。下面我来介绍这种停顿中间的思路:

首先:要滑动肯定需要用到scrollto的方法,当然,如果外布局可以滑动,我们就要将其塞入水平Scrollview中,然后控制父控件滑动即可,滑动解决了,那么滑动的距离怎么计算呢?很简单:

centerX=tab[0]+...+tab[postion]-(screenWidth-tab[postion])/2;

大家是否发现了这个公式的前段和上个位置偏移量计算的后段一样,是的,都是求出当前postion之前的tab总和,那么我们可以抽出一个函数专门计算这个宽度之和:

/**     * 获取position前几项tab宽度之和     *     * @return     */    private int getTransitXByPosition(int posotion) {        int defaultNum = 0;        for (int i = 0; i < posotion; i++) {            defaultNum += tabLengthArray[i];        }        return defaultNum;    }

方法简单到爆,就一个累加算法,完美实现了滑动中间,剩下的问题就是在什么时候执行了,我打开了ios版网易新闻看了10秒,果断发现它是在滑动后才移动到中间的,那么思路就清晰了,我只要重写onPageScrollStateChanged(擦,这单词真难拼),在state==Viewpager.SCROLL_STATE_IDLE中添加即可,告此,指示器已经完整实现。

用法

我们的指示器到这里几乎所有的原理已经打通,骨头有了,剩下的就是肉了,所以,我们得暴露一些方法给使用者,我总结了下,总共包含如下:

protected void style2() {        mIndicator2.setTitles(mDatas);        mIndicator2.setDefaultHeight(dp2px(30));//设置默认高度为30dp        mIndicator2.setTabPadding(dp2px(10), 0, dp2px(10), dp2px(5));//设置tabPadding左右10dp        mIndicator2.setBackgroundRadius(dp2px(35));//设置外框半径25dp        mIndicator2.setShowTabSizeChange(true);//显示字体大小切换效果        mIndicator2.setShowBackground(false);//不显示背景        mIndicator2.setShowIndicator(true);//显示指示器        mIndicator2.setDeuceTabWidth(false);//不平分tab宽度,默认为平分        mIndicator2.setTabTextSize(14);//设置tab默认字体大小        mIndicator2.setTabMaxTextSize(18);//设置tab变换字体大小,如果setShowTabSizeChange设置false,则按默认字体大小        mIndicator2.setTabPressColor(Color.RED);//设置tab选中后的字体颜色        mIndicator2.setTabTextColor(Color.parseColor("#666666"));//设置未选中时字体颜色        mIndicator2.setIndicatorColor(Color.RED);//设置指示器颜色为红色        mIndicator2.setmBackgroundColor(Color.RED);//设置背景颜色为红色,如果setShowBackground为false则无背景        mIndicator2.setBackgroundLineColor(Color.RED);//设置背景框颜色,如果setShowBackground为false则无背景框颜色        mIndicator2.setBackgroundStrokeWidth(dp2px(1));//设置背景框宽度        mIndicator2.setDrawIndicatorCreator(new DrawIndicatorCreator() {            @Override            public void drawIndicator(Canvas canvas, int left, int top,                    int right, int bottom, Paint paint, int raduis) {                //设置下滑线条                RectF oval = new RectF(left, bottom - dp2px(2), right, bottom);                canvas.drawRect(oval, paint);            }        });    }

是的,暴露的方法非常非常多,其中细心的朋友发现,下划线指示器也只是两行代码的事,是不是so easy~当然,我也考虑过一些问题,比如说,用户想设置一频只显示4个tab数量怎么办,你拿着放大镜也找不到设置tab数量的方法,那么为什么我没有提供这个方法呢,原因很简单,因为我们的tab的宽度是长短不一的,而且用户可以设置setDeuceTabWidth来控制是否平分宽度,如果平分,为了防止字体变大而换行,我们设置了已最长字体大小平分,这样就可以避免了字体显示异常,如果用户确实有一频固定显示几个tab的需求,那么解决方法也很简单,只要设置setTabWidth即可,这个方法优先级最高,只要设置了setTabWidth指定tab宽度后,所有平分与不平分都没有关系了,当然,如果文本太长换行了的话,只能通过设置字体大小来控制了。

总结:

总的来说,实现这样一个指示器并没有太复杂的逻辑,主要还是一些简单的坐标计算,先设置并填装好我们的tab,然后画我们的指示器,通过重绘来控制指示器位置,然后监听Viewpager滑动。原理非常简单,但是实现过程中也确实是摸打滚爬,虽然效果实现了,但是内部逻辑可能还能再优,这将是我自定义View的第一篇博客,当然,肯定不会是最后一篇,我将继续坚持安卓自定义View的开发之路,所见所学,都会分享出来,欢迎读者多多支持哦~

 

其他效果:

 

 

 

 

 

 

圆角:

mIndicator2.setBackgroundRadius(dp2px(10));//设置外框半径25dp

 

三角形:

Path mPath = new Path();                int mTriangleHeight = (bottom / 3) - dp2px(5);                mPath.moveTo(left / 2 - dp2px(50), bottom);                mPath.lineTo(left / 2 + dp2px(50), bottom);                mPath.lineTo(left / 2, -mTriangleHeight);                mPath.close();                canvas.save();                // 画笔平移到正确的位置                canvas.translate(left / 2 + (right - left) / 2, bottom + 1);                canvas.drawPath(mPath, paint);                canvas.restore();

 

 

下载地址:

CSDN地址:

作者:yangpeixing

QQ群:251664830 欢迎大神加入

转载请注明出处~谢谢~

 

 

 

 

 

 

转载于:https://www.cnblogs.com/yangpeixing/p/6340457.html

你可能感兴趣的文章
汇编语言的应用
查看>>
device platform 相应的表
查看>>
安德鲁斯----多媒体编程
查看>>
中断小笔记
查看>>
FreeBinary 格式说明
查看>>
使用Spring Cloud和Docker构建微服务
查看>>
九州云实战人员为您揭秘成功部署OpenStack几大要点
查看>>
CloudCC:智能CRM究竟能否成为下一个行业风口?
查看>>
追求绿色数据中心
查看>>
Web开发初学指南
查看>>
探寻光存储没落的真正原因
查看>>
高通64位ARMv8系列服务器芯片商标命名:Centriq
查看>>
构建智能的新一代网络——专访Mellanox市场部副总裁 Gilad Shainer
查看>>
《数字视频和高清:算法和接口》一导读
查看>>
《中国人工智能学会通讯》——6.6 实体消歧技术研究
查看>>
如何在Windows查看端口占用情况及查杀进程
查看>>
云存储应用Upthere获7700万美元股权债务融资
查看>>
洗茶,你误会了多少年?
查看>>
贵阳高新区力争打造“千亿级大数据园区”
查看>>
安防众筹不止于卖产品 思维拓展刺激消费
查看>>