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

Commit f0336f8a authored by Seigo Nonaka's avatar Seigo Nonaka Committed by Android (Google) Code Review
Browse files

Merge "Add and implement inter character justification" into main

parents c07bc276 99893a1e
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -17875,6 +17875,7 @@ package android.graphics.text {
    field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2
    field public static final int HYPHENATION_FREQUENCY_NONE = 0; // 0x0
    field public static final int HYPHENATION_FREQUENCY_NORMAL = 1; // 0x1
    field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
    field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
    field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
  }
@@ -46954,6 +46955,7 @@ package android.text {
    field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP;
    field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL;
    field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER;
    field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
    field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
    field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
  }
+13 −5
Original line number Diff line number Diff line
@@ -152,7 +152,8 @@ public abstract class Layout {
    /** @hide */
    @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
            LineBreaker.JUSTIFICATION_MODE_NONE,
            LineBreaker.JUSTIFICATION_MODE_INTER_WORD
            LineBreaker.JUSTIFICATION_MODE_INTER_WORD,
            LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface JustificationMode {}
@@ -168,6 +169,13 @@ public abstract class Layout {
    public static final int JUSTIFICATION_MODE_INTER_WORD =
            LineBreaker.JUSTIFICATION_MODE_INTER_WORD;

    /**
     * Value for justification mode indicating the text is justified by stretching letter spacing.
     */
    @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
    public static final int JUSTIFICATION_MODE_INTER_CHARACTER =
            LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER;

    /*
     * Line spacing multiplier for default line spacing.
     */
@@ -809,7 +817,7 @@ public abstract class Layout {
                        getEllipsisStart(lineNum) + getEllipsisCount(lineNum),
                        isFallbackLineSpacingEnabled());
                if (justify) {
                    tl.justify(right - left - indentWidth);
                    tl.justify(mJustificationMode, right - left - indentWidth);
                }
                tl.draw(canvas, x, ltop, lbaseline, lbottom);
            }
@@ -1058,7 +1066,7 @@ public abstract class Layout {
                    getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                    isFallbackLineSpacingEnabled());
            if (isJustificationRequired(line)) {
                tl.justify(getJustifyWidth(line));
                tl.justify(mJustificationMode, getJustifyWidth(line));
            }
            tl.metrics(null, rectF, false, null);

@@ -1794,7 +1802,7 @@ public abstract class Layout {
                getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                isFallbackLineSpacingEnabled());
        if (isJustificationRequired(line)) {
            tl.justify(getJustifyWidth(line));
            tl.justify(mJustificationMode, getJustifyWidth(line));
        }
        final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
        TextLine.recycle(tl);
@@ -1882,7 +1890,7 @@ public abstract class Layout {
                getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                isFallbackLineSpacingEnabled());
        if (isJustificationRequired(line)) {
            tl.justify(getJustifyWidth(line));
            tl.justify(mJustificationMode, getJustifyWidth(line));
        }
        final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
        TextLine.recycle(tl);
+56 −12
Original line number Diff line number Diff line
@@ -100,9 +100,25 @@ public class TextLine {

    // Additional width of whitespace for justification. This value is per whitespace, thus
    // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces).
    private float mAddedWidthForJustify;
    private float mAddedWordSpacingInPx;
    private float mAddedLetterSpacingInPx;
    private boolean mIsJustifying;

    @VisibleForTesting
    public float getAddedWordSpacingInPx() {
        return mAddedWordSpacingInPx;
    }

    @VisibleForTesting
    public float getAddedLetterSpacingInPx() {
        return mAddedLetterSpacingInPx;
    }

    @VisibleForTesting
    public boolean isJustifying() {
        return mIsJustifying;
    }

    private final TextPaint mWorkPaint = new TextPaint();
    private final TextPaint mActivePaint = new TextPaint();
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -259,7 +275,7 @@ public class TextLine {
            }
        }
        mTabs = tabStops;
        mAddedWidthForJustify = 0;
        mAddedWordSpacingInPx = 0;
        mIsJustifying = false;

        mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;
@@ -274,19 +290,42 @@ public class TextLine {
     * Justify the line to the given width.
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public void justify(float justifyWidth) {
    public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) {
        int end = mLen;
        while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
            end--;
        }
        if (justificationMode == Layout.JUSTIFICATION_MODE_INTER_WORD) {
            float width = Math.abs(measure(end, false, null, null, null));
            final int spaces = countStretchableSpaces(0, end);
            if (spaces == 0) {
                // There are no stretchable spaces, so we can't help the justification by adding any
                // width.
                return;
            }
        final float width = Math.abs(measure(end, false, null, null, null));
        mAddedWidthForJustify = (justifyWidth - width) / spaces;
            mAddedWordSpacingInPx = (justifyWidth - width) / spaces;
            mAddedLetterSpacingInPx = 0;
        } else {  // justificationMode == Layout.JUSTIFICATION_MODE_INTER_CHARACTER
            LineInfo lineInfo = new LineInfo();
            float width = Math.abs(measure(end, false, null, null, lineInfo));

            int lettersCount = lineInfo.getClusterCount();
            if (lettersCount < 2) {
                return;
            }
            mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
            if (mAddedLetterSpacingInPx > 0.03) {
                // If the letter spacing is more than 0.03em, the ligatures are automatically
                // disabled, so re-calculate everything without ligatures.
                final String oldFontFeatures = mPaint.getFontFeatureSettings();
                mPaint.setFontFeatureSettings(oldFontFeatures + ", \"liga\" off, \"cliga\" off");
                width = Math.abs(measure(end, false, null, null, lineInfo));
                lettersCount = lineInfo.getClusterCount();
                mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
                mPaint.setFontFeatureSettings(oldFontFeatures);
            }
            mAddedWordSpacingInPx = 0;
        }
        mIsJustifying = true;
    }

@@ -529,6 +568,9 @@ public class TextLine {
            throw new IndexOutOfBoundsException(
                    "offset(" + offset + ") should be less than line limit(" + mLen + ")");
        }
        if (lineInfo != null) {
            lineInfo.setClusterCount(0);
        }
        final int target = trailing ? offset - 1 : offset;
        if (target < 0) {
            return 0;
@@ -1076,7 +1118,8 @@ public class TextLine {
        TextPaint wp = mWorkPaint;
        wp.set(mPaint);
        if (mIsJustifying) {
            wp.setWordSpacing(mAddedWidthForJustify);
            wp.setWordSpacing(mAddedWordSpacingInPx);
            wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize());  // Convert to Em
        }

        int spanStart = runStart;
@@ -1277,7 +1320,8 @@ public class TextLine {
            @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo,
            int runFlag) {
        if (mIsJustifying) {
            wp.setWordSpacing(mAddedWidthForJustify);
            wp.setWordSpacing(mAddedWordSpacingInPx);
            wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize());  // Convert to Em
        }
        // Get metrics first (even for empty strings or "0" width runs)
        if (drawBounds != null && fmi == null) {
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.text

import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class TextLineJustificationTest {

    @Rule
    @JvmField
    val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()

    private val PAINT = TextPaint().apply {
        textSize = 10f // make 1em = 10px
    }

    private fun makeTextLine(cs: CharSequence, paint: TextPaint) = TextLine.obtain().apply {
        set(paint, cs, 0, cs.length, Layout.DIR_LEFT_TO_RIGHT, Layout.DIRS_ALL_LEFT_TO_RIGHT, false,
                null, 0, 0, false)
    }

    private fun getClusterCount(cs: CharSequence, paint: TextPaint) = TextLine.LineInfo().apply {
        makeTextLine(cs, paint).also {
            it.metrics(null, null, false, this)
            TextLine.recycle(it)
        }
    }.clusterCount

    fun justifyTest_WithoutJustify() {
        val line = "Hello, World."
        val tl = makeTextLine(line, PAINT)

        // Without calling justify method, justifying should be false and all added spaces should
        // be zeros.
        assertThat(tl.isJustifying).isFalse()
        assertThat(tl.addedWordSpacingInPx).isEqualTo(0)
        assertThat(tl.addedLetterSpacingInPx).isEqualTo(0)
    }

    @Test
    fun justifyTest_IntrCharacter_Latin() {
        val line = "Hello, World."
        val clusterCount = getClusterCount(line, PAINT)
        val originalWidth = Layout.getDesiredWidth(line, PAINT)
        val extraWidth = 100f

        val tl = makeTextLine(line, PAINT)
        tl.justify(Layout.JUSTIFICATION_MODE_INTER_CHARACTER, originalWidth + extraWidth)

        assertThat(tl.isJustifying).isTrue()
        assertThat(tl.addedWordSpacingInPx).isEqualTo(0)
        assertThat(tl.addedLetterSpacingInPx).isEqualTo(extraWidth / (clusterCount - 1))

        TextLine.recycle(tl)
    }

    @Test
    fun justifyTest_IntrCharacter_Japanese() {
        val line = "\u672C\u65E5\u306F\u6674\u5929\u306A\u308A\u3002"
        val clusterCount = getClusterCount(line, PAINT)
        val originalWidth = Layout.getDesiredWidth(line, PAINT)
        val extraWidth = 100f

        val tl = makeTextLine(line, PAINT)
        tl.justify(Layout.JUSTIFICATION_MODE_INTER_CHARACTER, originalWidth + extraWidth)

        assertThat(tl.isJustifying).isTrue()
        assertThat(tl.addedWordSpacingInPx).isEqualTo(0)
        assertThat(tl.addedLetterSpacingInPx).isEqualTo(extraWidth / (clusterCount - 1))

        TextLine.recycle(tl)
    }

    @Test
    fun justifyTest_IntrWord_Latin() {
        val line = "Hello, World."
        val originalWidth = Layout.getDesiredWidth(line, PAINT)
        val extraWidth = 100f

        val tl = makeTextLine(line, PAINT)
        tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, originalWidth + extraWidth)

        assertThat(tl.isJustifying).isTrue()
        // This text contains only one whitespace, so word spacing should be same to the extraWidth.
        assertThat(tl.addedWordSpacingInPx).isEqualTo(extraWidth)
        assertThat(tl.addedLetterSpacingInPx).isEqualTo(0)

        TextLine.recycle(tl)
    }

    @Test
    fun justifyTest_IntrWord_Japanese() {
        val line = "\u672C\u65E5\u306F\u6674\u0020\u5929\u306A\u308A\u3002"
        val originalWidth = Layout.getDesiredWidth(line, PAINT)
        val extraWidth = 100f

        val tl = makeTextLine(line, PAINT)
        tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, originalWidth + extraWidth)

        assertThat(tl.isJustifying).isTrue()
        // This text contains only one whitespace, so word spacing should be same to the extraWidth.
        assertThat(tl.addedWordSpacingInPx).isEqualTo(extraWidth)
        assertThat(tl.addedLetterSpacingInPx).isEqualTo(0)

        TextLine.recycle(tl)
    }
}
 No newline at end of file
+1 −1
Original line number Diff line number Diff line
@@ -53,7 +53,7 @@ public class TextLineTest {
        final float originalWidth = tl.metrics(null, null, false, null);
        final float expandedWidth = 2 * originalWidth;

        tl.justify(expandedWidth);
        tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, expandedWidth);
        final float newWidth = tl.metrics(null, null, false, null);
        TextLine.recycle(tl);
        return Math.abs(newWidth - expandedWidth) < 0.5;
Loading