diff --git a/core/java/android/view/inputmethod/TextAppearanceInfo.java b/core/java/android/view/inputmethod/TextAppearanceInfo.java index 500c41cd1fed8..05717dd5d9307 100644 --- a/core/java/android/view/inputmethod/TextAppearanceInfo.java +++ b/core/java/android/view/inputmethod/TextAppearanceInfo.java @@ -31,7 +31,10 @@ import android.inputmethodservice.InputMethodService; import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; +import android.text.Spanned; +import android.text.TextPaint; import android.text.method.TransformationMethod; +import android.text.style.CharacterStyle; import android.widget.TextView; import java.util.Objects; @@ -182,6 +185,70 @@ public final class TextAppearanceInfo implements Parcelable { mLinkTextColor = builder.mLinkTextColor; } + /** + * Creates a new instance of {@link TextAppearanceInfo} by extracting text appearance from the + * character before cursor in the target {@link TextView}. + * @param textView the target {@link TextView}. + * @return the new instance of {@link TextAppearanceInfo}. + * @hide + */ + @NonNull + public static TextAppearanceInfo createFromTextView(@NonNull TextView textView) { + final int selectionStart = textView.getSelectionStart(); + final CharSequence text = textView.getText(); + TextPaint textPaint = new TextPaint(); + textPaint.set(textView.getPaint()); // Copy from textView + if (text instanceof Spanned && text.length() > 0 && selectionStart > 0) { + // Extract the CharacterStyle spans that changes text appearance in the character before + // cursor. + Spanned spannedText = (Spanned) text; + int lastCh = selectionStart - 1; + CharacterStyle[] spans = spannedText.getSpans(lastCh, lastCh, CharacterStyle.class); + if (spans != null) { + for (CharacterStyle span: spans) { + // Exclude spans that end at lastCh + if (spannedText.getSpanStart(span) <= lastCh + && lastCh < spannedText.getSpanEnd(span)) { + span.updateDrawState(textPaint); // Override the TextPaint + } + } + } + } + Typeface typeface = textPaint.getTypeface(); + String systemFontFamilyName = null; + int textWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED; + int textStyle = Typeface.NORMAL; + if (typeface != null) { + systemFontFamilyName = typeface.getSystemFontFamilyName(); + textWeight = typeface.getWeight(); + textStyle = typeface.getStyle(); + } + TextAppearanceInfo.Builder builder = new TextAppearanceInfo.Builder(); + builder.setTextSize(textPaint.getTextSize()) + .setTextLocales(textPaint.getTextLocales()) + .setSystemFontFamilyName(systemFontFamilyName) + .setTextFontWeight(textWeight) + .setTextStyle(textStyle) + .setShadowDx(textPaint.getShadowLayerDx()) + .setShadowDy(textPaint.getShadowLayerDy()) + .setShadowRadius(textPaint.getShadowLayerRadius()) + .setShadowColor(textPaint.getShadowLayerColor()) + .setElegantTextHeight(textPaint.isElegantTextHeight()) + .setLetterSpacing(textPaint.getLetterSpacing()) + .setFontFeatureSettings(textPaint.getFontFeatureSettings()) + .setFontVariationSettings(textPaint.getFontVariationSettings()) + .setTextScaleX(textPaint.getTextScaleX()) + .setTextColor(textPaint.getColor()) + .setLinkTextColor(textPaint.linkColor) + .setAllCaps(textView.isAllCaps()) + .setFallbackLineSpacing(textView.isFallbackLineSpacing()) + .setLineBreakStyle(textView.getLineBreakStyle()) + .setLineBreakWordStyle(textView.getLineBreakWordStyle()) + .setHighlightTextColor(textView.getHighlightColor()) + .setHintTextColor(textView.getCurrentHintTextColor()); + return builder.build(); + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 0a3ea8a9f356e..e643080ebdd96 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -21,7 +21,6 @@ import static android.widget.TextView.ACCESSIBILITY_ACTION_SMART_START_ID; import android.R; import android.animation.ValueAnimator; -import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -38,7 +37,6 @@ import android.content.UndoOperation; import android.content.UndoOwner; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; @@ -51,10 +49,8 @@ import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.RenderNode; -import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.fonts.FontStyle; import android.os.Build; import android.os.Bundle; import android.os.LocaleList; @@ -4774,41 +4770,7 @@ public class Editor { } if (includeTextAppearance) { - Typeface typeface = mTextView.getPaint().getTypeface(); - String systemFontFamilyName = null; - int textFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED; - if (typeface != null) { - systemFontFamilyName = typeface.getSystemFontFamilyName(); - textFontWeight = typeface.getWeight(); - } - ColorStateList linkTextColors = mTextView.getLinkTextColors(); - @ColorInt int linkTextColor = linkTextColors != null - ? linkTextColors.getDefaultColor() : 0; - - TextAppearanceInfo.Builder appearanceBuilder = new TextAppearanceInfo.Builder(); - appearanceBuilder.setTextSize(mTextView.getTextSize()) - .setTextLocales(mTextView.getTextLocales()) - .setSystemFontFamilyName(systemFontFamilyName) - .setTextFontWeight(textFontWeight) - .setTextStyle(mTextView.getTypefaceStyle()) - .setAllCaps(mTextView.isAllCaps()) - .setShadowDx(mTextView.getShadowDx()) - .setShadowDy(mTextView.getShadowDy()) - .setShadowRadius(mTextView.getShadowRadius()) - .setShadowColor(mTextView.getShadowColor()) - .setElegantTextHeight(mTextView.isElegantTextHeight()) - .setFallbackLineSpacing(mTextView.isFallbackLineSpacing()) - .setLetterSpacing(mTextView.getLetterSpacing()) - .setFontFeatureSettings(mTextView.getFontFeatureSettings()) - .setFontVariationSettings(mTextView.getFontVariationSettings()) - .setLineBreakStyle(mTextView.getLineBreakStyle()) - .setLineBreakWordStyle(mTextView.getLineBreakWordStyle()) - .setTextScaleX(mTextView.getTextScaleX()) - .setHighlightTextColor(mTextView.getHighlightColor()) - .setTextColor(mTextView.getCurrentTextColor()) - .setHintTextColor(mTextView.getCurrentHintTextColor()) - .setLinkTextColor(linkTextColor); - builder.setTextAppearanceInfo(appearanceBuilder.build()); + builder.setTextAppearanceInfo(TextAppearanceInfo.createFromTextView(mTextView)); } imm.updateCursorAnchorInfo(mTextView, builder.build()); diff --git a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java b/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java new file mode 100644 index 0000000000000..f93cd18a8521e --- /dev/null +++ b/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 android.view.inputmethod; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.fonts.FontStyle; +import android.graphics.text.LineBreakConfig; +import android.os.LocaleList; +import android.os.Parcel; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.ScaleXSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TextAppearanceInfoTest { + private static final float EPSILON = 0.0000001f; + private static final String TEST_TEXT = "Happy birthday!"; + private static final float TEXT_SIZE = 16.5f; + private static final LocaleList TEXT_LOCALES = LocaleList.forLanguageTags("en,ja"); + private static final String FONT_FAMILY_NAME = "sans-serif"; + private static final int TEXT_WEIGHT = FontStyle.FONT_WEIGHT_MEDIUM; + private static final int TEXT_STYLE = Typeface.ITALIC; + private static final boolean ALL_CAPS = true; + private static final float SHADOW_DX = 2.0f; + private static final float SHADOW_DY = 2.0f; + private static final float SHADOW_RADIUS = 2.0f; + private static final int SHADOW_COLOR = Color.GRAY; + private static final boolean ELEGANT_TEXT_HEIGHT = true; + private static final boolean FALLBACK_LINE_SPACING = true; + private static final float LETTER_SPACING = 5.0f; + private static final String FONT_FEATURE_SETTINGS = "smcp"; + private static final String FONT_VARIATION_SETTINGS = "'wdth' 1.0"; + private static final int LINE_BREAK_STYLE = LineBreakConfig.LINE_BREAK_STYLE_LOOSE; + private static final int LINE_BREAK_WORD_STYLE = LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE; + private static final float TEXT_SCALEX = 1.5f; + private static final int HIGHLIGHT_TEXT_COLOR = Color.YELLOW; + private static final int TEXT_COLOR = Color.RED; + private static final int HINT_TEXT_COLOR = Color.GREEN; + private static final int LINK_TEXT_COLOR = Color.BLUE; + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private final EditText mEditText = new EditText(mContext); + private final SpannableStringBuilder mSpannableText = new SpannableStringBuilder(TEST_TEXT); + private Canvas mCanvas; + + @Before + public void setUp() { + mEditText.setText(mSpannableText); + mEditText.getPaint().setTextSize(TEXT_SIZE); + mEditText.setTextLocales(TEXT_LOCALES); + Typeface family = Typeface.create(FONT_FAMILY_NAME, Typeface.NORMAL); + mEditText.setTypeface( + Typeface.create(family, TEXT_WEIGHT, (TEXT_STYLE & Typeface.ITALIC) != 0)); + mEditText.setAllCaps(ALL_CAPS); + mEditText.setShadowLayer(SHADOW_RADIUS, SHADOW_DX, SHADOW_DY, SHADOW_COLOR); + mEditText.setElegantTextHeight(ELEGANT_TEXT_HEIGHT); + mEditText.setFallbackLineSpacing(FALLBACK_LINE_SPACING); + mEditText.setLetterSpacing(LETTER_SPACING); + mEditText.setFontFeatureSettings(FONT_FEATURE_SETTINGS); + mEditText.setFontVariationSettings(FONT_VARIATION_SETTINGS); + mEditText.setLineBreakStyle(LINE_BREAK_STYLE); + mEditText.setLineBreakWordStyle(LINE_BREAK_WORD_STYLE); + mEditText.setTextScaleX(TEXT_SCALEX); + mEditText.setHighlightColor(HIGHLIGHT_TEXT_COLOR); + mEditText.setTextColor(TEXT_COLOR); + mEditText.setHintTextColor(HINT_TEXT_COLOR); + mEditText.setLinkTextColor(LINK_TEXT_COLOR); + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + mEditText.setLayoutParams(params); + mEditText.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + Bitmap bitmap = + Bitmap.createBitmap( + Math.max(1, mEditText.getMeasuredWidth()), + Math.max(1, mEditText.getMeasuredHeight()), + Bitmap.Config.ARGB_8888); + mEditText.layout(0, 0, mEditText.getMeasuredWidth(), mEditText.getMeasuredHeight()); + mCanvas = new Canvas(bitmap); + mEditText.draw(mCanvas); + } + + @Test + public void testCreateFromTextView_noSpan() { + assertTextAppearanceInfoContentsEqual(TextAppearanceInfo.createFromTextView(mEditText)); + } + + @Test + public void testCreateFromTextView_withSpan1() { + AbsoluteSizeSpan sizeSpan = new AbsoluteSizeSpan(30); + mSpannableText.setSpan(sizeSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.CYAN); + mSpannableText.setSpan(colorSpan, 1, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + TypefaceSpan typefaceSpan = new TypefaceSpan("cursive"); + mSpannableText.setSpan(typefaceSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + mEditText.setText(mSpannableText); + + // |Happy birthday! + mEditText.setSelection(0); + TextAppearanceInfo info1 = TextAppearanceInfo.createFromTextView(mEditText); + assertEquals(info1.getTextSize(), TEXT_SIZE, EPSILON); + assertEquals(info1.getTextColor(), TEXT_COLOR); + assertEquals(info1.getSystemFontFamilyName(), FONT_FAMILY_NAME); + + // H|appy birthday! + mEditText.setSelection(1); + TextAppearanceInfo info2 = TextAppearanceInfo.createFromTextView(mEditText); + assertEquals(info2.getTextSize(), 30f, EPSILON); + assertEquals(info2.getTextColor(), TEXT_COLOR); + assertEquals(info2.getSystemFontFamilyName(), FONT_FAMILY_NAME); + + // Ha|ppy birthday! + mEditText.setSelection(2); + TextAppearanceInfo info3 = TextAppearanceInfo.createFromTextView(mEditText); + assertEquals(info3.getTextSize(), 30f, EPSILON); + assertEquals(info3.getTextColor(), Color.CYAN); + assertEquals(info3.getSystemFontFamilyName(), FONT_FAMILY_NAME); + + // Ha[ppy birthday!] + mEditText.setSelection(2, mSpannableText.length()); + TextAppearanceInfo info4 = TextAppearanceInfo.createFromTextView(mEditText); + assertEquals(info4.getTextSize(), 30f, EPSILON); + assertEquals(info4.getTextColor(), Color.CYAN); + assertEquals(info4.getSystemFontFamilyName(), FONT_FAMILY_NAME); + + // Happy| birthday! + mEditText.setSelection(5); + TextAppearanceInfo info5 = TextAppearanceInfo.createFromTextView(mEditText); + assertEquals(info5.getTextSize(), 30f, EPSILON); + assertEquals(info5.getTextColor(), Color.CYAN); + assertEquals(info5.getSystemFontFamilyName(), "cursive"); + } + + @Test + public void testCreateFromTextView_withSpan2() { + // aab| + SpannableStringBuilder spannableText = new SpannableStringBuilder("aab"); + + AbsoluteSizeSpan sizeSpan = new AbsoluteSizeSpan(30); + spannableText.setSpan(sizeSpan, 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.CYAN); + spannableText.setSpan(colorSpan, 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + StyleSpan styleSpan = new StyleSpan(Typeface.BOLD); + spannableText.setSpan(styleSpan, 1, 2, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); + + TypefaceSpan typefaceSpan = new TypefaceSpan("cursive"); + spannableText.setSpan(typefaceSpan, 3, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + ScaleXSpan scaleXSpan = new ScaleXSpan(2.0f); + spannableText.setSpan(scaleXSpan, 3, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + mEditText.setText(spannableText); + mEditText.setSelection(3); + TextAppearanceInfo info = TextAppearanceInfo.createFromTextView(mEditText); + + // The character before cursor 'b' should only have an AbsoluteSizeSpan. + assertEquals(info.getTextSize(), 30f, EPSILON); + assertEquals(info.getTextColor(), TEXT_COLOR); + assertEquals(info.getTextStyle(), TEXT_STYLE); + assertEquals(info.getSystemFontFamilyName(), FONT_FAMILY_NAME); + assertEquals(info.getTextScaleX(), TEXT_SCALEX, EPSILON); + } + + @Test + public void testCreateFromTextView_contradictorySpans() { + // Set multiple contradictory spans + AbsoluteSizeSpan sizeSpan1 = new AbsoluteSizeSpan(30); + CustomForegroundColorSpan colorSpan1 = new CustomForegroundColorSpan(Color.BLUE); + AbsoluteSizeSpan sizeSpan2 = new AbsoluteSizeSpan(10); + CustomForegroundColorSpan colorSpan2 = new CustomForegroundColorSpan(Color.GREEN); + + mSpannableText.setSpan(sizeSpan1, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpannableText.setSpan(colorSpan1, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpannableText.setSpan(sizeSpan2, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpannableText.setSpan(colorSpan2, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + mEditText.setText(mSpannableText); + mEditText.draw(mCanvas); + mEditText.setSelection(3); + // Get a copy of the real TextPaint after setting the last span + TextPaint realTextPaint = colorSpan2.lastTextPaint; + assertNotNull(realTextPaint); + TextAppearanceInfo info1 = TextAppearanceInfo.createFromTextView(mEditText); + // Verify the real TextPaint equals the last span of multiple contradictory spans + assertEquals(info1.getTextSize(), 10f, EPSILON); + assertEquals(info1.getTextSize(), realTextPaint.getTextSize(), EPSILON); + assertEquals(info1.getTextColor(), Color.GREEN); + assertEquals(info1.getTextColor(), realTextPaint.getColor()); + assertEquals(info1.getSystemFontFamilyName(), FONT_FAMILY_NAME); + } + + private void assertTextAppearanceInfoContentsEqual(TextAppearanceInfo textAppearanceInfo) { + assertEquals(textAppearanceInfo.getTextSize(), TEXT_SIZE, EPSILON); + assertEquals(textAppearanceInfo.getTextLocales(), TEXT_LOCALES); + assertEquals(textAppearanceInfo.getSystemFontFamilyName(), FONT_FAMILY_NAME); + assertEquals(textAppearanceInfo.getTextFontWeight(), TEXT_WEIGHT); + assertEquals(textAppearanceInfo.getTextStyle(), TEXT_STYLE); + assertEquals(textAppearanceInfo.isAllCaps(), ALL_CAPS); + assertEquals(textAppearanceInfo.getShadowRadius(), SHADOW_RADIUS, EPSILON); + assertEquals(textAppearanceInfo.getShadowDx(), SHADOW_DX, EPSILON); + assertEquals(textAppearanceInfo.getShadowDy(), SHADOW_DY, EPSILON); + assertEquals(textAppearanceInfo.getShadowColor(), SHADOW_COLOR); + assertEquals(textAppearanceInfo.isElegantTextHeight(), ELEGANT_TEXT_HEIGHT); + assertEquals(textAppearanceInfo.isFallbackLineSpacing(), FALLBACK_LINE_SPACING); + assertEquals(textAppearanceInfo.getLetterSpacing(), LETTER_SPACING, EPSILON); + assertEquals(textAppearanceInfo.getFontFeatureSettings(), FONT_FEATURE_SETTINGS); + assertEquals(textAppearanceInfo.getFontVariationSettings(), FONT_VARIATION_SETTINGS); + assertEquals(textAppearanceInfo.getLineBreakStyle(), LINE_BREAK_STYLE); + assertEquals(textAppearanceInfo.getLineBreakWordStyle(), LINE_BREAK_WORD_STYLE); + assertEquals(textAppearanceInfo.getTextScaleX(), TEXT_SCALEX, EPSILON); + assertEquals(textAppearanceInfo.getTextColor(), TEXT_COLOR); + assertEquals(textAppearanceInfo.getHighlightTextColor(), HIGHLIGHT_TEXT_COLOR); + assertEquals(textAppearanceInfo.getHintTextColor(), HINT_TEXT_COLOR); + assertEquals(textAppearanceInfo.getLinkTextColor(), LINK_TEXT_COLOR); + } + + static class CustomForegroundColorSpan extends ForegroundColorSpan { + @Nullable public TextPaint lastTextPaint = null; + + CustomForegroundColorSpan(int color) { + super(color); + } + + CustomForegroundColorSpan(@NonNull Parcel src) { + super(src); + } + + @Override + public void updateDrawState(@NonNull TextPaint tp) { + super.updateDrawState(tp); + // Copy the real TextPaint + TextPaint tpCopy = new TextPaint(); + tpCopy.set(tp); + lastTextPaint = tpCopy; + } + } +}