前言
话说我们之前的一些文章中"玩"过一些"文字游戏",哈哈,正经的说,应该被称为:文本处理。
主要有三篇
- 包括Html自定义引擎
- 文本展示优化
- 女神节点阵体特效
有兴趣的同学可以看看我之前的文章。
今天是母亲节,其实这个日子很容易被忽略,同样父亲节也是,我们常常说要孝敬父母,然而,这两个假期连调休都没有。
之前,写点阵体特效特效一文恰逢「3·7女生节」和「3·8妇女节」,因此祝福了一下各位女生。当今的女性同胞们也非常优秀,也很有主见。但是在大环境比较差的时候,女性同胞受到的歧视风险和失业风险比以前更多。正如之前的一篇文章中说过,广大妇女们有干不完的家务和走不完的曲折之路,当然,不可否认有的女性地位很高,然而,我们也要尊重事实。
扯的多了,咱来了解下技术。
Android布局和绘制是一项非常重要的技能,无论Compose还是传统的View,都需要进行测量、布局和绘制。
但是,一些技能的掌握往往需要长时间的沉淀,当然,也要对现象进行总结。
有的时候,我们可能目标是做其他事,但是往往实现了意想不到的事。
正如这句话,我本来打算将字体录入GestureLibrary中,进行文字识别的,然而,在实现过程中发现,描边路径会增加字体的笔画,显然不适合录入手势库。然后我尝试将当个文字绘制到Bitmap,扩大采样步长,但无法确定起始点,因此也无法录入手势库。
当然,我们知道软键盘可以通过手写输入,理论上利用点阵体(HZK16字体文件)+ 汉明距离实现相似计算也可以实现,但是这种方式性能应该不高,后续我们研究下其他方案。
本篇效果
如下,就是本篇的实现效果
在这里,我们可以一个字一个字的绘制,当然,一个字一个字的绘制太慢了,作为开发者,咱最好不要浪费用户的时间。
尤其是某些地图类和打车类,动不动弄个开屏广告(带视频的),特别是用户设备电量不足时还弹出来,弄不好没电关机了,车到了都却联系不上司机。
上面的效果,步长太长,因此,点无法成线,下面将PathMeasure变短之后,效果就是连线了。
以上就是本篇的效果,其实本篇我们使用PathMeasure + Text Path 来实现,我们还可以用来实现3D文字或者描边特效。
原理
PathMeasure采样
在之前文章中,我们实现过《链接:过山车效果》,在这篇文章中我们了解了PathMeasure的基本用法。
和以往不同的是,之前的文章中,我们之前是主动构建各种图形,本篇我们通过Paint获取字体的Path,提供给PathMeasure采样。
注意点
Path长度问题
这里要注意的是,如果采样的是多个文字的Path,那么PathMeasure#getLength值默认是第一个文字的Path,而不是所有文字的总长。
Path 切换
我们拿到的Path是第一个文字的长度,那么如何切到下一个,这里就得使用pathMeasure#nextContour方法
采样
采样时需要注意的是,我们要将所有点拿到,之前博客通过PathMeasure#getSegment对片段采样,但不知道为什么有这种方式很多BUG,会出现不稳定的效果。
下面方式我们本篇不适用,会出现过程不稳定、不连续的问题
java
mPathMeasure.getSegment(0, mPathMeasure.getLength() * value, mAnimPath, true);
本篇我们使用的是提前采样,将所有的点保存下来,这样我们自己来控制点的展示,从而避免了不稳定不连续的问题。
java
public static List<PointF> textPathToPoints(String text, TextPaint paint) {
Path fontPath = new Path();
paint.getTextPath(text, 0, text.length(), 0f, paint.getFontSpacing(), fontPath);
fontPath.close();
PathMeasure pathMeasure = new PathMeasure(fontPath, false);
List<PointF> points = new ArrayList<>();
float[] pos = new float[2];
do {
float distance = 0f;
while (distance < pathMeasure.getLength()) {
distance += 5f;
pathMeasure.getPosTan(distance, pos, null);
points.add(new PointF(pos[0], pos[1]));
}
} while (pathMeasure.nextContour());
return points;
}
好了,这是本篇的原理,下面来看看实现部分
本篇实现
下面是本篇的关键代码
绘制信息
我们定义一个类来保存和控制单个文字的动画状态和数据。
arduino
static class FontText{
int index; //当前文字在Text中的索引,用来实现文字单个文字偏移计算
int currentSize; //用户控制绘制到什么位置
int color; // 颜色
List<PointF> pointFS; // 点位
public FontText(int index,List<PointF> textPathToPoints,int color) {
this.pointFS = textPathToPoints;
this.index = index;
this.color = color;
}
}
计算和保存点位
这里,我们接着将信息保存到textPoints中,textPoints是一个Map对象
java
if (textPoints.isEmpty()) {
measureTextWidth = mTextPaint.measureText(text); //总长度
for (int i = 0; i < text.length(); i++) {
String word = text.substring(i, i + 1);
textPoints.put(word,new FontText(i,textPathToPoints(word, mTextPaint),randomColor())); //单个文字的点位
}
}
绘制
下面是绘制部分,加了很多注释,主要做了如下几件事
- 将文字水平居中绘制
- 统计绘制完整的文字
- 所有文字一起测量
- 计算单个文字的X轴偏移
- 计算单个文字的Points
- 绘制文字
java
int height = getHeight();
int width = getWidth();
float halfOfTextWidth = measureTextWidth / 2f; //计算中心点一半的长度
float baseline = getTextPaintBaseline(mTextPaint); //计算BaseLine
int count = canvas.save();
canvas.translate(width / 2f, height / 2f); //平移到View中心点
float spanSize = measureTextWidth / textPoints.size();
int finishCount = 0; //统计完成绘制的文字总数
for (Map.Entry<String,FontText> entry : textPoints.entrySet()){
FontText textPoint = entry.getValue();
int size = textPoint.currentSize;
int pointSize = textPoint.pointFS.size();
float offset = textPoint.index * spanSize; //文字X轴方向的偏移
mPaint.setColor(textPoint.color);
for (int i = 0; i < size; i++) {
PointF pointF = textPoint.pointFS.get(i);
//绘制点
canvas.drawPoint(pointF.x - halfOfTextWidth + offset, pointF.y + baseline, mPaint);
}
textPoint.currentSize = Math.min(++size,pointSize);
if(textPoint.currentSize == pointSize){
finishCount++; // 当前绘制到的位置和pointSize
}
}
canvas.restoreToCount(count);
if(finishCount == textPoints.size()){
//所有的文字都完成绘制的,过1s之后重新绘制
for (Map.Entry<String,FontText> entry : textPoints.entrySet()){
entry.getValue().currentSize = 0;
}
postInvalidateDelayed(1000);
}else {
postInvalidateDelayed(16);
}
好了,到这里本篇就结束了,代码量不大,很容易阅读,有问题咱评论区见。
总结
本篇实际上也是点阵体,比较适合做一些文字特效,相比而言,要比之前的一篇文章要简单的一些,基本上就是四部
- 获取Path
- Path转Point集合
- Point绘制
下面我们贴出源码,供大家参考
源码
如下是本篇源码,没有特别的封装,使用的时候可能还需要你自行改造。
java
public class FontPathToPointsView extends View {
private TextPaint mTextPaint;
private DisplayMetrics mDM;
private String text = "母亲节快乐";
private float measureTextWidth;
float[] hslColor = new float[3];
private final Map<String,FontText> textPoints = new ArrayMap<String,FontText>();
private Paint.FontMetrics fm = new Paint.FontMetrics();
private TextPaint mPaint;
private void initPaint() {
mDM = getResources().getDisplayMetrics();
//否则提供给外部纹理绘制
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setAntiAlias(true);
mTextPaint.setDither(true);
mTextPaint.setStrokeCap(Paint.Cap.ROUND);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setTextSize(sp2px(50));
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeWidth(5f);
}
public FontPathToPointsView(Context context) {
super(context);
}
public FontPathToPointsView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public FontPathToPointsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
initPaint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
textPoints.clear();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (text == null) {
return;
}
if (textPoints.isEmpty()) {
measureTextWidth = mTextPaint.measureText(text);
for (int i = 0; i < text.length(); i++) {
String word = text.substring(i, i + 1);
textPoints.put(word,new FontText(i,textPathToPoints(word, mTextPaint),randomColor()));
}
}
if (textPoints.isEmpty()) {
return;
}
int height = getHeight();
int width = getWidth();
float halfOfTextWidth = measureTextWidth / 2f; //计算中心点一半的长度
float baseline = getTextPaintBaseline(mTextPaint); //计算BaseLine
int count = canvas.save();
canvas.translate(width / 2f, height / 2f); //平移到View中心点
float spanSize = measureTextWidth / textPoints.size();
int finishCount = 0; //统计完成绘制的文字总数
for (Map.Entry<String,FontText> entry : textPoints.entrySet()){
FontText textPoint = entry.getValue();
int size = textPoint.currentSize;
int pointSize = textPoint.pointFS.size();
float offset = textPoint.index * spanSize; //文字X轴方向的偏移
mPaint.setColor(textPoint.color);
for (int i = 0; i < size; i++) {
PointF pointF = textPoint.pointFS.get(i);
//绘制点
canvas.drawPoint(pointF.x - halfOfTextWidth + offset, pointF.y + baseline, mPaint);
}
textPoint.currentSize = Math.min(++size,pointSize);
if(textPoint.currentSize == pointSize){
finishCount++; // 当前绘制到的位置和pointSize
}
}
canvas.restoreToCount(count);
if(finishCount == textPoints.size()){
//所有的文字都完成绘制的,过1s之后重新绘制
for (Map.Entry<String,FontText> entry : textPoints.entrySet()){
entry.getValue().currentSize = 0;
}
postInvalidateDelayed(1000);
}else {
postInvalidateDelayed(16);
}
}
public static List<PointF> textPathToPoints(String text, TextPaint paint) {
Path fontPath = new Path();
paint.getTextPath(text, 0, text.length(), 0f, paint.getFontSpacing(), fontPath);
fontPath.close();
PathMeasure pathMeasure = new PathMeasure(fontPath, false);
List<PointF> points = new ArrayList<>();
float[] pos = new float[2];
do {
float distance = 0f;
while (distance < pathMeasure.getLength()) {
distance += 5f;
pathMeasure.getPosTan(distance, pos, null);
points.add(new PointF(pos[0], pos[1]));
}
} while (pathMeasure.nextContour());
return points;
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}
public float getTextPaintBaseline(Paint p) {
p.getFontMetrics(fm);
Paint.FontMetrics fontMetrics = fm;
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}
static class FontText{
int index; //当前文字在Text中的索引
int currentSize; //用户控制绘制到什么位置
int color; // 颜色
List<PointF> pointFS; // 点位
public FontText(int index,List<PointF> textPathToPoints,int color) {
this.pointFS = textPathToPoints;
this.index = index;
this.color = color;
}
}
private int randomColor() {
hslColor[0] = (float) (Math.random() * 360);
hslColor[1] = 0.5f;
hslColor[2] = 0.5f;
return HSLToColor(hslColor);
}
@ColorInt
static int HSLToColor(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);
}
}
本篇到这里结束了,我们后续的View、Compose定义相关文章会减少,会在性能和播放相关方面转移,,欢迎点赞加关注。