作者:时光少年
链接:
https://juejin.cn/post/7343915907938451483
本文由作者授权发布。
前言
就在3月7日-3月8日,女神们迎来了自己的节日,有些人过了“慷慨的”半天假期,有些人还是一直忙碌,更有一些人都忘记甚至不知道有这个节日。工作的忙碌,成了所有人忙碌的生活,时间的流逝,让我们忘记了年轻时的自己。人生本就苦短,有几次能清风拂面,心旷神怡的日子,特别是对于大部分女孩、妻子、母亲而言,从人生的起点到终点,有干不完的家务,走不完的曲折之路,消除不了的工作壁垒.....,对于她们,关心是非常必要的。
本篇主要利用“点阵体”实现文字特效,我们先来了解下什么是“点阵体”呢?在字体发展的历史上,鉴于一些显示器本身的展示清晰度并不高,一些情况下,文字的展示使用类似LED那种展示,LED的话,只要确定LED单元在二维数组的点位索引,通过一组LED同时发光,就能展示出特定的文字。其实,早期的点阵体文字缺陷也是非常明显,如果要换一个显示器,那么意味着由需要重新排列,此外,最大的缺陷是不能缩放、拉伸,颜色控制起来也比较复杂。后来,随着显示技术的发展,诞生了使用矢量字体来处理这种问题的方法。关于矢量字体
当然,当前的字体更加发达,使用了大量的贝塞尔曲线来进行绘制,因为其不仅仅便于描述路径,而且还能设计出笔锋,可想而知,如果没有贝塞尔曲线,字体的可能会非常昂贵。
其实,在本篇之前,我们利用实现过自定义矢量字体,文章可参考《Android 自定义液晶体数字》,在这篇文章中,我们虽然没有使用到贝塞尔曲线,但总体上来说,本篇实现的字体也是矢量字体。
关于点阵字体
实际上,目前使用点阵体的很多都是LED霓虹灯之类的,用途其实已经远远被矢量字体超越了,甚至一些情况下,点阵体也是通过矢量字体生成的,通过这种方式,就可以规避点阵体的无法缩放、压扁等问题。在Android中,Paint都自动加载了矢量字体,我们之前有一篇文章《Android 实现LED展示效果》中,专门使用了这种方式来生成点阵体文字。
https://juejin.cn/post/7304973928039153705
目前,点阵体还有一个用途就是文字识别了,很多文字识别软件依靠点阵计算出相似度,匹配出相应的文字。通过上面的对比我们可以知道,矢量字体可以实现点阵字体,但反过来是不行的。
本篇,我们实际上也是通过矢量字体来生成点阵,当然,本篇我们会使用一种更加巧妙的方式,在点阵范围内实现文字、表情符、圆圈等绘制。主要分为以下步骤:这里,我们有遇到了碰撞检测,实际上,利用图片分片也是可以的,但是这里用碰撞检测的原因是可以在更小范围内计算出重合点。我们之前的文章中,介绍过碰撞检测,通过《Android Region碰撞检测问题优化》我们知道,Region的碰撞可以被优化,不需要借助算法就能实现碰撞检测。
https://juejin.cn/post/7310412252552085513
初始化Paint
接下来我们实现本篇的内容,首先,Paint初始化是必要的常规步骤,这里要注意一个问题,如果使用了BlendMode,可能绘制不了文字。private void initPaint() {
//否则提供给外部纹理绘制
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(false);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setTextSize(dp2px(150));
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
//PaintCompat.setBlendMode(mPaint, BlendModeCompat.LIGHTEN);
}
初始化关键变量
下面初始化变量,用来记录重合点位、碰撞区域检测,其中,我们本篇会用到Region,Region的用法可以参考我之前的文章。//记录点位,保存重合点位
List<Particle> points = new ArrayList<>();
Path textPath = new Path(); // 文本路径
//文本区域,这里是指文字路径区域,并不是文字大小区域
Region textPathRegion = new Region();
//这里才是文本区域
Region mainRegion = new Region();
//文本区域边界
RectF textRect = new RectF();
//分片区域边界
Rect measureRect = new Rect();
当然,我们这里也要将点位作为粒子存储,下面是粒子的描述对象,另外,radius不能是小于0的,否则会出现文本测量异常。static class Particle extends PointF {
float t; //矢量性质,用来调整radius的伸缩值
int color; // 颜色
float radius; //当前半径
float range; //半径最大区域
public void update() {
radius = radius + t;
if (radius >= range || radius <= 0) {
t = -t;
}
if (radius < 0) {
radius = 0; //防止小于0时,文字测量异常
}
if(radius > range){
radius = range;
}
}
}
测量和提取Path
为什么要提取Path呢,提取Path是为了Region服务,这样可以计算出文字染色的区域。这里首先要记住,提取的Path前最好计算出文本的展示位置,否则,绘制的时候需要做一些平移。另外一点是,最好在提取Path之前设置字体,也是方便区域测量,减少后期缩放操作。float measureTextWidth = mPaint.measureText(text, 0, text.length); //测量文本宽度
float baseline = getTextPaintBaseline(mPaint); //获取基线
mPaint.getTextPath(text, 0, text.length, -(measureTextWidth / 2f), baseline, textPath);
下面,将Path设置到Region中,通过下面的操作,Region只包含文字染色的区域,其他区域都是外部区域,textPath.computeBounds(textRect, true);
mainRegion.set((int) textRect.left, (int) textRect.top, (int) textRect.right, (int) textRect.bottom);
textPathRegion.setPath(textPath, mainRegion);
碰撞检测
这里,我们使用文本区域,而不是Canvas 区域,可以有效减少计算量,而step是分片(矩形)单元的大小,我们通过measureRect计算出中心点,然后利用上面的Region的contains方法去检测点位,而我们知道,contains是精确测量,因此,也要避免性能问题。float textRectWidth = textRect.width(); //文本区域
float textRectHeight = textRect.height(); //文本区域
int step =3;
int index = 0; //容器索引
for (int i = 0; i < textRectHeight; i += step) {
for (int j = 0; j < textRectWidth; j += step) {
int row = (int) (-textRectHeight / 2 + i * step);
int col = (int) (-textRectWidth / 2 + j * step);
measureRect.set(col, row, col + step, row + step);
//检测下面点位是不是和文本的染色区域重合
if (!textPathRegion.contains(measureRect.centerX(), measureRect.centerY())) {
continue; //如果不重合,这个位置不能绘制
}
//从容器中取出粒子
Particle p = points.size() > index ? points.get(index) : null;
index++;
if (p == null) {
p = new Particle();
points.add(p);
}
//保存点位
p.x = measureRect.centerX();
p.y = measureRect.centerY();
int randomInt = 1 + (int) (Math.random() * 100);
float t = (float) ((randomInt % 2 == 0 ? -1f : 1f) * Math.random());
p.color = Color.BLACK;
p.radius = (float) (step * random);
p.range = step * 5;
p.t = t;
}
}
while (points.size() > index + 1) { //删除脏数据
points.remove(points.size() - 1);
}
for (int i = 0; i < points.size(); i += 2) {
Particle point = points.get(i);
mPaint.setColor(point.color);
canvas.drawCircle(point.x,point.y,point.radius,mPaint);
point.update();
}
invalidate();
canvas.drawCircle(point.x,point.y,point.radius,mPaint);
private float floatRandom(){
return (float)Math.random();
}
....
//设置颜色
p.color = argb(floatRandom(),floatRandom(),floatRandom(),floatRandom());
有些凌乱,稍微挑战下背景和点位密度,将View背景设置为黑色,然后step设置为5。还是不够亮,不够亮的问题怎么解决,当然是使用HLS或者HSV的颜色空间了,下面我们使用HSL来优化亮度,hSL的第三个参数为两度,为0.5时最亮,过界或者小于这个值亮度会递减。第二个值为色彩饱和度,第一个值为色相。- 色相(H) 是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。
- 饱和度(S) 是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取 0-1f 的数值。
- 亮度(L) ,取 0-1f,增加亮度,颜色会向白色变化;减少亮度,颜色会向黑色变化。
hsl[0] = (float) (random * 360); //色相
hsl[1] = 0.5f; // 饱和度 - 颜色的浓度
hsl[2] = 0.5f; //最亮 - 颜色的两度
p.color = HSLToColor(hsl);
好看多了。
文字着色
这里我们用字幕C去绘制,感觉还好,就是亮度变差了,可能笔划不够粗,后续优化的可以取一些亮色的颜色。
表情符着色
动图实现
动图实际上,我们要调整radius或者textSize实现文本动起来
主要功能已经实现了,下面,我们,我们实现一些动画效果,首先是表情符动起来 当然,这里要让表情符动起来,需要注意基线要根据TextSize重新计算,我们可以把radius设置到Paint.setTextSize中。 text = "🤭".toCharArray();
由于表情符属于文字,因此需要调整 textSize。
是不是很简单呢,实际上,核心功能已经实现了,我们实现开头的效果看一下,下面改成画圆。当然绘制圆是不需要设置textSize的,调整radius即可。
祝福文字
本篇到这里就结束了,本篇的核心内容就是利用点阵字体的思想,实现文字描绘特效,在本篇,我们还回忆了以前几篇文章,其中最重要的还是“碰撞检测”相关,另外我们也可以了解到字体设计的核心思想,以及如何让颜色变的更亮。public class WordParticleView extends View {
private char[] text = "永远的女神".toCharArray();
private final DisplayMetrics mDM;
Paint.FontMetrics fm = new Paint.FontMetrics();
boolean shouldUpdateTextPath = true;
private TextPaint mPaint;
{
mDM = getResources().getDisplayMetrics();
initPaint();
}
private void initPaint() {
//否则提供给外部纹理绘制
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(false);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setTextSize(dp2px(150));
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
PaintCompat.setBlendMode(mPaint, BlendModeCompat.LIGHTEN);
}
public WordParticleView(Context context) {
this(context, null);
}
public WordParticleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WordParticleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize;
}
setMeasuredDimension(widthSize, heightSize);
}
public void setText(String text) {
if (text == null) return;
shouldUpdateTextPath = true;
this.text = text.toCharArray();
}
//记录点位,保存重合点位
List<Particle> points = new ArrayList<>();
Path textPath = new Path(); // 文本路径
//文本区域,这里是指文字路径区域,并不是文字大小区域
Region textPathRegion = new Region();
//这里才是文本区域
Region mainRegion = new Region();
//文本区域边界
RectF textRect = new RectF();
//分片区域边界
Rect measureRect = new Rect();
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
shouldUpdateTextPath = true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (text == null) return;
int saveRecord = canvas.save();
canvas.translate(getWidth() / 2f, getHeight() / 2f);
float measureTextWidth = mPaint.measureText(text, 0, text.length);
float baseline = getTextPaintBaseline(mPaint);
final int step = 3; //步长
if (shouldUpdateTextPath) {
textPath.reset();
mPaint.getTextPath(text, 0, text.length, -(measureTextWidth / 2f), baseline, textPath);
shouldUpdateTextPath = false;
//染色区域设置
textPath.computeBounds(textRect, true);
mainRegion.set((int) textRect.left, (int) textRect.top, (int) textRect.right, (int) textRect.bottom);
textPathRegion.setPath(textPath, mainRegion);
float textRectWidth = textRect.width();
float textRectHeight = textRect.height();
int index = 0;
for (int i = 0; i < textRectHeight; i += step) {
for (int j = 0; j < textRectWidth; j += step) {
int row = (int) (-textRectHeight / 2 + i * step);
int col = (int) (-textRectWidth / 2 + j * step);
measureRect.set(col, row, col + step, row + step);
if (!textPathRegion.contains(measureRect.centerX(), measureRect.centerY())) {
continue;
}
Particle p = points.size() > index ? points.get(index) : null;
index++;
if (p == null) {
p = new Particle();
points.add(p);
}
p.x = measureRect.centerX();
p.y = measureRect.centerY();
double random = Math.random();
hsl[0] = (float) (random * 360);
hsl[1] = 0.5f;
hsl[2] = 0.5f; //最亮
p.color = HSLToColor(hsl);
int randomInt = 1 + (int) (Math.random() * 100);
float t = (float) ((randomInt % 2 == 0 ? -1f : 1f) * Math.random());
p.radius = (float) (step * random);
p.range = step * 5;
p.t = t;
}
}
while (points.size() > index + 1) {
points.remove(points.size() - 1);
}
}
float textSize = mPaint.getTextSize();
for (int i = 0; i < points.size(); i += 2) {
Particle point = points.get(i);
mPaint.setColor(point.color);
canvas.drawCircle(point.x,point.y,point.radius,mPaint);
point.update();
}
mPaint.setTextSize(textSize);
canvas.restoreToCount(saveRecord);
postInvalidateDelayed(0);
}
public float getTextPaintBaseline(Paint p) {
p.getFontMetrics(fm);
Paint.FontMetrics fontMetrics = fm;
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
public static int argb(float alpha, float red, float green, float blue) {
return ((int) (alpha * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}
float[] hsl = new float[3];
@ColorInt
public static int HSLToColor(@NonNull float[] hsl) {
final float h = hsl[0];
final float s = hsl[1];
final float l = hsl[2];
final float c = (1f - Math.abs(2 * l - 1f)) * s;
final float m = l - 0.5f * c;
final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
final int hueSegment = (int) h / 60;
int r = 0, g = 0, b = 0;
switch (hueSegment) {
case 0:
r = Math.round(255 * (c + m));
g = Math.round(255 * (x + m));
b = Math.round(255 * m);
break;
case 1:
r = Math.round(255 * (x + m));
g = Math.round(255 * (c + m));
b = Math.round(255 * m);
break;
case 2:
r = Math.round(255 * m);
g = Math.round(255 * (c + m));
b = Math.round(255 * (x + m));
break;
case 3:
r = Math.round(255 * m);
g = Math.round(255 * (x + m));
b = Math.round(255 * (c + m));
break;
case 4:
r = Math.round(255 * (x + m));
g = Math.round(255 * m);
b = Math.round(255 * (c + m));
break;
case 5:
case 6:
r = Math.round(255 * (c + m));
g = Math.round(255 * m);
b = Math.round(255 * (x + m));
break;
}
r = constrain(r, 0, 255);
g = constrain(g, 0, 255);
b = constrain(b, 0, 255);
return Color.rgb(r, g, b);
}
private static int constrain(int amount, int low, int high) {
return amount < low ? low : Math.min(amount, high);
}
static class Particle extends PointF {
float t;
int color;
float radius;
float range;
public void update() {
radius = radius + t;
if (radius >= range || radius <= 0) {
t = -t;
}
if (radius < 0) {
radius = 0;
}
if(radius > range){
radius = range;
}
}
}
}
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!