diff --git a/.editorconfig b/.editorconfig index bc137906a..7f8c8fcd5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java index 076742c9a..c0995b363 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java @@ -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"; diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java index ad0950448..738143771 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java @@ -501,6 +501,12 @@ public class HippyTextInput extends AppCompatEditText implements HippyViewBase, return false; } + public Map jsIsFocused() { + Map result = new HashMap<>(); + result.put("value", hasFocus()); + return result; + } + public void setBlurOrOnFocus(boolean blur) { if (blur) { setOnFocusChangeListener(this); diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java index 440c9f639..2914b30f5 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java @@ -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= 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 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); } } diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/component/text/VirtualNode.java b/renderer/native/android/src/main/java/com/tencent/renderer/component/text/VirtualNode.java index e1a23fe58..78f7dbe42 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/component/text/VirtualNode.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/component/text/VirtualNode.java @@ -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); } diff --git a/renderer/native/ios/renderer/component/textinput/NativeRenderTextViewManager.mm b/renderer/native/ios/renderer/component/textinput/NativeRenderTextViewManager.mm index e393c8d64..6b8c00cf9 100644 --- a/renderer/native/ios/renderer/component/textinput/NativeRenderTextViewManager.mm +++ b/renderer/native/ios/renderer/component/textinput/NativeRenderTextViewManager.mm @@ -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 renderContext, NSDictionary *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 renderContext, NSDictionary *viewRegistry){