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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_16674697/article/details/51954228

一直想写博客来着,可惜直到现在才真正抽出时间。最近一直在研究网易新闻这个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() {
        // 创建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();


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

作者:yangpeixing

QQ群:251664830 欢迎大神加入

转载请注明出处~谢谢~





















猜你喜欢

转载自blog.csdn.net/qq_16674697/article/details/51954228