feat(android): add Text and TextInput features from 2.0 (#2526)

* feat(android): feature from 2.0

- add breakStrategy prop for Text and TextInput
- add isFocused method for TextInput
- add ellipsizeMode prop support for Text componment

* feat(android): feature from 2.0

- revert demo

Co-authored-by: Zoom Chan <zoom_chan@163.com>
This commit is contained in:
iPel
2022-10-12 20:33:15 +08:00
committed by GitHub
parent 5796f5964a
commit fea9add52e
7 changed files with 257 additions and 33 deletions
+3
View File
@@ -14,5 +14,8 @@ trim_trailing_whitespace = true
max_line_length = 0
trim_trailing_whitespace = false
[*.java]
indent_size = 4
[COMMIT_EDITMSG]
max_line_length = 0
@@ -106,6 +106,7 @@ public class NodeProps {
public static final String LINE_SPACING_EXTRA = "lineSpacingExtra";
public static final String NUMBER_OF_LINES = "numberOfLines";
public static final String ELLIPSIZE_MODE = "ellipsizeMode";
public static final String BREAK_STRATEGY = "breakStrategy";
public static final String ON = "on";
public static final String RESIZE_MODE = "resizeMode";
public static final String RESIZE_METHOD = "resizeMethod";
@@ -501,6 +501,12 @@ public class HippyTextInput extends AppCompatEditText implements HippyViewBase,
return false;
}
public Map<String, Object> jsIsFocused() {
Map<String, Object> result = new HashMap<>();
result.put("value", hasFocus());
return result;
}
public void setBlurOrOnFocus(boolean blur) {
if (blur) {
setOnFocusChangeListener(this);
@@ -26,6 +26,7 @@ import android.os.MessageQueue;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
import android.text.TextUtils;
import android.text.method.PasswordTransformationMethod;
import android.util.TypedValue;
@@ -49,6 +50,7 @@ import com.tencent.mtt.hippy.views.hippypager.HippyPager;
import com.tencent.renderer.NativeRender;
import com.tencent.renderer.NativeRenderException;
import com.tencent.renderer.NativeRendererManager;
import com.tencent.renderer.component.text.TextVirtualNode;
import com.tencent.renderer.utils.ArrayUtils;
import java.util.LinkedList;
@@ -71,6 +73,7 @@ public class HippyTextInputController extends HippyViewController<HippyTextInput
private static final String FUNC_CLEAR = "clear";
private static final String FUNC_FOCUS = "focusTextInput";
private static final String FUNC_BLUR = "blurTextInput";
private static final String FUNC_IS_FOCUSED = "isFocused";
private static final String FUNC_GET_VALUE = "getValue";
private static final String FUNC_SET_VALUE = "setValue";
private static final String FUNC_KEYBOARD_DISMISS = "dissmiss";
@@ -440,6 +443,26 @@ public class HippyTextInputController extends HippyViewController<HippyTextInput
}
@HippyControllerProps(name = NodeProps.BREAK_STRATEGY, defaultType = HippyControllerProps.STRING)
public void setBreakStrategy(HippyTextInput view, String strategy) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int strategyInt;
switch (strategy) {
case TextVirtualNode.STRATEGY_HIGH_QUALITY:
strategyInt = Layout.BREAK_STRATEGY_HIGH_QUALITY;
break;
case TextVirtualNode.STRATEGY_BALANCED:
strategyInt = Layout.BREAK_STRATEGY_BALANCED;
break;
case TextVirtualNode.STRATEGY_SIMPLE:
default:
strategyInt = Layout.BREAK_STRATEGY_SIMPLE;
}
// noinspection WrongConstant
view.setBreakStrategy(strategyInt);
}
}
@Override
public void dispatchFunction(@NonNull HippyTextInput textInput, @NonNull String functionName,
@NonNull HippyArray params, @NonNull Promise promise) {
@@ -449,8 +472,13 @@ public class HippyTextInputController extends HippyViewController<HippyTextInput
@Override
public void dispatchFunction(@NonNull final HippyTextInput textInput,
@NonNull String functionName, @NonNull List params, @NonNull Promise promise) {
if (FUNC_GET_VALUE.equals(functionName) && promise != null) {
if (promise == null) {
return;
}
if (FUNC_GET_VALUE.equals(functionName)) {
promise.resolve(textInput.jsGetValue());
} else if (FUNC_IS_FOCUSED.equals(functionName)) {
promise.resolve(textInput.jsIsFocused());
}
}
@@ -31,22 +31,18 @@ import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.CharacterStyle;
import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.tencent.link_supplier.proxy.framework.FontAdapter;
import com.tencent.mtt.hippy.annotation.HippyControllerProps;
import com.tencent.mtt.hippy.dom.node.NodeProps;
import com.tencent.mtt.hippy.utils.I18nUtil;
import com.tencent.mtt.hippy.utils.PixelUtil;
import com.tencent.renderer.NativeRender;
import com.tencent.renderer.utils.FlexUtils.FlexMeasureMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -54,18 +50,27 @@ import java.util.Map;
public class TextVirtualNode extends VirtualNode {
public static final String STRATEGY_SIMPLE = "simple";
public static final String STRATEGY_HIGH_QUALITY = "high_quality";
public static final String STRATEGY_BALANCED = "balanced";
private static final int TEXT_BOLD_MIN_VALUE = 500;
private static final int TEXT_SHADOW_COLOR_DEFAULT = 0x55000000;
private static final String TEXT_FONT_STYLE_ITALIC = "italic";
private static final String TEXT_FONT_STYLE_BOLD = "bold";
private static final String TEXT_DECORATION_UNDERLINE = "underline";
private static final String TEXT_DECORATION_LINE_THROUGH = "line-through";
private static final String MODE_HEAD = "head";
private static final String MODE_MIDDLE = "middle";
private static final String MODE_TAIL = "tail";
private static final String MODE_CLIP = "clip";
private static final String TEXT_ALIGN_LEFT = "left";
private static final String TEXT_ALIGN_AUTO = "auto";
private static final String TEXT_ALIGN_JUSTIFY = "justify";
private static final String TEXT_ALIGN_RIGHT = "right";
private static final String TEXT_ALIGN_CENTER = "center";
private static final String ELLIPSIS = "...";
// via android.text.TextUtils#ELLIPSIS_NORMAL
private static final String ELLIPSIS = "\u2026";
protected int mColor = Color.BLACK;
protected int mNumberOfLines;
@@ -84,6 +89,8 @@ public class TextVirtualNode extends VirtualNode {
protected boolean mHasUnderlineTextDecoration = false;
protected boolean mHasLineThroughTextDecoration = false;
protected boolean mEnableScale = false;
protected String mEllipsizeMode = MODE_TAIL;
protected String mBreakStrategy = STRATEGY_SIMPLE;
@Nullable
protected String mFontFamily;
@Nullable
@@ -92,7 +99,10 @@ public class TextVirtualNode extends VirtualNode {
protected CharSequence mText;
protected Layout.Alignment mAlignment = Layout.Alignment.ALIGN_NORMAL;
@Nullable
protected TextPaint mTextPaint;
protected TextPaint mTextPaintInstance;
// for compatibility with 2.13.x and earlier versions to calculate the layout height of empty Text nodes
@Nullable
protected TextPaint mTextPaintForEmpty;
@Nullable
protected final FontAdapter mFontAdapter;
@Nullable
@@ -139,6 +149,9 @@ public class TextVirtualNode extends VirtualNode {
defaultNumber = NodeProps.FONT_SIZE_SP)
public void setFontSize(float size) {
mFontSize = (int) Math.ceil(PixelUtil.dp2px(size));
if (mTextPaintInstance != null) {
mTextPaintInstance.setTextSize(mFontSize);
}
markDirty();
}
@@ -247,6 +260,46 @@ public class TextVirtualNode extends VirtualNode {
markDirty();
}
@SuppressWarnings("unused")
@HippyControllerProps(name = NodeProps.ELLIPSIZE_MODE, defaultType = HippyControllerProps.STRING)
public void setEllipsizeMode(String mode) {
if (TextUtils.equals(mode, mEllipsizeMode)) {
return;
}
switch (mode) {
case MODE_TAIL:
case MODE_CLIP:
case MODE_MIDDLE:
case MODE_HEAD:
mEllipsizeMode = mode;
break;
default:
mEllipsizeMode = MODE_TAIL;
}
markDirty();
}
@SuppressWarnings("unused")
@HippyControllerProps(name = NodeProps.BREAK_STRATEGY, defaultType = HippyControllerProps.STRING)
public void setBreakStrategy(String strategy) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
if (TextUtils.equals(strategy, mBreakStrategy)) {
return;
}
switch (strategy) {
case STRATEGY_SIMPLE:
case STRATEGY_HIGH_QUALITY:
case STRATEGY_BALANCED:
mBreakStrategy = strategy;
break;
default:
mBreakStrategy = STRATEGY_SIMPLE;
}
markDirty();
}
@SuppressWarnings("unused")
@HippyControllerProps(name = NodeProps.TEXT_ALIGN, defaultType = HippyControllerProps.STRING,
defaultString = TEXT_ALIGN_LEFT)
@@ -410,9 +463,6 @@ public class TextVirtualNode extends VirtualNode {
@NonNull
protected Layout createLayout(final float width, final FlexMeasureMode widthMode) {
if (mTextPaint == null) {
mTextPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
}
if (mSpanned == null || mDirty) {
mSpanned = createSpan(true);
mDirty = false;
@@ -421,38 +471,54 @@ public class TextVirtualNode extends VirtualNode {
// no need to create layout again.
return mLayout;
}
final TextPaint textPaint = getTextPaint();
Layout layout;
BoringLayout.Metrics boring = BoringLayout.isBoring(mSpanned, mTextPaint);
float desiredWidth = Layout.getDesiredWidth(mSpanned, mTextPaint);
BoringLayout.Metrics boring = BoringLayout.isBoring(mSpanned, textPaint);
float desiredWidth = Layout.getDesiredWidth(mSpanned, textPaint);
boolean unconstrainedWidth = (widthMode == FlexMeasureMode.UNDEFINED) || width < 0;
if (boring != null && (unconstrainedWidth || boring.width <= width)) {
layout = BoringLayout
.make(mSpanned, mTextPaint, boring.width, mAlignment,
.make(mSpanned, textPaint, boring.width, mAlignment,
getLineSpacingMultiplier(), mLineSpacingExtra, boring, true);
} else {
if (!unconstrainedWidth && desiredWidth > width) {
desiredWidth = width;
}
layout = buildStaticLayout(mSpanned, (int) Math.ceil(desiredWidth));
layout = buildStaticLayout(mSpanned, textPaint, (int) Math.ceil(desiredWidth));
}
if (mNumberOfLines > 0 && layout.getLineCount() > mNumberOfLines) {
int lastLineStart = layout.getLineStart(mNumberOfLines - 1);
int lastLineEnd = layout.getLineEnd(mNumberOfLines - 1);
if (lastLineStart < lastLineEnd) {
int measureWidth = (int) Math.ceil(unconstrainedWidth ? desiredWidth : width);
try {
layout = createLayoutWithNumberOfLine(lastLineEnd, layout.getWidth());
layout = truncateLayoutWithNumberOfLine(layout, measureWidth, mNumberOfLines);
} catch (Exception e) {
e.printStackTrace();
}
}
}
layout.getPaint().setTextSize(mFontSize);
mLayout = layout;
mLastLayoutWidth = layout.getWidth();
return layout;
}
private StaticLayout buildStaticLayout(CharSequence source, int width) {
private TextPaint getTextPaint() {
if (TextUtils.isEmpty(mText)) {
if (mTextPaintForEmpty == null) {
mTextPaintForEmpty = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
}
return mTextPaintForEmpty;
}
if (mTextPaintInstance == null) {
mTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
mTextPaintInstance.setTextSize(mFontSize);
}
return mTextPaintInstance;
}
private StaticLayout buildStaticLayout(CharSequence source, TextPaint paint, int width) {
Layout.Alignment alignment = mAlignment;
if (I18nUtil.isRTL()) {
BidiFormatter bidiFormatter = BidiFormatter.getInstance();
@@ -461,24 +527,125 @@ public class TextVirtualNode extends VirtualNode {
alignment = Layout.Alignment.ALIGN_NORMAL;
}
}
return new StaticLayout(source, mTextPaint, width, alignment, getLineSpacingMultiplier(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return StaticLayout.Builder.obtain(source, 0, source.length(), paint, width)
.setAlignment(alignment)
.setLineSpacing(mLineSpacingExtra, getLineSpacingMultiplier())
.setIncludePad(true)
.setBreakStrategy(getBreakStrategy())
.build();
} else {
return new StaticLayout(source, paint, width, alignment, getLineSpacingMultiplier(),
mLineSpacingExtra, true);
}
}
@NonNull
private StaticLayout createLayoutWithNumberOfLine(int lastLineEnd, int width) {
String text = (mSpanned == null) ? "" : mSpanned.toString();
SpannableStringBuilder builder = (SpannableStringBuilder) mSpanned
.subSequence(0, text.length());
int last = lastLineEnd > ELLIPSIS.length() ? (lastLineEnd - ELLIPSIS.length()) : lastLineEnd;
CharacterStyle[] spans = builder.getSpans(last, text.length(), CharacterStyle.class);
if (spans != null && spans.length > 0) {
for (CharacterStyle span : spans) {
if (builder.getSpanStart(span) >= last) {
builder.removeSpan(span);
@RequiresApi(api = Build.VERSION_CODES.M)
private int getBreakStrategy() {
final String strategy = mBreakStrategy;
switch (strategy) {
case STRATEGY_HIGH_QUALITY:
return Layout.BREAK_STRATEGY_HIGH_QUALITY;
case STRATEGY_BALANCED:
return Layout.BREAK_STRATEGY_BALANCED;
case STRATEGY_SIMPLE:
default:
return Layout.BREAK_STRATEGY_SIMPLE;
}
}
private StaticLayout truncateLayoutWithNumberOfLine(Layout preLayout, int width, int numberOfLines) {
int lineCount = preLayout.getLineCount();
assert lineCount >= 2;
CharSequence origin = preLayout.getText();
TextPaint paint = preLayout.getPaint();
CharSequence truncated;
if (MODE_CLIP.equals(mEllipsizeMode)) {
int end = preLayout.getLineEnd(numberOfLines - 1);
if (origin.charAt(end - 1) == '\n') {
// there will be an unexpected blank line, if ends with a new line char, trim it
--end;
}
truncated = origin.subSequence(0, end);
} else {
TextPaint measurePaint = new TextPaint();
measurePaint.set(paint);
int start = preLayout.getLineStart(numberOfLines - 1);
CharSequence formerLines = start > 0 ? origin.subSequence(0, start) : null;
boolean newLine = formerLines != null && formerLines.charAt(formerLines.length() - 1) != '\n';
CharSequence lastLine;
if (MODE_HEAD.equals(mEllipsizeMode)) {
float formerTextSize = numberOfLines >= 2 ? getLineHeight(preLayout, numberOfLines - 2) : paint.getTextSize();
float latterTextSize = Math.max(getLineHeight(preLayout, lineCount - 2), getLineHeight(preLayout, lineCount - 1));
measurePaint.setTextSize(Math.max(formerTextSize, latterTextSize));
lastLine = ellipsizeHead(origin, measurePaint, width, start);
} else if (MODE_MIDDLE.equals(mEllipsizeMode)) {
measurePaint.setTextSize(Math.max(getLineHeight(preLayout, numberOfLines - 1), getLineHeight(preLayout, lineCount - 1)));
lastLine = ellipsizeMiddle(origin, measurePaint, width, start);
} else /*if (MODE_TAIL.equals(mEllipsizeMode))*/ {
measurePaint.setTextSize(getLineHeight(preLayout, numberOfLines - 1));
int end = preLayout.getLineEnd(numberOfLines - 1);
lastLine = ellipsizeTail(origin, measurePaint, width, start, end);
}
// concat everything
truncated = formerLines == null ? lastLine : TextUtils.concat(formerLines, newLine ? "\n" : "", lastLine);
}
return buildStaticLayout(truncated, paint, width);
}
private float getLineHeight(Layout layout, int line) {
return layout.getLineTop(line + 1) - layout.getLineTop(line);
}
private CharSequence ellipsizeHead(CharSequence origin, TextPaint paint, int width, int start) {
start = Math.max(start, TextUtils.lastIndexOf(origin, '\n') + 1);
// "…${last line of the rest part}"
CharSequence tmp = TextUtils.concat(ELLIPSIS, origin.subSequence(start, origin.length()));
return TextUtils.ellipsize(tmp, paint, width, TextUtils.TruncateAt.START);
}
private CharSequence ellipsizeMiddle(CharSequence origin, TextPaint paint, int width, int start) {
int leftEnd, rightStart;
if ((leftEnd = TextUtils.indexOf(origin, '\n', start)) != -1) {
rightStart = TextUtils.lastIndexOf(origin, '\n') + 1;
assert leftEnd < rightStart;
// "${first line of the rest part}…${last line of the rest part}"
CharSequence tmp = TextUtils.concat(origin.subSequence(start, leftEnd), ELLIPSIS, origin.subSequence(rightStart, origin.length()));
final int[] outRange = new int[2];
TextUtils.EllipsizeCallback callback = new TextUtils.EllipsizeCallback() {
@Override
public void ellipsized(int l, int r) {
outRange[0] = l;
outRange[1] = r;
}
};
CharSequence line = TextUtils.ellipsize(tmp, paint, width, TextUtils.TruncateAt.MIDDLE, false, callback);
if (line != tmp) {
int pos0 = leftEnd - start;
int pos1 = pos0 + ELLIPSIS.length();
if (outRange[0] > pos0) {
line = new SpannableStringBuilder(tmp).replace(pos0, outRange[1], ELLIPSIS);
} else if (outRange[1] < pos1) {
line = new SpannableStringBuilder(tmp).replace(outRange[0], pos1, ELLIPSIS);
}
}
return line;
} else {
// "${only one line of the rest part}"
CharSequence tmp = origin.subSequence(start, origin.length());
return TextUtils.ellipsize(tmp, paint, width, TextUtils.TruncateAt.MIDDLE);
}
return buildStaticLayout(builder.replace(last, text.length(), ELLIPSIS), width);
}
private CharSequence ellipsizeTail(CharSequence origin, TextPaint paint, int width, int start, int end) {
if (origin.charAt(end - 1) == '\n') {
// there will be an unexpected blank line, if ends with a new line char, trim it
--end;
}
// "${first line of the rest part}…"
CharSequence tmp = TextUtils.concat(origin.subSequence(start, end), ELLIPSIS);
return TextUtils.ellipsize(tmp, paint, width, TextUtils.TruncateAt.END);
}
}
@@ -19,6 +19,7 @@ package com.tencent.renderer.component.text;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ImageSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -135,9 +136,13 @@ public abstract class VirtualNode {
}
public void execute(SpannableStringBuilder builder) {
int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
if (mStart == 0) {
int spanFlags;
if (mWhat instanceof ImageSpan) {
spanFlags = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
} else if (mStart == 0) {
spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
} else {
spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
}
builder.setSpan(mWhat, mStart, mEnd, spanFlags);
}
@@ -83,6 +83,20 @@ NATIVE_RENDER_COMPONENT_EXPORT_METHOD(focusTextInput:(nonnull NSNumber *)compone
}
// clang-format on
// clang-format off
NATIVE_RENDER_COMPONENT_EXPORT_METHOD(isFocused:(nonnull NSNumber *)componentTag callback:(RenderUIResponseSenderBlock)callback) {
[self.renderContext addUIBlock:^(__unused id<NativeRenderContext> renderContext, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
NativeRenderBaseTextInput *view = (NativeRenderBaseTextInput *)viewRegistry[componentTag];
if (view == nil) return ;
if (![view isKindOfClass:[NativeRenderBaseTextInput class]]) {
NativeRenderLogError(@"Invalid view returned from registry, expecting NativeRenderBaseTextInput, got: %@", view);
}
BOOL isFocused = [view isFirstResponder];
callback([NSDictionary dictionaryWithObject:[NSNumber numberWithBool:isFocused] forKey:@"value"]);
}];
}
// clang-format on
// clang-format off
NATIVE_RENDER_COMPONENT_EXPORT_METHOD(blurTextInput:(nonnull NSNumber *)componentTag) {
[self.renderContext addUIBlock:^(__unused id<NativeRenderContext> renderContext, NSDictionary<NSNumber *, UIView *> *viewRegistry){