Android 高级UI解密 (一) :Paint图形文字绘制 与 高级渲染

前言

背景介绍

Android在5.0版本时经历了一个彻底变革,一套适用于所有设备类型和任意软件版本的设计准则 —– Material Design出现了,其界面风格发生了很大的变化。

UIUser Interface(用户界面)的简称,泛指用户的操作界面,view是用户界面最基本的组件,其扩展了View类,更控制着屏幕上的绘制和实践,例如被触控。屏幕上显示的所有元素都依赖view。其中主要分为两类view:

  • 独立view,例如用于显示文字或图片。(自定义控件……)
  • 多个view组合的布局。(ViewGroup……)

除此之外,动画在应用中的地位也逐渐增加,许多行为操作需要借助动画过度,Material Design也特别强调了设计的流畅性,涉及到了Android系统中的View动画、属性动画。

目标说明

在了解以上背景后,可见UI对Android开发的重要性,这里的UI开发就是界面绘制,包括图片、文本、自定义控件等等,实现基本后还要考虑使用色彩滤镜、着色器和一些高级技巧来绘制任何想要实现的效果。

自定义View或Layout时,不仅要了解测量(onMeasure)、布局(onLayout)、展示(onDraw),更要考虑处理不同的点击、滑动输入事件(onTouchEvent)和事件传递,甚至需要注意其保存、恢复状态。其中更是涉及到Paint、Canvas、Pathgraphics包中各类的灵活运用,还需要结合动画。

这是一个庞大的工程,不仅仅是一个自定义控件,专业来说是界面绘制,需要考虑很多方面,也是一个考验开发人员的技能点。此系列文章将重点着于界面绘制(自定义控件)展开,归纳总结相关知识技术,基于其根本,拓展UI相关重点部分,最终达成界面绘制学习实践目的。涉及到的内容约为以下部分:

  • Paint、Canvas、Path绘制学习及实践
  • 高级渲染与UI绘制流程
  • 事件传递与分发机制应用
  • 动画(属性动画、MD动画)学习及扩展
  • 自定义控件实例编写(界面绘制)



此系列的第一步由graphics包中Paint类归纳开始,它是基本的绘制工具,可搭配Canvas轻松完成图形、文字绘制,更有各种渲染器、颜色过滤器实现高级绘制。此篇涉及到的知识点如下:

  • Paint绘制图形、文字的重点API学习
  • 剖析文字绘制中“基线”的奥秘
  • Paint的 五大类着色器实现高级渲染
  • 自定义控件实践(进度条、圆形头像、歌词显示器)

一. Paint的基本使用

这里写图片描述

Paint类可根据设置的风格(style)和颜色(color)信息来绘制几何图形(geometry)、文字(text)、图片(bitmap)。

方法返回值 Paint相关方法 方法含义
void reset() 重置Paint相关设置
void setColor(int color) 设置Paint画笔颜色
void setAlpha(int a) 设置画笔颜色的透明度,范围是[0,255]

一般将Paint绘制分为两部分,即图形相关绘制和文字相关绘制,以下来详细介绍。


1. 图形绘制相关

方法返回值 Paint相关方法 方法含义
void setStyle(Paint.Style style) 设置画笔的风格,用来描述最初的几何图形如何展现(Paint.Style.FILL、Paint.Style.FILL_AND_STROKE、Paint.Style.STROKE)
void setStrokeWidth(float width) 设置Paint画笔描边(stoking)时的宽度
void setStrokeCap(Paint.Cap cap) 设置画笔笔帽(Paint.Cap.BUTT、Paint.Cap.ROUND、Paint.Cap.SQUARE)
void setStrokeJoin(Paint.Join join) 设置画笔的Join,即交汇处的展示形式(Paint.Join.MITER、Paint.Join.ROUND、Paint.Join.BEVEL)
void setAntiAlias(boolean aa) 设置或清除ANTI_ALIAS_FLAG,图形保真,消除混叠现象,使几何图形边缘平滑过渡,对图形内部无影响。(注:会损耗性能)
void setDither(boolean dither)) 设置或清除DITHER_FLAG,抖动在高版本上会影响颜色绘制,使用图像抖动处理会使绘制的图片等颜色更加的清晰以及饱满。(注:会损耗性能)

(1)setStyle

paint.setStyle(Paint.Style style)
  • API作用:设置画笔的风格,用来描述最初的几何图形如何展现。
  • 参数类型Paint.Style:
    • FILL:填充;
    • FILL_AND_STROKE:填充并描边;
    • STROKE:描边

注意:很明显三种类型的区别就在于是否“描边”,而这里的描边也就是Paint画笔绘制时的宽度,需要搭配setStrokeWidth理解,线条宽度。单位为像素,默认值是 0。

如下图实例,在设置画笔paint宽度之后,绘制圆时设定半径为100,则在FILL_AND_STROKE模式下的圆实际半径有120,因为还包含了画笔的宽度20,STROKE模式下的圆只呈现出描边的宽度20。

mPaint.setStrokeWidth(20);//画笔的宽度

//设置画笔的样式
mPaint.setStyle(Paint.Style.FILL);//填充内容
canvas.drawCircle(200, 200, 100, mPaint);

mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
...

mPaint.setStyle(Paint.Style.STROKE);//描边
...

这里写图片描述

(2)setStrokeCap

paint.setStrokeCap(Paint.Cap cap)
  • API作用:设置画笔笔帽。
  • 参数类型Paint.Cap:
    • BUTT:没有
    • ROUND:圆形笔帽
    • SQUARE:方形笔帽

注意:此API多用于线条之间的绘制,决定图形初始端和末端的结尾形式,ROUND相当于BUTT则是多出一段圆形笔帽,整体看起来更圆润;而SQUARE则是方形笔帽,看似与BUTT无差别,但实际长度会多出一截。

(当所绘制的图形宽度为1个像素时,三者区别并无差异,若是宽度较为明显,则根据器模式展现出不同样式)

这里写图片描述

(3)setStrokeJoin

paint.setStrokeJoin(Paint.Join join)
  • API作用:设置画笔的Join,即图形交汇处的展示形式。
  • 参数类型Paint.Join:
    • MITER:尖角
    • ROUND:圆角
    • BEVEL:平角

注意:设置图形的交汇展示形式,重点在于“交汇”,直接调用drawLine没有效果,以下例子是采用path进行展示,path.lineTo可体现出线条绘画的连接处。

如下图实例,根据不同的类型设置分别体现出量线条交汇处风格。

        mPaint.setStrokeWidth(30);//画笔的宽度
        mPaint.setStyle(Paint.Style.STROKE);//填充内容

        Path path = new Path();
        path.moveTo(100, 100);
        path.lineTo(300, 100);
        path.lineTo(100, 300);
        mPaint.setStrokeJoin(Paint.Join.MITER);
        canvas.drawPath(path, mPaint);

这里写图片描述

(另外还有个setStrokeMiter(float miter)是设置笔画的倾斜度,miter > = 0。 如小时候用的铅笔,削的时候斜与垂直削出来的笔尖效果是不一样的。 主要是用来设置笔触的连接处的样式。)



2. 文字绘制相关

(1)常用API总结

方法返回值 Paint相关方法 方法含义
void setTextSize(float textSize) 设置文字大小
void setTypeface(Typeface typeface) 设置文字格式类型(Typeface.BOLD、Typeface.BOLD_ITALIC、Typeface.ITALIC、Typeface.NORMAL)
void setStrikeThruText(boolean strikeThruText) 设置文本删除线,即STRIKE_THRU_TEXT_FLAG标识位
void setUnderlineText(boolean underlineText) 设置文本下划线,即UNDERLINE_TEXT_FLAG标识位
void setTextSkewX(float skewX) 设置文本倾斜长度(默认0,官方推荐的-0.25f是斜体)
void setTextAlign(Paint.Align align) 设置文本对齐方式(Paint.Align.CENTER、Paint.Align.LEFT、Paint.Align.RIGHT)
float getFontSpacing() 根据字体当前所设置的类型、大小返回其行间距
float getLetterSpacing() 返回字符之间的间距
int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth) 测量文本,若超出maxsize 则停止测量
void getTextBounds(String text, int start, int end, Rect bounds) 返回包含所有字符的最小矩形,即参数中的bounds(暗含的原点为(0,0))
int getTextWidths(String text, float[] widths) 返回文本的字符数参数widths数组中含有每个字符的宽度(注:一个中文是2个字符)
float measureText(String text, int start, int end) 返回文本的宽度(注:只是一个粗略的结果 )

(2)基线baseline

下面需要强调的是与文字绘制相关的一个重点——基线。注意此部分的标题是“文字绘制”,但文字不仅限于中文,另包含标点符号、英文等其他语言,那么需要注意一个细节:文字之间的排序规则如何保证它们看起来错落有致,像一条直线。

这里写图片描述

查看以上图片中的三行文字:

  • 第一行的中文排列看起来十分规矩,6个字的最顶端和最底端都位于一条线上。
  • 第二行的英文排列相较于中文字体,各个字母的形态不尽相同,但摆放起来也算错落有致。前6个字母中有个别几个的最顶端并非位于同一条线,但最底端都位于同一条线上,直到最后一个字母“ g ”打破了这个规律,它的下部分多出了一截。
  • 第三行的中英文结合,英文中的“g”相较于其他中英文仍多出了一截,并且中文的最顶端高于英文的最顶端。

规则概念

通过以上的例子简单认识到文字排序的规则:

  • 中文排列规则:汉字以一字见方的正方形框架为基准定位,笔画在字框内居中并充满字框。原则上并不存在基线,只有字框和字框中心。
  • 西文排列规则:即“基线”概念,是西文字体设计与排版的概念,源自西文字母的主体底部对齐的位置。
  • 中西文混排规则:要考虑汉字和西文的纵向对齐关系,通过基线高度、字号等尺度参数来调节整体的视觉效果。

西文字体中线条含义

由于西文的特殊性引入了“基线”的概念,特别是在混排时,中西文也是根据“基线”来进行调整。在了解“基线”核心——字母的主体底部对齐的位置,这条“基线”具体是指哪个部分呢?

这里写图片描述

  • 红线baseline:基线
  • 蓝线descent:文字底部
  • 黄线ascent:文字最顶部
  • 绿线top、bottom:最大高度
  • 紫线:中心(非必要)

以上就是这几条线的定义,最简单的是接触文字的顶部ascent底部descent;再者是文字的最大顶部top最大底部bottom,它与文字相间稍许距离;最后是基线baseline,位于一个中下方的位置,即字母的主体底部对齐的位置

获取线条高度API

这里写图片描述

        FontMetrics fontMetrics = mPaint.getFontMetrics();
        fontMetrics.top;
        fontMetrics.ascent;
        fontMetrics.descent;
        fontMetrics.bottom;

如上代码,官方提供了API来获取对应线条的值,但时只能根据API获取 最大顶部top、最大底部bottom、文字顶部ascent、文字底部descent这四个值。“基线”本身是一个抽象的概念,因此所有的四个值都是以基线baseLine为基准来计算的。即baseline以上的值(最大顶部top、文字顶部ascent)是负的,以下的值(最大底部bottom、文字底部descen)是正的。



3. 自定义控件中的文字摆放

Canvas.drawText(String text, float x, float y, Paint paint)

在自定义控件中难免涉及到文字的绘画,在Canvas类中有一个方法如上,顾名思义就是根据画笔、坐标设置绘画出文字,需要注意的是:根据给出的x、y坐标绘画文字的细节,这个坐标是基于文字的左上角还是最左部中心点?

这里写图片描述

都不是,此坐标是文字最左部基线处,即y坐标就是文字的基线处! 在做自定义控件的时候canvas.drawText(x,y)
这个y并不是text的左上角,而是以baseline为基准的。

因此在绘制自定义控件时,若未理解以上重点,很难达到“对齐”效果。

问题定位:如何可以做到真正的“对齐”?

唯一的难点是我们绘制文字时指定的坐标(x,y)是文字的基线baseline部分,并非是其top位置,导致文字难以对齐。如此看来,我们一般可轻易获取到一个已知的y值,这个y值可能是文字部分的topY值,或者是其centerY值,但不应直接将y值设置到drawText方法中,应当根据此y值求得baselineY值,再调用方法绘制。

分情况讨论

如上问题已定位到在已知一个 y值的情况下,如何求得baselineY值?这里分两种最经典的情况进行讨论:自定义控件内部是纵向和横向,即已知的y值是topY和centerY。以此进行举例:

  • 已知的y值是topY: 例如内部是纵向的自定义控件,需要文字紧接于图形下方。已知其顶点,应根据此topY计算出baselineY,因为文字是以基线为标准的,若直接使用会导致整体文字偏上,与图形重叠。
  • 已知的y值是centerY: 例如内部是横向的自定义控件,需要文字与左边的图形对齐。已知其中心点,应根据此centerY计算出baselineY,因为文字是以基线为标准的,若直接使用会导致整体文字偏上,并未与左边图形达到对齐效果。

(1)已知top值,求baselineY值?(例如内部是纵向的自定义控件)

这个top值,也就是已知的y值,也是手机屏幕坐标中的y值,但要是要获取基于手机屏幕的baseline值,还需要加上baseline线到top线之间的距离。前面已经讲解过,可根据API获得基于baseline坐标的top值(即baseline线到top线之间的距离),需注意该数值是负数,因此计算时使用“-”而不是“+”,公式如下:

//已知绘制文字部分的 top值(基于手机屏幕坐标),求baseline值(基于手机屏幕坐标)?

    ......
    FontMetrics fontMetrics = mPaint.getFontMetrics();
    baselineY = topY - fontMetrics.top;  //⭐️⭐️⭐️⭐️⭐️
    ......
    canvas.drawText("abcdefg", 100, baselineY, mPaint);

实例

这里写图片描述

查看上图实例展示,已知红线的y坐标,而我们的需求就是将文字摆放在红线下方此时红线的y坐标相当于是文字部分的top值,这里将文字赋予了两种不同的y值进行绘制:

  • 红体字:直接使用红线的y值调用canvas.drwaText 进行绘制,再次验证了之前的理论:该坐标是以文字的基线为标准的,即给出的坐标实际上是绘画出文字的基线最左侧坐标,从而导致文字整体偏上,与红线交叠,与需求严重不符!
  • 黑体字:根据以上公式,由topY值(即红线的y值)计算出baselineY值,再调用方法进行绘制,黑体字正处于红线下方,满足需求效果!

(2)已知centerY值,求baselineY值?(例如内部是横向的自定义控件)

这个已知的y值,也就是文字部分需要对齐的标准值,即文字部分的centerY值(下图中的紫线,也是文字的中心线),但要是要获取基于手机屏幕的baseline值,还需要加上baseline线到centerY线之间的距离如下图展示,这段距离用a表示,同样借助已知API求出已知值,由于centerY是中心线,因此centerY到top、bottom的距离相同,可求出centerY到bottom的距离,再根据API获得基于baseline到bottom的距离,前者减去后者即可得到a的值。

这里写图片描述

//已知绘制文字部分的 centerY值(基于手机屏幕坐标),求baseline值(基于手机屏幕坐标)?

    ......
    FontMetrics fontMetrics = mPaint.getFontMetrics();
    baselineY = centerHeight + (fontMetrics.bottom-fontMetrics.top)/2 - fontMetrics.bottom;
  //⭐️⭐️⭐️⭐️⭐️
    ......
    canvas.drawText("abcdefg", 100, baselineY, mPaint);

实例

这里写图片描述

查看上图实例展示,已知红线的y坐标,而我们的需求就是将文字与红线对齐,即红线就是文字部分的中心线此时红线的y坐标相当于是文字部分的centerY值,这里将文字赋予了两种不同的y值进行绘制:

  • 红体字:直接使用红线的y值调用canvas.drwaText 进行绘制,与上例同理,与需求严重不符!
  • 黑体字:根据以上公式,由centerY值(即红线的y值)计算出baselineY值,再调用方法进行绘制,黑体字正与红线对齐,满足需求效果!

小结

通过以上讲解得知在进行文字绘制时,需要注意其“基线”概念、明确字体内部几条线的API获取方法、CanvasdrawText 的内含重点,若要真正做到摆放整齐、对齐,其y值需要另外计算。



4.实例——自定义进度条控件

这里写图片描述

在了解Paint画笔的基本用法API,完成简单的自定义控件是不费吹灰之力。例如上图中的进度条展示,这是一个典型又简单的自定义控件。

定义一个CustomProgressBar 类,继承自View ,重写它的onDraw方法进行绘制,同时定义资源文件attr.xml中的相关属性。以上是自定义控件的常规步骤,重点就在于重写的onDraw方法,主要可分为以下3个步骤:

  • 绘画默认的大圆环
  • 绘画文字——进度百分比
  • 绘画显示进度的圆弧

以上3个步骤都是通过设置Paint画笔相关属性,最后使用Canvas 的简单API绘制方法进行绘制,第1、3步骤绘制图形较为常规,都是一些简单的API方法,主要是第2步骤中绘制文字部分涉及到以上所讲解的有关文字“基线”问题,在设置其绘画位置需要先通过公式进行简单计算,才能达到“对齐中心”的效果。

核心代码如下:(完整代码文章末尾提供)


public class CustomProgressBar extends View {

    ......
    //成员变量属性值

    public CustomProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);

        paint = new Paint();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomProgressBar);
        //获取自定义控件属性
        ......
        typedArray.recycle();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //1. 画默认的大圆环
        int center = getWidth()/2;//中心坐标点
        float radius =  center-roundWidth/2;//半径
        paint.setColor(roundColor);
        paint.setStyle(Paint.Style.STROKE);//设置空心(描边)
        paint.setStrokeWidth(roundWidth);//圆环的宽度
        paint.setAntiAlias(true);
        canvas.drawCircle(center, center, radius , paint);

        //2. 画文字------进度百分比
        paint.setColor(textColor);
        paint.setStrokeWidth(0);//圆环的宽度
        paint.setTextSize(textSize);
        paint.setTypeface(Typeface.DEFAULT_BOLD);

        int percent = (int) (progress/(float)max * 100);
        if(percent!=0){
            canvas.drawText(percent+"%", (getWidth()-paint.measureText(percent+"%"))/2f, 
                    //y公式: float baselineY = centerY + (fontMetrics.bottom-fontMetrics.top)/2 - fontMetrics.bottom
                    getWidth()/2f-(paint.descent()+paint.ascent())/2f, 
                    paint);
        }

        //3. 画圆弧
        RectF oval = new RectF(center-radius, center-radius, center+radius, center+radius);//矩形区域,定义圆弧的形状大小
        paint.setColor(roundProgressColor);
        paint.setStrokeWidth(roundWidth);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawArc(oval , 0, 360*progress/max, false, paint);

    }

    //属性的get、set方法
    ......
}




二. Paint高级渲染

Shader setShader(Shader shader)

以上是Paint类的一个高级渲染使用方法,设置Shader到画笔中。这里涉及到一个新类Shader,以下是对它的解释:

这里写图片描述

着色器是在绘制过程中返回水平跨度颜色的对象的一个基础类。调用paint.setShader方法将Shader的子类设置其中,再用该画笔Paint 绘制的任何对象(除位图之外)将从着色器中获取它的颜色。

以上解释仍旧有些模糊,并且将Shader翻译成“着色器”,它是一个特有的概念,与直接设置颜色setColor不同的是,setShader它会设置出一套颜色变化的规则,而不单出只是设置一种颜色。理论解释到此为止,接下来直接用其子类来实践高级渲染的效果,Shader的子类有以下5个:

  • BimapShader位图的图像渲染器
  • LinearGradient线性渲染
  • RadialGradient环形渲染
    水波纹效果,充电水波纹扩散效果、调色板
  • SweepGradient梯度渲染(扫描渲染)
    微信等雷达扫描效果。手机卫士垃圾扫描
  • ComposeShader组合渲染

1. BimapShader位图的图像渲染器

BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)

API含义:使用一张位图作为纹理来对某一区域进行填充,参数依次:

  • bitmap:用来作为填充的位图;
  • tileX:X轴方向上位图的衔接形式;
  • tileY:Y轴方向上位图的衔接形式;

API参数注意:此API作用是对某一区域进行图片填充,后两位参数类型是 Shader.TileMode,具体有以下三种:

  • CLAMP:如果渲染器超出原始边界范围,则会复制边缘颜色对超出范围的区域进行着色
  • REPEAT:平铺形式重复渲染
  • MIRROR:在横向和纵向上以镜像的方式重复渲染位图。

原图:

这里写图片描述

CLAMP类型:

这里写图片描述

REPEAT类型:

这里写图片描述

MIRROR类型:

这里写图片描述

三种类型展示效果如上,可知若并非是纯色彩形的Bitmap,REPEAT、MIRROR类型的展示效果有些眼花缭乱,并非十分理想。若图片大小和展示大小差异较大,CLAMP类型下的显示效果也不尽人意。因此,应当根据实际情况合理选择类型展示。

自定义控件实例——圆形图像控件

圆形图像控件在各APP中运用广泛,在网上已有多种实现方式,不乏有一些优化版本,而此处只是为了实践以上API所实现的一个简单版本,效果如下:

这里写图片描述

为了实践BimapShader位图的图像渲染器,实现以上效果的简单版本,这里只需要对图片做出裁剪,同时运用到了Matrix 来设置像素矩阵调整大小,解决图片宽高不一致而无法获取中心图片的问题。

以下是核心代码,完整源码文章末尾提供

public class CircleImageView extends View {

    ......

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
        mPaint.setShader(bitmapShader);

        //设置像素矩阵,来调整大小,为了解决宽高不一致的问题。
        float scale = Math.max(mWidth, mHeight)*1.0f/Math.min(mWidth, mHeight);
        Matrix matrix = new Matrix();
        matrix.setScale(scale, scale);//缩放比例
        bitmapShader.setLocalMatrix(matrix);

        canvas.drawCircle(Math.min(mWidth, mHeight)/2f, scale*Math.max(mWidth, mHeight)/2f, Math.max(mWidth, mHeight)/2f, mPaint);
    }
}

2. LinearGradient线性渲染

LinearGradient(float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile);

API含义:实现某一区域内颜色的线性渐变效果,参数依次是:

  • x0:渐变的起始点x坐标
  • y0:渐变的起始点y坐标
  • x1:渐变的终点x坐标
  • y1:渐变的终点y坐标
  • colors:渐变的颜色数组
  • positions:颜色数组的相对位置,范围是[0, 1.0]。若指定为null,则均匀分布。
  • tile:平铺方式

API参数注意:这里的坐标位置参数指定并未代表只有这一块区域颜色线性变化,而是指颜色沿着这两点连线趋势线性渐变!例如下图中,xy皆变化,则变化趋势是沿着斜线的;y不变化,则颜色是沿着横向渐变;x不变化,颜色是沿着竖向渐变。

这里写图片描述

使用场景:歌词滚动。

        LinearGradient linearGradient = new LinearGradient(100, 100, 600, 600, colors, null, TileMode.REPEAT);
        paint.setShader(linearGradient);
        canvas.drawRect(100, 100, 600, 600, paint);

这里写图片描述

自定义控件实例——滚动歌词

LinearGradient的作用是线性渲染,即实现某一区域内颜色的线性渐变,上图也简单演示了该效果,但实际中实用最多的情况是“滚动歌词”。例如移动端上各音乐APP展示的歌词滚动:颜色的变化代表歌曲的演唱进度,这种颜色变化的原理正是线性渐变。以下为了实践此API实现了一个简单的自定义控件实例,效果如下:

这里写图片描述

以上只提供了一句歌词的颜色滚动,只是为了呈现此效果的实际运用,颇为简陋,读者了解其思路后可举一反三自行完善。其原理就是充分利用 线性渐变 API,步骤如下:

  • 设置好x、y坐标,即一块矩形区域(自定为3个文字大小)
  • 再设置线性变化的三种颜色:歌词本色、歌词变化色、歌词后色;
  • onDraw方法中设置矩阵变换Matrix使矩形区域向右移动,达到歌词颜色变换滚动效果;
  • onDraw方法最后使用postInvalidateDelayed来触发此方法被不断调用,矩形不断移动。

注意:根据以上API位置参数的讲解,这里的颜色线性渐变是沿着横轴变换,因此y轴不变;另外源码中的一个优化:由于onDraw方法会被多次调用,因此一些基本参数配置代码放到onSizeChanged方法中。

以下是核心代码,完整源码文章末尾提供

public class LinearGradientTextView extends TextView {

    //成员变量
    ......

    /*
     * onDraw方法会被调用多次,因此一些基本设置放到onSizeChanged中
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        paint = getPaint();
        //GradientSize=3个文字的大小
        String text = getText().toString();
        float textWidth = paint.measureText(text);
        int GradientSize =(int) (3*textWidth/text.length());
        linearGradient = new LinearGradient(-GradientSize, 0, 0, 0, colors, new float[]{0,0.5f,1}, TileMode.CLAMP);//边缘融合
        paint.setShader(linearGradient);
        matrix = new Matrix();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float textWidth = getPaint().measureText(getText().toString());
        translateX += deltaX;
        if(translateX > textWidth + 1|| translateX < 1){
            return;
        }
        matrix.setTranslate(translateX, 0);
        linearGradient.setLocalMatrix(matrix);

        //歌词颜色不断变化
        postInvalidateDelayed(50);
    }

}

3. RadialGradient环形渲染

public RadialGradient (float x, float y, float radius, int[] colors, float[] positions, Shader.TileMode tile);

API含义:实现某一环形区域内颜色的环形渐变效果,参数依次是:

  • x:环形的圆心x坐标
  • y:环形的圆心y坐标
  • radius:环形的半径
  • colors:环形渐变的颜色数组
  • positions:指定颜色数组的相对位置,范围是[0, 1.0]。若指定为null,则均匀分布。
  • tile:平铺方式

API参数注意:以上API解释为是实现环形区域内的颜色渐变,查看参数
,colors颜色数组代表需要指定的颜色渐变顺序。而positions位置数组则对应颜色数组,指定颜色占据环形区域内的位置。

使用场景:水波纹效果(充电水波纹扩散效果)、调色板。

radialGradient = new RadialGradient(500, 500, 300, colors, null, TileMode.REPEAT);
paint.setShader(radialGradient);
canvas.drawCircle(500, 500, 300, paint);

这里写图片描述


4. SweepGradient梯度渲染

public SweepGradient (float cx, float cy, int[] colors, float[] positions)

API含义:扫描渲染,就是以某个点位中心旋转一周所形成的效果。参数依次是:

  • cx:扫描的中心x坐标
  • cy:扫描的中心y坐标
  • colors:梯度渐变的颜色数组
  • positions:指定颜色数组的相对位置,范围是[0, 1.0]

注意: SweepGradientRadialGradient的区别,一个是扫描式的渲染(一个颜色占据区域的一部分),一个是环形式的渲染(一个颜色占据环形的一圈),因此前者创建的参数中并无半径、平铺方式。

使用场景:雷达扫描效果,手机卫士垃圾扫描。(动态改变颜色位置即可达到目的)

    sweepGradient = new SweepGradient(500, 500, colors, null);
    paint.setShader(sweepGradient);
    canvas.drawCircle(500, 500, 300, paint);

这里写图片描述


5. ComposeShader组合渲染

ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

API含义:渲染效果的叠加,参数依次:

  • shaderA:第一种渲染效果
  • shaderB:第二种渲染效果
  • mode:两种渲染效果的叠加模式

看到PorterDuff就知道什么了吧?比如将BitmapShaderLinearGradient的混合渲染效果等。

这里写图片描述

上面这张图从一定程度上形象地说明了运用PorterDuff.Mode进行图像合成的作用,两个图形一圆一方通过一定的计算产生了不同的合成效果,我们在实际工作中需要做图片处理时可以参考这张图的示意快速地选择合适的Mode。

PorterDuff.Mode类型全解:

  • CLEAR    所绘制不会提交到画布上
  • SRC     显示上层绘制图片
  • DST     显示下层绘制图片
  • SRC_OVER  正常绘制显示,上下层绘制叠盖。
  • DST_OVER  上下层都显示。下层居上显示。
  • SRC_IN    取两层绘制交集。显示上层。
  • DST_IN    取两层绘制交集。显示下层。
  • SRC_OUT   取上层绘制非交集部分。
  • DST_OUT   取下层绘制非交集部分。
  • SRC_ATOP  取下层非交集部分与上层交集部分
  • DST_ATOP  取上层非交集部分与下层交集部分
  • XOR     异或:去除两图层交集部分
  • DARKEN   取两图层全部区域,交集部分颜色加深
  • LIGHTEN   取两图层全部,点亮交集部分颜色
  • MULTIPLY  取两图层交集部分叠加后颜色
  • SCREEN    取两图层全部区域,交集部分变为透明色



文章小结

本篇内容核心围绕Paint使用API展开进行归纳总结,大部分API方法易懂,使用简单。主要重点在于文字绘制中对“基线”进行深度剖析,其运算公式的总结需要注意,还有第二大点开始介绍Paint的高级渲染方法—-五大“着色器”学习归纳。

整体而言,此篇文章较为基础,更偏向科普性质,笔者同时结合API做了大量的简单demo,体现API之间使用的差异性,并随之拓展实践了几个简单的自定义控件(进度条、圆形头像、歌词显示器)。由于Paint部分较多,还有过滤器矩阵变换相关内容留给下一篇继续归纳。


笔者废话

在我之前做的项目中涉及到一些或简单、或复杂的自定义控件等,大部分都是百度copy他人现成的结果学习使用,很少亲自动手实践,这一整个界面绘制流程并不熟练,在之前校招面试还被问到给出一个具体UI需求的实现思路,确实认识到其重要性,狠下心来系统性地学习归纳UI相关知识点,因而得出此系列。(不得不说写博客归纳这些知识点时间消耗太大了,进度颇慢,但还是会坚持完成,其收获我心自知!我觉得我可以不做程序员当编辑写文章去咯~)


(代码整理中,后续会提供)

若有错误,欢迎指教~

猜你喜欢

转载自blog.csdn.net/ITermeng/article/details/77968042