Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 44fdc24a authored by Ziqi Chen's avatar Ziqi Chen Committed by Android (Google) Code Review
Browse files

Merge "Make TextAppearanceInfo support spannable text"

parents 7eb3a2db 222e6821
Loading
Loading
Loading
Loading
+67 −0
Original line number Diff line number Diff line
@@ -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;
+1 −39
Original line number Diff line number Diff line
@@ -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());

+281 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}