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

17 篇文章 0 订阅
17 篇文章 0 订阅

效果

 直接上源码:

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;
            }
        }
    }
}

详细讲解:

字体原理

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

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

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

关于矢量字体

矢量字体有以下特性

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

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

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

企业微信20240309-103953@2x.png

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

关于点阵字体

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

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

点阵体文字

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

通过上面的对比我们可以知道,矢量字体可以实现点阵字体,但反过来是不行的。

本篇原理

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

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

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

实现

初始化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());

企业微信20240309-084710@2x.png

有些凌乱,稍微挑战下背景和点位密度,将View背景设置为黑色,然后step设置为5

企业微信20240309-085147@2x.png

还是不够亮,不够亮的问题怎么解决,当然是使用HLS或者HSV的颜色空间了,下面我们使用HSL来优化亮度,hSL的第三个参数为两度,为0.5时最亮,过界或者小于这个值亮度会递减。第二个值为色彩饱和度,第一个值为色相。

  • 色相(H) 是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。
  • 饱和度(S) 是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取 0-1f 的数值。
  • 亮度(L) ,取 0-1f,增加亮度,颜色会向白色变化;减少亮度,颜色会向黑色变化。

上面是HSL模型,具体细节的话如下

色相变化范围

202101051439154.png

亮度变化范围

20210105143947291.png

调整代码逻辑

hsl[0] = (float) (random * 360);  //色相
hsl[1] = 0.5f; // 饱和度 - 颜色的浓度
hsl[2] = 0.5f;  //最亮 - 颜色的两度
p.color = HSLToColor(hsl);

 

好看多了

文字着色

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

canvas.drawText(....)

 

普通文字说色

这里我们用字幕C去绘制,感觉还好,就是亮度变差了,可能笔划不够粗,后续优化的可以取一些亮色的颜色。

企业微信20240309-093758@2x.png

表情符着色

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

企业微信20240309-094051@2x.png

动图实现

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

文本动起来

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

 text = "🤭".toCharArray();

由于表情符属于文字,因此需要调整 textSize 

当然,鲜花也可以动起来

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

当然绘制圆是不需要设置textSize的,调整radius即可。

祝福文字

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

总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值