UI-重复动态绘制文字 Posted on 2024-05-14 | In Android代码片段 原文:Android 描边动画实现母亲节祝福效果 - 掘金 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.PointF; import android.text.TextPaint; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Android 描边动画实现母亲节祝福效果 * <p> * 本质:点阵体,比较适合做一些文字特效 * 获取Path * Path转Point集合 * Point绘制 */ 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 + i, new FontText(i, textPathToPoints(word, mTextPaint), randomColor()));//单个文字的点位 } } if (textPoints.isEmpty()) { return; } /** * 绘制: * 将文字水平居中绘制 * 统计绘制完整的文字 * 所有文字一起测量 * 计算单个文字的X轴偏移 * 计算单个文字的Points * 绘制文字 */ 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); } }