一直想写博客来着,可惜直到现在才真正抽出时间。最近一直在研究网易新闻这个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的构造代码:

 <pre name="code" class="java">    /*** 创建默认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() {// 创建drawableGradientDrawable 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;

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

    @Overrideprotected 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;// 清除第一次默认indexsuper.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,贴上代码:

   @Overridepublic 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));//设置默认高度为30dpmIndicator2.setTabPadding(dp2px(10), 0, dp2px(10), dp2px(5));//设置tabPadding左右10dpmIndicator2.setBackgroundRadius(dp2px(35));//设置外框半径25dpmIndicator2.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() {@Overridepublic 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();

下载地址:http://download.csdn.net/detail/qq_16674697/9580348

作者:yangpeixing

QQ群:251664830 欢迎大神加入

转载请注明出处~谢谢~


安卓自定义View——网易颜色渐变效果指示器相关推荐

  1. android控件向内弧度_安卓自定义 View 基础:坐标系、角度弧度、颜色

    安卓自定义View基础 - 坐标系 一.屏幕坐标系和数学坐标系的区别 由于移动设备一般定义屏幕左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向, 所以在手机屏幕上的坐标系与数学中常见的坐标系是 ...

  2. 安卓自定义view全解:初始化,onDraw函数,onMeasure函数,用户手势事件

    全栈工程师开发手册 (作者:栾鹏) 安卓教程全解 安卓自定义view全解. view类包含如下函数.可供重写. onFinishInflate() 回调方法,当应用从XML加载该组件并用它构建界面之后 ...

  3. 安卓自定义view中 绘画基本图形点线面,矩形,方形,圆,扇形,文字及沿着特定方向布局,自定义圆角ImageView图片等等相关api使用方法及举例

    安卓自定义view中 绘画基本图形点线面,矩形,方形,圆,扇形,文字及沿着特定方向布局,自定义圆角ImageView图片等等相关api使用方法及举例,图片压缩处理逻辑 本文旨在介绍自定义View的实现 ...

  4. 安卓自定义View进阶-事件分发机制原理

    之前讲解了很多与View绘图相关的知识,你可以在 安卓自定义View教程目录 中查看到这些文章,如果你理解了这些文章,那么至少2D绘图部分不是难题了,大部分的需求都能满足,但是关于View还有很多知识 ...

  5. 安卓自定义View的状态保存与恢复

    安卓自定义View的状态保存与恢复 我们在开发某些安卓应用(如安卓小游戏)时,可能会用到自定义View,这时候往往需要保存自定义View的状态信息,以便在遇到某些情况(如由于系统内存资源紧张被系统杀死 ...

  6. 安卓自定义View实现加载gif图片

    开题:加载GIF的场景在安卓开发中还比较常见,网上也有一些三方法的框架会支持对gif的加载,在上篇博客为大家推荐的图片加载库Glide也支持gif的加载Glide工具类的简单封装,今天给大家分享通过自 ...

  7. android 屏幕坐标色彩,Android自定义View实现颜色选取器

    Android 自定义View 颜色选取器,可以实现水平.竖直选择颜色类似 SeekBar 的方式通过滑动选择颜色. 效果图 xml 属性 1.indicatorColor 指示点颜色 2.indic ...

  8. 安卓自定义 View 进阶: 图片文字

    一.Canvas的常用操作速查表 操作类型 相关API 备注 绘制颜色 drawColor, drawRGB, drawARGB 使用单一颜色填充整个画布 绘制基本形状 drawPoint, draw ...

  9. 安卓自定义View进阶-Canvas之图片文字

    在上一篇文章Canvas之画布操作中我们了解了画布的一些基本操作方法,本次了解一些绘制图片文字相关的内容.如果你对前几篇文章讲述的内容熟练掌握的话,那么恭喜你,本篇结束之后,大部分的自定义View已经 ...

最新文章

  1. Silverlight揭秘
  2. 递归解决CSDN论坛上的小朋友分苹果问题
  3. class.forname()中要写相对路径吗?_你喜欢这里吗?翻译成“Do you like here?”是错误的,为啥呢?...
  4. Windows下的.NET+ Memcached安装
  5. Homography
  6. writer在java中的意思_Java在FileWriter和BufferedWriter之间的区别
  7. WebSocket教程(一)
  8. goland 合并分支
  9. position有哪些属性?
  10. oracle加载日记账直服务器,Oracle EBS GL 总账日记账打开报错此职责无可用函数
  11. Mac更换JDK版本
  12. js 剩余时间,天,小时,分钟,秒
  13. 【windows7 bluescreen蓝屏的解决方法】
  14. 微信小程序【获取用户昵称头像和昵称(附源码)】
  15. 微信小程序 - - 授权登录退出和缓存
  16. requests.exceptions.ConnectionError:HTTPSConnectionPool(host
  17. Yuma格式历书的总结
  18. 茹立云:推荐未来的价值要比搜索引擎大
  19. 使用R进行倾向得分匹配(PSM)
  20. mysql主从同步加密_教你构建MySQL主从结构,实现基于SSL加密的主从同步机制

热门文章

  1. IDEA_02 修改项目编码格式修改字体,大小
  2. C语言-数字炸弹汉诺塔
  3. 2021-08-08 CFF-CSP 邻域均值 C和C++混写
  4. 中小学科技创新实验室
  5. 彼得·德鲁克,现代管理之父,“大师中的大师”
  6. PY4E 作业练习 答案代码 exercises answer code
  7. Django:使用QuerySet删除和查询数据表
  8. 自然语言处理学习笔记(1)——词典分词
  9. 我的世界服务器修改table,转化桌 (Transmutation Table)
  10. 开发板udhcpc获取不到广电网络数字电视机顶盒ip问题解决