Merge changes from topic 'ksk_long_text_edit'
* changes:
Always redraw text that protrude from line bounds.
Lazy RenderNode creation to improve performance.
diff --git a/apct-tests/perftests/core/src/android/text/DynamicLayoutPerfTest.java b/apct-tests/perftests/core/src/android/text/DynamicLayoutPerfTest.java
new file mode 100644
index 0000000..e644a1f
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/text/DynamicLayoutPerfTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 3c7741d..9ac8996 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -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 @@
.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 @@
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 @@
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 @@
}
}
+ 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.
@@ -409,17 +431,41 @@
}
/**
+ * @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
* not on the end line of the previous block.
*/
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 @@
final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
if (line > previousBlockEndLine) {
mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
+ updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
mNumberOfBlocks++;
}
}
@@ -506,13 +553,25 @@
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 @@
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;
}
}
@@ -577,6 +639,21 @@
/**
* @hide
*/
+ public int getBlockIndex(int index) {
+ return mBlockIndices[index];
+ }
+
+ /**
+ * @hide
+ * @param index
+ */
+ public void setBlockIndex(int index, int blockIndex) {
+ mBlockIndices[index] = blockIndex;
+ }
+
+ /**
+ * @hide
+ */
public int getNumberOfBlocks() {
return mNumberOfBlocks;
}
@@ -645,7 +722,12 @@
*/
@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 @@
// 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 @@
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 @@
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;
}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index bb131a0..70d183d 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -1181,7 +1181,7 @@
*/
@Override
public int getHyphen(int line) {
- return mLines[mColumns * line + HYPHEN] & 0xff;
+ return mLines[mColumns * line + HYPHEN] & HYPHEN_MASK;
}
/**
@@ -1295,6 +1295,7 @@
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
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index a0447a6..bf49048 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -70,6 +70,7 @@
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;
@@ -176,11 +177,21 @@
InputMethodState mInputMethodState;
private static class TextRenderNode {
+ // Render node has 3 recording states:
+ // 1. Recorded operations are valid.
+ // #needsRecord() returns false, but needsToBeShifted is false.
+ // 2. Recorded operations are not valid, but just the position needed to be updated.
+ // #needsRecord() returns false, but needsToBeShifted is true.
+ // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
+ // true.
RenderNode renderNode;
boolean isDirty;
+ // Becomes true when recorded operations can be reused, but the position has to be updated.
+ boolean needsToBeShifted;
public TextRenderNode(String name) {
- isDirty = true;
renderNode = RenderNode.create(name, null);
+ isDirty = true;
+ needsToBeShifted = true;
}
boolean needsRecord() {
return isDirty || !renderNode.isValid();
@@ -1686,85 +1697,138 @@
final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
- int endOfPreviousBlock = -1;
- int searchStartIndex = 0;
- for (int i = 0; i < numberOfBlocks; i++) {
- int blockEndLine = blockEndLines[i];
- int blockIndex = blockIndices[i];
-
- final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
- if (blockIsInvalid) {
- blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
- searchStartIndex);
- // Note how dynamic layout's internal block indices get updated from Editor
- blockIndices[i] = blockIndex;
- if (mTextRenderNodes[blockIndex] != null) {
- mTextRenderNodes[blockIndex].isDirty = true;
+ 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;
}
- searchStartIndex = blockIndex + 1;
}
-
- if (mTextRenderNodes[blockIndex] == null) {
- mTextRenderNodes[blockIndex] =
- new TextRenderNode("Text " + blockIndex);
- }
-
- final boolean blockDisplayListIsInvalid =
- mTextRenderNodes[blockIndex].needsRecord();
- RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
- if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) {
- final int blockBeginLine = endOfPreviousBlock + 1;
- final int top = layout.getLineTop(blockBeginLine);
- final int bottom = layout.getLineBottom(blockEndLine);
- int left = 0;
- int right = mTextView.getWidth();
- if (mTextView.getHorizontallyScrolling()) {
- float min = Float.MAX_VALUE;
- float max = Float.MIN_VALUE;
- for (int line = blockBeginLine; line <= blockEndLine; line++) {
- min = Math.min(min, layout.getLineLeft(line));
- max = Math.max(max, layout.getLineRight(line));
- }
- left = (int) min;
- right = (int) (max + 0.5f);
- }
-
- // Rebuild display list if it is invalid
- if (blockDisplayListIsInvalid) {
- final DisplayListCanvas displayListCanvas = blockDisplayList.start(
- right - left, bottom - top);
- try {
- // drawText is always relative to TextView's origin, this translation
- // brings this range of text back to the top left corner of the viewport
- displayListCanvas.translate(-left, -top);
- layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
- mTextRenderNodes[blockIndex].isDirty = false;
- // No need to untranslate, previous context is popped after
- // drawDisplayList
- } finally {
- blockDisplayList.end(displayListCanvas);
- // Same as drawDisplayList below, handled by our TextView's parent
- blockDisplayList.setClipToBounds(false);
- }
- }
-
- // Valid disply list whose index is >= indexFirstChangedBlock
- // only needs to update its drawing location.
- blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
- }
-
- ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
-
- endOfPreviousBlock = blockEndLine;
}
- dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks);
+ int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
+ if (startBlock < 0) {
+ startBlock = -(startBlock + 1);
+ }
+ startBlock = Math.min(indexFirstChangedBlock, startBlock);
+
+ int startIndexToFindAvailableRenderNode = 0;
+ int lastIndex = numberOfBlocks;
+
+ for (int i = startBlock; i < numberOfBlocks; i++) {
+ final int blockIndex = blockIndices[i];
+ if (i >= indexFirstChangedBlock
+ && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
+ && mTextRenderNodes[blockIndex] != null) {
+ mTextRenderNodes[blockIndex].needsToBeShifted = true;
+ }
+ if (blockEndLines[i] < firstLine) {
+ // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
+ // be redrawn after they get scrolled into drawing range.
+ continue;
+ }
+ startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
+ highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
+ blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
+ if (blockEndLines[i] >= lastLine) {
+ lastIndex = Math.max(indexFirstChangedBlock, i + 1);
+ 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 {
// Boring layout is used for empty and hint text
layout.drawText(canvas, firstLine, lastLine);
}
}
+ private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
+ Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
+ int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
+ int startIndexToFindAvailableRenderNode) {
+ final int blockEndLine = blockEndLines[blockInfoIndex];
+ int blockIndex = blockIndices[blockInfoIndex];
+
+ final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
+ if (blockIsInvalid) {
+ blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
+ startIndexToFindAvailableRenderNode);
+ // Note how dynamic layout's internal block indices get updated from Editor
+ blockIndices[blockInfoIndex] = blockIndex;
+ if (mTextRenderNodes[blockIndex] != null) {
+ mTextRenderNodes[blockIndex].isDirty = true;
+ }
+ startIndexToFindAvailableRenderNode = blockIndex + 1;
+ }
+
+ if (mTextRenderNodes[blockIndex] == null) {
+ mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
+ }
+
+ final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
+ RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
+ if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
+ final int blockBeginLine = blockInfoIndex == 0 ?
+ 0 : blockEndLines[blockInfoIndex - 1] + 1;
+ final int top = layout.getLineTop(blockBeginLine);
+ final int bottom = layout.getLineBottom(blockEndLine);
+ int left = 0;
+ int right = mTextView.getWidth();
+ if (mTextView.getHorizontallyScrolling()) {
+ float min = Float.MAX_VALUE;
+ float max = Float.MIN_VALUE;
+ for (int line = blockBeginLine; line <= blockEndLine; line++) {
+ min = Math.min(min, layout.getLineLeft(line));
+ max = Math.max(max, layout.getLineRight(line));
+ }
+ left = (int) min;
+ right = (int) (max + 0.5f);
+ }
+
+ // Rebuild display list if it is invalid
+ if (blockDisplayListIsInvalid) {
+ final DisplayListCanvas displayListCanvas = blockDisplayList.start(
+ right - left, bottom - top);
+ try {
+ // drawText is always relative to TextView's origin, this translation
+ // brings this range of text back to the top left corner of the viewport
+ displayListCanvas.translate(-left, -top);
+ layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
+ mTextRenderNodes[blockIndex].isDirty = false;
+ // No need to untranslate, previous context is popped after
+ // drawDisplayList
+ } finally {
+ blockDisplayList.end(displayListCanvas);
+ // Same as drawDisplayList below, handled by our TextView's parent
+ blockDisplayList.setClipToBounds(false);
+ }
+ }
+
+ // Valid display list only needs to update its drawing location.
+ blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
+ mTextRenderNodes[blockIndex].needsToBeShifted = false;
+ }
+ ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
+ return startIndexToFindAvailableRenderNode;
+ }
+
private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
int searchStartIndex) {
int length = mTextRenderNodes.length;
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutTest.java b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
new file mode 100644
index 0000000..9362ed9
--- /dev/null
+++ b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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());
+ }
+}
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 3cbc4f0..81bbfa9 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -2397,13 +2397,12 @@
* Note: just like Canvas.drawText, this will respect the Align setting in
* the paint.
*
- * @param text The text to retrieve the path from
- * @param index The index of the first character in text
- * @param count The number of characterss starting with index
- * @param x The x coordinate of the text's origin
- * @param y The y coordinate of the text's origin
- * @param path The path to receive the data describing the text. Must
- * be allocated by the caller.
+ * @param text the text to retrieve the path from
+ * @param index the index of the first character in text
+ * @param count the number of characters starting with index
+ * @param x the x coordinate of the text's origin
+ * @param y the y coordinate of the text's origin
+ * @param path the path to receive the data describing the text. Must be allocated by the caller
*/
public void getTextPath(char[] text, int index, int count,
float x, float y, Path path) {
@@ -2419,13 +2418,12 @@
* Note: just like Canvas.drawText, this will respect the Align setting
* in the paint.
*
- * @param text The text to retrieve the path from
- * @param start The first character in the text
- * @param end 1 past the last charcter in the text
- * @param x The x coordinate of the text's origin
- * @param y The y coordinate of the text's origin
- * @param path The path to receive the data describing the text. Must
- * be allocated by the caller.
+ * @param text the text to retrieve the path from
+ * @param start the first character in the text
+ * @param end 1 past the last character in the text
+ * @param x the x coordinate of the text's origin
+ * @param y the y coordinate of the text's origin
+ * @param path the path to receive the data describing the text. Must be allocated by the caller
*/
public void getTextPath(String text, int start, int end,
float x, float y, Path path) {
@@ -2440,11 +2438,10 @@
* Return in bounds (allocated by the caller) the smallest rectangle that
* encloses all of the characters, with an implied origin at (0,0).
*
- * @param text String to measure and return its bounds
- * @param start Index of the first char in the string to measure
- * @param end 1 past the last char in the string measure
- * @param bounds Returns the unioned bounds of all the text. Must be
- * allocated by the caller.
+ * @param text string to measure and return its bounds
+ * @param start index of the first char in the string to measure
+ * @param end 1 past the last char in the string to measure
+ * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
*/
public void getTextBounds(String text, int start, int end, Rect bounds) {
if ((start | end | (end - start) | (text.length() - end)) < 0) {
@@ -2460,11 +2457,33 @@
* Return in bounds (allocated by the caller) the smallest rectangle that
* encloses all of the characters, with an implied origin at (0,0).
*
- * @param text Array of chars to measure and return their unioned bounds
- * @param index Index of the first char in the array to measure
- * @param count The number of chars, beginning at index, to measure
- * @param bounds Returns the unioned bounds of all the text. Must be
- * allocated by the caller.
+ * @param text text to measure and return its bounds
+ * @param start index of the first char in the text to measure
+ * @param end 1 past the last char in the text to measure
+ * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
+ * @hide
+ */
+ public void getTextBounds(CharSequence text, int start, int end, Rect bounds) {
+ if ((start | end | (end - start) | (text.length() - end)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (bounds == null) {
+ throw new NullPointerException("need bounds Rect");
+ }
+ char[] buf = TemporaryBuffer.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ getTextBounds(buf, 0, end - start, bounds);
+ TemporaryBuffer.recycle(buf);
+ }
+
+ /**
+ * Return in bounds (allocated by the caller) the smallest rectangle that
+ * encloses all of the characters, with an implied origin at (0,0).
+ *
+ * @param text array of chars to measure and return their unioned bounds
+ * @param index index of the first char in the array to measure
+ * @param count the number of chars, beginning at index, to measure
+ * @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
*/
public void getTextBounds(char[] text, int index, int count, Rect bounds) {
if ((index | count) < 0 || index + count > text.length) {