Skip to content

Commit

Permalink
feat(android): support text decoration features
Browse files Browse the repository at this point in the history
* support `textDecorationColor` and `textDecorationStyle` props
* improve `verticalAlign` with line-breaking
  • Loading branch information
iPel committed Nov 27, 2023
1 parent 8e420c4 commit ac9c64a
Show file tree
Hide file tree
Showing 10 changed files with 724 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ public class NodeProps {
public static final String TEXT_ALIGN = "textAlign";
public static final String TEXT_ALIGN_VERTICAL = "textAlignVertical";
public static final String TEXT_DECORATION_LINE = "textDecorationLine";
public static final String TEXT_DECORATION_COLOR = "textDecorationColor";
public static final String TEXT_DECORATION_STYLE = "textDecorationStyle";
public static final String TEXT_SHADOW_OFFSET = "textShadowOffset";
public static final String TEXT_SHADOW_RADIUS = "textShadowRadius";
public static final String TEXT_SHADOW_COLOR = "textShadowColor";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@
import android.graphics.drawable.Drawable;

import android.text.Layout;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.tencent.renderer.component.text.TextLineMetricsHelper;
import com.tencent.renderer.component.text.TextRenderSupplier;

public class TextDrawable extends Drawable {
Expand All @@ -60,8 +62,10 @@ public class TextDrawable extends Drawable {
private Layout mLayout;
@Nullable
private BackgroundHolder mBackgroundHolder;
private TextLineMetricsHelper mHelper;

public void setTextLayout(@NonNull Object obj) {
Layout oldLayout = mLayout;
if (obj instanceof TextRenderSupplier) {
mLayout = ((TextRenderSupplier) obj).layout;
mLeftPadding = ((TextRenderSupplier) obj).leftPadding;
Expand All @@ -71,6 +75,22 @@ public void setTextLayout(@NonNull Object obj) {
} else if (obj instanceof Layout) {
mLayout = (Layout) obj;
}
if (mLayout != oldLayout) {
mHelper = getTextLineMetricsHelper(mLayout);
}
}

private TextLineMetricsHelper getTextLineMetricsHelper(Layout layout) {
if (layout != null) {
CharSequence text = layout.getText();
if (text instanceof Spanned) {
TextLineMetricsHelper[] spans = ((Spanned) text).getSpans(0, 0, TextLineMetricsHelper.class);
if (spans != null && spans.length > 0) {
return spans[0];
}
}
}
return null;
}

public void setBackgroundHolder(@Nullable BackgroundHolder holder) {
Expand Down Expand Up @@ -119,7 +139,13 @@ public void draw(@NonNull Canvas canvas) {
if (paint != null) {
paint.setFakeBoldText(mFakeBoldText);
}
mLayout.draw(canvas);
if (mHelper != null) {
mHelper.initialize();
mLayout.draw(canvas);
mHelper.drawTextDecoration(canvas, mLayout);
} else {
mLayout.draw(canvas);
}
canvas.restore();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* Tencent is pleased to support the open source community by making Hippy available.
* Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* 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.tencent.renderer.component.text;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.ReplacementSpan;
import android.text.style.UpdateAppearance;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class TextDecorationSpan extends CharacterStyle implements UpdateAppearance, TextLineMetricsHelper.LineMetrics {

public static final int STYLE_SOLID = 0;
public static final int STYLE_DOUBLE = 1;
public static final int STYLE_DOTTED = 2;
public static final int STYLE_DASHED = 3;
// according to kStdUnderline_Thickness defined in aosp's Paint.h
private static final float THICKNESS = (1.0f / 18.0f);

private final boolean underline;
private final boolean lineThrough;
private final int color;
private final int style;
private final boolean needSpecialDraw;

private TextLineMetricsHelper helper;

public TextDecorationSpan(boolean underline, boolean lineThrough, int color, int style) {
assert underline || lineThrough;
this.underline = underline;
this.lineThrough = lineThrough;
this.color = color;
this.style = style;
needSpecialDraw = style != STYLE_SOLID || (lineThrough && color != Color.TRANSPARENT);
}

public boolean needSpecialDraw() {
return needSpecialDraw;
}

@Override
public void updateDrawState(TextPaint tp) {
if (!underline && !lineThrough) {
// should never happened
return;
}
if (needSpecialDraw && helper != null) {
int color = this.color == Color.TRANSPARENT ? tp.getColor() : this.color;
helper.markTextDecoration(underline, lineThrough, color, style, tp.getTextSize());
return;
}
if (underline) {
if (color == Color.TRANSPARENT || !trySetUnderlineColor(tp, color, tp.getTextSize() * THICKNESS)) {
tp.setUnderlineText(true);
}
}
if (lineThrough) {
tp.setStrikeThruText(true);
}
}

private static boolean trySetUnderlineColor(TextPaint tp, int color, float thickness) {
try {
// Since these were @hide fields made public, we can link directly against it with
// a try/catch for its absence instead of doing the same through reflection.
// noinspection NewApi
tp.underlineColor = color;
// noinspection NewApi
tp.underlineThickness = thickness;
return true;
} catch (NoSuchFieldError e) {
return false;
}
}

@Override
public void setLineMetrics(TextLineMetricsHelper helper) {
if (needSpecialDraw) {
this.helper = helper;
}
}

public static final class StartMark extends ReplacementSpan implements TextLineMetricsHelper.LineMetrics {

private TextLineMetricsHelper helper;

@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
@Nullable Paint.FontMetricsInt fm) {
return 0;
}

@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
int bottom, @NonNull Paint paint) {
if (helper != null) {
helper.markTextDecorationStart(this, start, x, y);
}
}

@Override
public void setLineMetrics(TextLineMetricsHelper helper) {
this.helper = helper;
}
}

public static final class EndMark extends ReplacementSpan implements TextLineMetricsHelper.LineMetrics {

private TextLineMetricsHelper helper;

@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
@Nullable Paint.FontMetricsInt fm) {
return 0;
}

@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
int bottom, @NonNull Paint paint) {
if (helper != null) {
helper.markTextDecorationEnd(x);
}
}

@Override
public void setLineMetrics(TextLineMetricsHelper helper) {
this.helper = helper;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
import com.tencent.renderer.component.image.ImageLoaderAdapter;
import com.tencent.renderer.component.image.ImageRequestListener;
import com.tencent.renderer.node.ImageVirtualNode;
import com.tencent.renderer.node.TextVirtualNode;
import com.tencent.renderer.node.VirtualNode;
import com.tencent.renderer.utils.EventUtils.EventType;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
Expand Down Expand Up @@ -184,16 +184,16 @@ public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
int transY;
assert mVerticalAlign != null;
switch (mVerticalAlign) {
case TextVirtualNode.V_ALIGN_TOP:
case VirtualNode.V_ALIGN_TOP:
transY = top + mMarginTop;
break;
case TextVirtualNode.V_ALIGN_MIDDLE:
case VirtualNode.V_ALIGN_MIDDLE:
transY = top + (bottom - top) / 2 - mMeasuredHeight / 2;
break;
case TextVirtualNode.V_ALIGN_BOTTOM:
case VirtualNode.V_ALIGN_BOTTOM:
transY = bottom - mMeasuredHeight - mMarginBottom;
break;
case TextVirtualNode.V_ALIGN_BASELINE:
case VirtualNode.V_ALIGN_BASELINE:
default:
transY = y - mMeasuredHeight - mMarginBottom;
break;
Expand Down
Loading

0 comments on commit ac9c64a

Please sign in to comment.