/* * Copyright (c) 2014-2017 Sean Liu. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.baoti.pioneer.misc.util; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; import android.graphics.drawable.shapes.Shape; import android.support.annotation.ColorInt; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextPaint; import android.text.style.CharacterStyle; import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; import android.text.style.UnderlineSpan; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * text span 工具类,提供可组合使用的 ReplacementSpan 实例。 * <br>可用于实现 绘制带圆角矩形背景的文字,甚至是绘制镂空的文字 * <br>NOTE: * 按照 {@link android.text.BoringLayout} 与 {@literal android.text.TextLine} 的设计, * {@literal <ReplacementSpan>1+2=<u>3</u></ReplacementSpan>} 中的{@literal <u>3</u>} 对应的{@link UnderlineSpan}是不生效的。 * * Created by sean on 2017/10/1. */ public class Spans { /** * 自定义 ReplacementSpan 基类,回调方法新增 outRect 参数,接收外层 span 的尺寸 */ public static class SupportSpan extends ReplacementSpan { protected final Paint.FontMetricsInt fontMetricsInt = new Paint.FontMetricsInt(); protected final Rect frame = new Rect(); private int fixedWidth = -1; @Override public final int getSize(@NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) { return getSize(frame, paint, text, start, end, fm); } @Override public final void draw(@NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) { draw(frame, canvas, text, start, end, x, top, y, bottom, paint); } public int getSize(@NonNull Rect outRect, @NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) { return getMinSize(outRect, paint, text, start, end, fm); } public int getMinSize(@NonNull Rect outRect, @NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) { if (fm != null) { paint.getFontMetricsInt(fm); } int width = fixedWidth >= 0 ? fixedWidth : (int) Math.ceil(paint.measureText(text, start, end)); paint.getFontMetricsInt(fontMetricsInt); frame.right = width; frame.top = fontMetricsInt.top; frame.bottom = fontMetricsInt.bottom; return width; } public void draw(@NonNull Rect outRect, @NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) { } public int getFixedWidth() { return fixedWidth; } public void setFixedWidth(int fixedWidth) { this.fixedWidth = fixedWidth; } } /** * 可嵌套使用的 ReplacementSpan,支持接收其他 CharacterStyle 作为内层 span 并产生对应效果 */ public static class SpanGroup extends SupportSpan { private final List<CharacterStyle> styles; public SpanGroup(CharacterStyle... styles) { this.styles = new ArrayList<>(styles.length); Collections.addAll(this.styles, styles); } @Override public int getSize(@NonNull Rect outRect, @NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) { int width = super.getSize(outRect, paint, text, start, end, fm); if (styles != null) { for (CharacterStyle style : styles) { if (style instanceof SupportSpan) { width = Math.max(width, ((SupportSpan) style).getSize(frame, paint, text, start, end, fm)); } else if (style instanceof ReplacementSpan) { width = Math.max(width, ((ReplacementSpan) style).getSize(paint, text, start, end, fm)); } else if (paint instanceof TextPaint) { if (style instanceof MetricAffectingSpan) { ((MetricAffectingSpan) style).updateMeasureState((TextPaint) paint); } } } } frame.right = width; return width; } @Override public void draw(@NonNull Rect outRect, @NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) { for (CharacterStyle style : styles) { if (style instanceof SupportSpan) { ((SupportSpan) style).draw(frame, canvas, text, start, end, x, top, y, bottom, paint); } else if (style instanceof ReplacementSpan) { ((ReplacementSpan) style).draw(canvas, text, start, end, x, top, y, bottom, paint); } else if (paint instanceof TextPaint) { style.updateDrawState((TextPaint) paint); } } } } /** * 文本绘制,用于与 ReplacementSpan 组合。 * <br>因为 TextLine 默认不会为 ReplacementSpan 绘文字 */ public static class TextSpan extends SpanGroup { private final Paint.Align align; private final Paint.Style style; private final float strokeWidth; public TextSpan(CharacterStyle... styles) { this(null, styles); } public TextSpan(Paint.Align align, CharacterStyle... styles) { this(align, null, 0, styles); } public TextSpan(Paint.Align align, Paint.Style style, float strokeWidth, CharacterStyle... styles) { super(styles); this.align = align; this.strokeWidth = strokeWidth; this.style = style; } @Override public void draw(@NonNull Rect outRect, @NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) { Paint.Align oldAlign = paint.getTextAlign(); Paint.Style oldStyle = paint.getStyle(); float oldStrokeWidth = paint.getStrokeWidth(); if (align != null) { paint.setTextAlign(align); } if (style != null) { paint.setStyle(style); } if (strokeWidth > 0) { paint.setStrokeWidth(strokeWidth); } super.draw(outRect, canvas, text, start, end, x, top, y, bottom, paint); switch (paint.getTextAlign()) { case CENTER: canvas.drawText(text, start, end, x + (outRect.right - outRect.left) / 2, y, paint); break; default: canvas.drawText(text, start, end, x, y, paint); break; } paint.setTextAlign(oldAlign); paint.setStyle(oldStyle); paint.setStrokeWidth(oldStrokeWidth); } } /** * 镂空绘制样式 * <br>注意:不支持在该文本内再进行span分块, * 如:{@literal <Hollow>ABC<fgcolor>DE</fgcolor></Hollow>}, * 可改为:{@literal <Hollow>ABC</Hollow><Hollow><fgcolor>DE</fgcolor></Hollow>} */ public static class HollowSpan extends SpanGroup { private final SpanGroup srcGroup; private final PorterDuffXfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); private Bitmap bitmap; private Canvas bitmapCanvas; /** * 镂空绘制 * @param styles 基础样式 * @param srcStyles 镂空样式 */ public HollowSpan(CharacterStyle[] styles, CharacterStyle[] srcStyles) { super(styles); this.srcGroup = new SpanGroup(srcStyles); } /** * 镂空绘制 * @param styles 基础样式 * @param srcGroup 镂空样式 */ public HollowSpan(CharacterStyle[] styles, SpanGroup srcGroup) { super(styles); this.srcGroup = srcGroup; } @Override public int getSize(@NonNull Rect outRect, @NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) { int width = super.getSize(outRect, paint, text, start, end, fm); srcGroup.getSize(outRect, paint, text, start, end, fm); return width; } @Override public void draw(@NonNull Rect outRect, @NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) { if (bitmap == null) { bitmap = Bitmap.createBitmap(frame.right - frame.left, frame.bottom - frame.top, Bitmap.Config.ARGB_8888); bitmapCanvas = new Canvas(bitmap); } bitmapCanvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); bitmapCanvas.translate(-x, 0); super.draw(outRect, bitmapCanvas, text, start, end, x, top, y, bottom, paint); Xfermode oldXfermode = paint.getXfermode(); paint.setXfermode(this.xfermode); srcGroup.draw(outRect, bitmapCanvas, text, start, end, x, top, y, bottom, paint); paint.setXfermode(oldXfermode); canvas.drawBitmap(bitmap, x, 0, null); } } /** * 调试绘制,画 top/bottom/ascent/descent 位置的线 */ public static class DebugSpan extends SpanGroup { public DebugSpan(CharacterStyle... styles) { super(styles); } @Override public void draw(@NonNull Rect outRect, @NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) { drawDebugLines(outRect, canvas, x, top, y, bottom, paint); super.draw(outRect, canvas, text, start, end, x, top, y, bottom, paint); } protected void drawDebugLines(Rect outRect, @NonNull Canvas canvas, float x, int top, int y, int bottom, @NonNull Paint paint) { int width = frame.width(); int oldColor = paint.getColor(); // x, y canvas.drawLine(x, y, x + width, y, paint); // x, top paint.setColor(Color.RED); canvas.drawLine(x, top, x + width, top, paint); canvas.drawLine(x, y + fontMetricsInt.top, x + width, bottom, paint); // x, ascent paint.setColor(Color.GREEN); canvas.drawLine(x, y + paint.ascent(), x + width, bottom, paint); canvas.drawLine(x, y + fontMetricsInt.ascent, x + width, y + fontMetricsInt.ascent, paint); // x, descent paint.setColor(Color.BLUE); canvas.drawLine(x, y + paint.descent(), x + width, top, paint); canvas.drawLine(x, y + fontMetricsInt.descent, x + width, y + fontMetricsInt.descent, paint); // x, bottom paint.setColor(Color.YELLOW); canvas.drawLine(x, bottom, x + width, bottom, paint); canvas.drawLine(x, y + fontMetricsInt.bottom, x + width, top, paint); paint.setColor(oldColor); } } /** * 图形绘制,可使用 Shape 绘制图形,如:圆角矩形 */ public static class ShapeSpan extends SpanGroup { private final Shape shape; private final Paint.Style style; private final int strokeWidth; @ColorInt private final int color; private final boolean includePad; // true:高度为 bottom - top,false:高度为 descent - ascent public ShapeSpan(Shape shape, Paint.Style style, int strokeWidth, @ColorInt int color, boolean includePad, CharacterStyle... styles) { super(styles); this.shape = shape; this.style = style; this.strokeWidth = strokeWidth; this.color = color; this.includePad = includePad; } @Override public int getSize(@NonNull Rect outRect, @NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) { int width = super.getSize(outRect, paint, text, start, end, fm); if (includePad) { shape.resize(frame.right - strokeWidth, fontMetricsInt.bottom - fontMetricsInt.top - strokeWidth); } else { shape.resize(frame.right - strokeWidth, fontMetricsInt.descent - fontMetricsInt.ascent - strokeWidth); } return width; } @Override public void draw(@NonNull Rect outRect, @NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) { float oldStrokeWidth = paint.getStrokeWidth(); Paint.Style oldStyle = paint.getStyle(); int oldColor = paint.getColor(); paint.setStrokeWidth(strokeWidth); paint.setStyle(style); paint.setColor(color); canvas.save(); if (includePad) { canvas.translate(x + strokeWidth / 2, strokeWidth / 2); } else { canvas.translate(x + strokeWidth / 2, fontMetricsInt.ascent - fontMetricsInt.top + strokeWidth / 2); } shape.draw(canvas, paint); canvas.restore(); paint.setColor(oldColor); paint.setStyle(oldStyle); paint.setStrokeWidth(oldStrokeWidth); float oldTextSize = paint.getTextSize(); paint.setTextSize(oldTextSize * 0.8f); super.draw(outRect, canvas, text, start, end, x, top, y, bottom, paint); paint.setTextSize(oldTextSize); } } }