Android 点阵体文字特效,祝福永远的女神

前言

就在3月7日-3月8日,女孩们和女人们迎来了自己的节日,有些人过了"慷慨的"半天假期,有些人还是一直忙碌,更有一些人都忘记甚至不知道有这个节日。工作的忙碌,成了所有人忙碌的生活,时间的流逝,让我们忘记了年轻时的自己。人生本就苦短,有几次能清风拂面,心旷神怡的日子,特别是对于大部分女孩、妻子、母亲而言,从人生的起点到终点,有干不完的家务,走不完的弯路,消除不了的工作壁垒.....,对于她们,关心是非常必要的。

这里,本篇做此图祝福她们。

字体原理

本篇主要利用"点阵体"实现文字特效,我们先来了解下什么是"点阵体"呢?在字体发展的历史上,鉴于一些显示器本身的展示清晰度并不高,一些情况下,文字的展示使用类似LED那种展示,LED的话,只要确定LED单元在二维数组的点位索引,通过一组LED同时发光,就能展示出特定的文字。

其实,早期的缺陷也是非常明显,如果要换一个显示器,那么意味着由需要重新排列,此外,最大的缺陷是不能缩放,颜色控制起来也比较复杂。

后来,随着显示技术的发展,诞生了使用矢量字体来处理这种问题的方法。

关于矢量字体

矢量字体有以下特性

  • 可测量: 大小可以测量
  • 可以缩放: 大小可以调整
  • 矢量性质: 压扁、缩放不会失真
  • 颜色变化: 可以实现颜色

当然,当前的字体更加发达,使用了大量的贝塞尔曲线来进行绘制,因为其不仅仅便于描述路径,而且还能设计出笔锋,可想而知,如果没有贝塞尔曲线,字体的可能会非常昂贵。

其实,在本篇之前,我们利用实现过自定义矢量字体,文章可参考《Android 自定义液晶体数字》,在这篇文章中,我们虽然没有使用到贝塞尔曲线,但总体上来说,本篇实现的字体也是矢量字体。

以上是矢量字体,那点阵体现状如何呢?

关于点阵字体

实际上,目前使用点阵体的很多都是LED霓虹灯之类的,用途其实已经远远被矢量字体超越了,甚至一些情况下,点阵体也是通过矢量字体生成的,通过这种方式,就可以规避点阵体的无法缩放、压扁等问题。

在Android中,Paint都自动加载了矢量字体,我们之前有一篇文章《Android 实现LED展示效果》中,专门使用了这种方式来生成点阵体文字。

目前,点阵体还有一个用途就是文字识别了,很多文字识别软件依靠点阵计算出相似度,匹配出相应的文字。

本篇原理

本篇,我们实际上也是通过矢量字体来生成点阵,当然,本篇我们会使用一种更加巧妙的方式,在点阵范围内实现文字、表情符😊、圆圈等绘制。 主要分为以下步骤

  • 测量文字区域,用于确定展示位置
  • Path提取,提取文字的绘制Path
  • 碰撞检测,计算出重合点,确定粒子绘制位置索引。
  • 绘制图案

这里,我们有遇到了碰撞检测,实际上,利用图片分片也是可以的,但是这里用碰撞检测的原因是可以在更小范围内计算出重合点。我们之前的文章中,介绍过碰撞检测,通过《Android Region碰撞检测问题优化》我们知道,Region的碰撞可以被优化,不需要借助算法就能实现碰撞检测。

实现

初始化Paint

接下来我们实现本篇的内容,首先,Paint初始化是必要的常规步骤,这里要注意一个问题,如果使用了BlendMode,可能绘制不了文字。

java 复制代码
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的用法可以参考我之前的文章。

java 复制代码
//记录点位,保存重合点位
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的,否则会出现文本测量异常。

java 复制代码
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之前设置字体,也是方便区域测量,减少后期缩放操作。

java 复制代码
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只包含文字染色的区域,其他区域都是外部区域,

java 复制代码
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是精确测量,因此,也要避免性能问题。

java 复制代码
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);
}

绘制

下面是绘制代码

java 复制代码
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();

点位着色

我们先绘制一下点位

java 复制代码
canvas.drawCircle(point.x,point.y,point.radius,mPaint);

效果还不错

继续上色

java 复制代码
private float floatRandom(){
  return (float)Math.random();
}
 p.color = argb(floatRandom(),floatRandom(),floatRandom(),floatRandom());

有些凌乱,稍微挑战下背景和点位密度

还是不够亮,不够亮的问题怎么解决,当然是使用HLS或者HSV的颜色空间了,下面我们使用HLS来优化亮度

java 复制代码
hsl[0] = (float) (random * 360);
hsl[1] = 0.5f;
hsl[2] = 0.5f;  //最亮
p.color = HSLToColor(hsl);

好看多了

文字着色

文字说色是使用文字去这些点位绘制

java 复制代码
canvas.drawText(....)

普通文字说色

这里我们用字幕C去绘制,感觉还好。

表情符着色

下面,我们用😊表情符着色看下效果,还是不错的

动图实现

动图实际上,我们要调整radius或者textSize实现

文本动起来

主要功能已经实现了,下面,我们,我们实现一些动画效果,首先是表情符动起来 当然,这里要让表情符动起来,需要注意基线要根据TextSize重新计算,我们可以把radius设置到Paint.setTextSize中

java 复制代码
 text = "🤭".toCharArray();

当然,鲜花也可以动起来

是不是很简单呢,实际上,核心功能已经实现了,我们实现开头的效果看一下,下面改成画圆。

祝福文字

我们将绘制的文本改成"永远的女神",效果如下

总结

本篇到这里就结束了,本篇的核心内容就是利用点阵字体的思想,实现文字描绘特效,在本篇,我们还回忆了以前几篇文章,其中最重要的还是"碰撞检测"相关,另外我们也可以了解到字体设计的核心思想,以及如何让颜色变的更亮。

好了,本篇就到这里,下面我们附上本篇的源码。

java 复制代码
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;
            }
        }
    }
}
相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
服装学院的IT男4 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2064 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男4 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
ChinaDragonDreamer6 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j