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

Commit f5af4a34 authored by Keisuke Kuroyanagi's avatar Keisuke Kuroyanagi
Browse files

Always redraw text that protrude from line bounds.

With I63af3a6ecbf92, we create RenderNode lazily, but
blocks containing contents that protrude from line top or
bottom cannot be simply lazily redrawn after edit or
scroll.
With this CL, we check if the contents protrude from line
top or bottom by comparing the text bounds with relevant
font metrics values and we always redrawn such blocks after
edit or scroll.

Bug: 27889485
Change-Id: I666da5eeb39f780c341597f347bfcba21eb34295
parent 499c1596
Loading
Loading
Loading
Loading
+142 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.os.Bundle;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import android.perftests.utils.StubActivity;

import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.text.style.ReplacementSpan;
import android.util.ArraySet;

import static android.text.Layout.Alignment.ALIGN_NORMAL;

import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Random;

import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runners.Parameterized;

@LargeTest
@RunWith(Parameterized.class)
public class DynamicLayoutPerfTest {

    @Parameters(name = "{0}")
    public static Collection cases() {
        return Arrays.asList(new Object[][] {
            { "0%", 0.0f},
            { "1%", 0.01f},
            { "5%", 0.05f},
            { "30%", 0.3f},
            { "100%", 1.0f},
        });
    }

    private final String mMetricKey;
    private final float mProbability;
    public DynamicLayoutPerfTest(String metricKey, float probability) {
        mMetricKey = metricKey;
        mProbability = probability;
    }

    private static class MockReplacementSpan extends ReplacementSpan {
        @Override
        public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
            return 10;
        }

        @Override
        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
                int y, int bottom, Paint paint) {
        }
    }

    @Rule
    public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);

    @Rule
    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();


    private final static String ALPHABETS = "abcdefghijklmnopqrstuvwxyz";

    private SpannableStringBuilder getText() {
        final long seed = 1234567890;
        final Random r = new Random(seed);
        final SpannableStringBuilder builder = new SpannableStringBuilder();

        final int paragraphCount = 100;
        for (int i = 0; i < paragraphCount; i++) {
            final int wordCount = 5 + r.nextInt(20);
            final boolean containsReplacementSpan = r.nextFloat() < mProbability;
            final int replacedWordIndex = containsReplacementSpan ? r.nextInt(wordCount) : -1;
            for (int j = 0; j < wordCount; j++) {
                final int startIndex = builder.length();
                final int wordLength = 1 + r.nextInt(10);
                for (int k = 0; k < wordLength; k++) {
                    char c = ALPHABETS.charAt(r.nextInt(ALPHABETS.length()));
                    builder.append(c);
                }
                if (replacedWordIndex == j) {
                    builder.setSpan(new MockReplacementSpan(), startIndex,
                            builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
                builder.append(' ');
            }
            builder.append('\n');
        }
        return builder;
    }

    @Test
    public void testGetBlocksAlwaysNeedToBeRedrawn() {
        final SpannableStringBuilder text = getText();
        final DynamicLayout layout = new DynamicLayout(text, new TextPaint(), 1000,
                ALIGN_NORMAL, 0, 0, false);

        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        final int steps = 10;
        while (state.keepRunning()) {
            for (int i = 0; i < steps; i++) {
                int offset = (text.length() * i) / steps;
                text.insert(offset, "\n");
                text.delete(offset, offset + 1);
                final ArraySet<Integer> set = layout.getBlocksAlwaysNeedToBeRedrawn();
                if (set != null) {
                    for (int j = 0; j < set.size(); j++) {
                        layout.getBlockIndex(set.valueAt(j));
                    }
                }
            }
        }
    }
}
+99 −8
Original line number Diff line number Diff line
@@ -17,8 +17,11 @@
package android.text;

import android.graphics.Paint;
import android.graphics.Rect;
import android.text.style.ReplacementSpan;
import android.text.style.UpdateLayout;
import android.text.style.WrapTogetherSpan;
import android.util.ArraySet;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
@@ -300,7 +303,6 @@ public class DynamicLayout extends Layout
                .setHyphenationFrequency(mHyphenationFrequency);
        reflowed.generate(b, false, true);
        int n = reflowed.getLineCount();

        // If the new layout has a blank line at the end, but it is not
        // the very end of the buffer, then we already have a line that
        // starts there, so disregard the blank line.
@@ -345,9 +347,10 @@ public class DynamicLayout extends Layout
        Directions[] objects = new Directions[1];

        for (int i = 0; i < n; i++) {
            ints[START] = reflowed.getLineStart(i) |
                          (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
                          (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
            final int start = reflowed.getLineStart(i);
            ints[START] = start;
            ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
            ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;

            int top = reflowed.getLineTop(i) + startv;
            if (i > 0)
@@ -361,7 +364,11 @@ public class DynamicLayout extends Layout
            ints[DESCENT] = desc;
            objects[0] = reflowed.getLineDirections(i);

            ints[HYPHEN] = reflowed.getHyphen(i);
            final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
            ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK;
            ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
                    contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
                            MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;

            if (mEllipsize) {
                ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
@@ -381,6 +388,21 @@ public class DynamicLayout extends Layout
        }
    }

    private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
        if (text instanceof Spanned) {
            final Spanned spanned = (Spanned) text;
            if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
                return true;
            }
        }
        // Spans other than ReplacementSpan can be ignored because line top and bottom are
        // disjunction of all tops and bottoms, although it's not optimal.
        final Paint paint = getPaint();
        paint.getTextBounds(text, start, end, mTempRect);
        final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
        return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
    }

    /**
     * Create the initial block structure, cutting the text into blocks of at least
     * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
@@ -408,6 +430,30 @@ public class DynamicLayout extends Layout
        }
    }

    /**
     * @hide
     */
    public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
        return mBlocksAlwaysNeedToBeRedrawn;
    }

    private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
        int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
        int endLine = mBlockEndLines[blockIndex];
        for (int i = startLine; i <= endLine; i++) {
            if (getContentMayProtrudeFromTopOrBottom(i)) {
                if (mBlocksAlwaysNeedToBeRedrawn == null) {
                    mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
                }
                mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
                return;
            }
        }
        if (mBlocksAlwaysNeedToBeRedrawn != null) {
            mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
        }
    }

    /**
     * Create a new block, ending at the specified character offset.
     * A block will actually be created only if has at least one line, i.e. this offset is
@@ -415,11 +461,11 @@ public class DynamicLayout extends Layout
     */
    private void addBlockAtOffset(int offset) {
        final int line = getLineForOffset(offset);

        if (mBlockEndLines == null) {
            // Initial creation of the array, no test on previous block ending line
            mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
            mBlockEndLines[mNumberOfBlocks] = line;
            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
            mNumberOfBlocks++;
            return;
        }
@@ -427,6 +473,7 @@ public class DynamicLayout extends Layout
        final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
        if (line > previousBlockEndLine) {
            mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
            mNumberOfBlocks++;
        }
    }
@@ -506,13 +553,25 @@ public class DynamicLayout extends Layout
                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
            mBlockEndLines = blockEndLines;
            mBlockIndices = blockIndices;
        } else {
        } else if (numAddedBlocks + numRemovedBlocks != 0) {
            System.arraycopy(mBlockEndLines, lastBlock + 1,
                    mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
            System.arraycopy(mBlockIndices, lastBlock + 1,
                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
        }

        if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
            final ArraySet<Integer> set = new ArraySet<>();
            for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
                Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
                if (block > firstBlock) {
                    block += numAddedBlocks - numRemovedBlocks;
                }
                set.add(block);
            }
            mBlocksAlwaysNeedToBeRedrawn = set;
        }

        mNumberOfBlocks = newNumberOfBlocks;
        int newFirstChangedBlock;
        final int deltaLines = newLineCount - (endLine - startLine + 1);
@@ -531,18 +590,21 @@ public class DynamicLayout extends Layout
        int blockIndex = firstBlock;
        if (createBlockBefore) {
            mBlockEndLines[blockIndex] = startLine - 1;
            updateAlwaysNeedsToBeRedrawn(blockIndex);
            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
            blockIndex++;
        }

        if (createBlock) {
            mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
            updateAlwaysNeedsToBeRedrawn(blockIndex);
            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
            blockIndex++;
        }

        if (createBlockAfter) {
            mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
            updateAlwaysNeedsToBeRedrawn(blockIndex);
            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
        }
    }
@@ -574,6 +636,21 @@ public class DynamicLayout extends Layout
        return mBlockIndices;
    }

    /**
     * @hide
     */
    public int getBlockIndex(int index) {
        return mBlockIndices[index];
    }

    /**
     * @hide
     * @param index
     */
    public void setBlockIndex(int index, int blockIndex) {
        mBlockIndices[index] = blockIndex;
    }

    /**
     * @hide
     */
@@ -645,7 +722,12 @@ public class DynamicLayout extends Layout
     */
    @Override
    public int getHyphen(int line) {
        return mInts.getValue(line, HYPHEN);
        return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
    }

    private boolean getContentMayProtrudeFromTopOrBottom(int line) {
        return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
                & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
    }

    @Override
@@ -741,6 +823,8 @@ public class DynamicLayout extends Layout
    // The indices of this block's display list in TextView's internal display list array or
    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
    private int[] mBlockIndices;
    // Set of blocks that always need to be redrawn.
    private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
    // Number of items actually currently being used in the above 2 arrays
    private int mNumberOfBlocks;
    // The first index of the blocks whose locations are changed
@@ -748,17 +832,22 @@ public class DynamicLayout extends Layout

    private int mTopPadding, mBottomPadding;

    private Rect mTempRect = new Rect();

    private static StaticLayout sStaticLayout = null;
    private static StaticLayout.Builder sBuilder = null;

    private static final Object[] sLock = new Object[0];

    // START, DIR, and TAB share the same entry.
    private static final int START = 0;
    private static final int DIR = START;
    private static final int TAB = START;
    private static final int TOP = 1;
    private static final int DESCENT = 2;
    // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
    private static final int HYPHEN = 3;
    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
    private static final int COLUMNS_NORMAL = 4;

    private static final int ELLIPSIS_START = 4;
@@ -768,6 +857,8 @@ public class DynamicLayout extends Layout
    private static final int START_MASK = 0x1FFFFFFF;
    private static final int DIR_SHIFT  = 30;
    private static final int TAB_MASK   = 0x20000000;
    private static final int HYPHEN_MASK = 0xFF;
    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;

    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
}
+2 −1
Original line number Diff line number Diff line
@@ -1181,7 +1181,7 @@ public class StaticLayout extends Layout {
     */
    @Override
    public int getHyphen(int line) {
        return mLines[mColumns * line + HYPHEN] & 0xff;
        return mLines[mColumns * line + HYPHEN] & HYPHEN_MASK;
    }

    /**
@@ -1295,6 +1295,7 @@ public class StaticLayout extends Layout {
    private static final int START_MASK = 0x1FFFFFFF;
    private static final int DIR_SHIFT  = 30;
    private static final int TAB_MASK   = 0x20000000;
    private static final int HYPHEN_MASK = 0xFF;

    private static final int TAB_INCREMENT = 20; // same as Layout, but that's private

+26 −0
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ import android.text.style.SuggestionRangeSpan;
import android.text.style.SuggestionSpan;
import android.text.style.TextAppearanceSpan;
import android.text.style.URLSpan;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
@@ -1696,6 +1697,17 @@ public class Editor {
            final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
            final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();

            final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
            if (blockSet != null) {
                for (int i = 0; i < blockSet.size(); i++) {
                    final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
                    if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
                            && mTextRenderNodes[blockIndex] != null) {
                        mTextRenderNodes[blockIndex].needsToBeShifted = true;
                    }
                }
            }

            int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
            if (startBlock < 0) {
                startBlock = -(startBlock + 1);
@@ -1725,6 +1737,20 @@ public class Editor {
                    break;
                }
            }
            if (blockSet != null) {
                for (int i = 0; i < blockSet.size(); i++) {
                    final int block = blockSet.valueAt(i);
                    final int blockIndex = dynamicLayout.getBlockIndex(block);
                    if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
                            || mTextRenderNodes[blockIndex] == null
                            || mTextRenderNodes[blockIndex].needsToBeShifted) {
                        startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
                                layout, highlight, highlightPaint, cursorOffsetVertical,
                                blockEndLines, blockIndices, block, numberOfBlocks,
                                startIndexToFindAvailableRenderNode);
                    }
                }
            }

            dynamicLayout.setIndexFirstChangedBlock(lastIndex);
        } else {
+105 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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 static android.text.Layout.Alignment.ALIGN_NORMAL;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.text.style.ReplacementSpan;
import junit.framework.TestCase;

public class DynamicLayoutTest extends TestCase {
    private static final int WIDTH = 10000;

    public void testGetBlocksAlwaysNeedToBeRedrawn_en() {
        final SpannableStringBuilder builder = new SpannableStringBuilder();
        final DynamicLayout layout = new DynamicLayout(builder, new TextPaint(), WIDTH,
                ALIGN_NORMAL, 0, 0, false);

        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());

        builder.append("abcd efg\n");
        builder.append("hijk lmn\n");
        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());

        builder.delete(0, builder.length());
        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());
    }


    private static class MockReplacementSpan extends ReplacementSpan {
        @Override
        public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
            return 10;
        }

        @Override
        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
                int y, int bottom, Paint paint) {
        }
    }

    public void testGetBlocksAlwaysNeedToBeRedrawn_replacementSpan() {
        final SpannableStringBuilder builder = new SpannableStringBuilder();
        final DynamicLayout layout = new DynamicLayout(builder, new TextPaint(), WIDTH,
                ALIGN_NORMAL, 0, 0, false);

        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());

        builder.append("abcd efg\n");
        builder.append("hijk lmn\n");
        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());

        builder.setSpan(new MockReplacementSpan(), 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        assertNotNull(layout.getBlocksAlwaysNeedToBeRedrawn());
        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));

        builder.setSpan(new MockReplacementSpan(), 9, 13, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(1));

        builder.delete(9, 13);
        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
        assertFalse(layout.getBlocksAlwaysNeedToBeRedrawn().contains(1));

        builder.delete(0, 4);
        assertFalse(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().isEmpty());
    }

    public void testGetBlocksAlwaysNeedToBeRedrawn_thai() {
        final SpannableStringBuilder builder = new SpannableStringBuilder();
        final DynamicLayout layout = new DynamicLayout(builder, new TextPaint(), WIDTH,
                ALIGN_NORMAL, 0, 0, false);

        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());

        builder.append("\u0E22\u0E34\u0E19\u0E14\u0E35\u0E15\u0E49\u0E2D\u0E19\u0E23\u0E31\u0E1A");
        builder.append("\u0E2A\u0E39\u0E48");
        assertNull(layout.getBlocksAlwaysNeedToBeRedrawn());

        builder.append("\u0E48\u0E48\u0E48\u0E48\u0E48");
        assertNotNull(layout.getBlocksAlwaysNeedToBeRedrawn());
        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));

        builder.delete(builder.length() -5, builder.length());
        assertFalse(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
        assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().isEmpty());
    }
}
Loading