TextView implementation of select and delete gestures
Bug: 240435922 Test: atest android.text.LayoutGetRangeForRectTest Test: atest android.widget.cts.TextViewHandwritingGestureTest Change-Id: I6ceaa4555b82f1c2650d2db3bf39674641d53b8f
This commit is contained in:
78
core/java/android/text/GraphemeClusterSegmentIterator.java
Normal file
78
core/java/android/text/GraphemeClusterSegmentIterator.java
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.graphics.Paint;
|
||||
|
||||
/**
|
||||
* Implementation of {@code SegmentIterator} using grapheme clusters as the text segment. Whitespace
|
||||
* characters are included as segments.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class GraphemeClusterSegmentIterator extends SegmentIterator {
|
||||
private final CharSequence mText;
|
||||
private final TextPaint mTextPaint;
|
||||
|
||||
public GraphemeClusterSegmentIterator(
|
||||
@NonNull CharSequence text, @NonNull TextPaint textPaint) {
|
||||
mText = text;
|
||||
mTextPaint = textPaint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int previousStartBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = mTextPaint.getTextRunCursor(
|
||||
mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
|
||||
return boundary == -1 ? DONE : boundary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int previousEndBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = mTextPaint.getTextRunCursor(
|
||||
mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
|
||||
// Check that there is another cursor position before, otherwise this is not a valid
|
||||
// end boundary.
|
||||
if (mTextPaint.getTextRunCursor(
|
||||
mText, 0, mText.length(), false, boundary, Paint.CURSOR_BEFORE) == -1) {
|
||||
return DONE;
|
||||
}
|
||||
return boundary == -1 ? DONE : boundary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextStartBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = mTextPaint.getTextRunCursor(
|
||||
mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
|
||||
// Check that there is another cursor position after, otherwise this is not a valid
|
||||
// start boundary.
|
||||
if (mTextPaint.getTextRunCursor(
|
||||
mText, 0, mText.length(), false, boundary, Paint.CURSOR_AFTER) == -1) {
|
||||
return DONE;
|
||||
}
|
||||
return boundary == -1 ? DONE : boundary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextEndBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = mTextPaint.getTextRunCursor(
|
||||
mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
|
||||
return boundary == -1 ? DONE : boundary;
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,13 @@ package android.text;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.compat.annotation.UnsupportedAppUsage;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.text.LineBreaker;
|
||||
import android.os.Build;
|
||||
import android.text.method.TextKeyListener;
|
||||
@@ -710,8 +712,7 @@ public abstract class Layout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the start position of the line, given the left and right bounds
|
||||
* of the margins.
|
||||
* Return the start position of the line, given the left and right bounds of the margins.
|
||||
*
|
||||
* @param line the line index
|
||||
* @param left the left bounds (0, or leading margin if ltr para)
|
||||
@@ -1312,6 +1313,38 @@ public abstract class Layout {
|
||||
return horizontal;
|
||||
}
|
||||
|
||||
private void fillHorizontalBoundsForLine(int line, float[] horizontalBounds) {
|
||||
final int lineStart = getLineStart(line);
|
||||
final int lineEnd = getLineEnd(line);
|
||||
final int lineLength = lineEnd - lineStart;
|
||||
|
||||
final int dir = getParagraphDirection(line);
|
||||
final Directions directions = getLineDirections(line);
|
||||
|
||||
final boolean hasTab = getLineContainsTab(line);
|
||||
TabStops tabStops = null;
|
||||
if (hasTab && mText instanceof Spanned) {
|
||||
// Just checking this line should be good enough, tabs should be
|
||||
// consistent across all lines in a paragraph.
|
||||
TabStopSpan[] tabs =
|
||||
getParagraphSpans((Spanned) mText, lineStart, lineEnd, TabStopSpan.class);
|
||||
if (tabs.length > 0) {
|
||||
tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
|
||||
}
|
||||
}
|
||||
|
||||
final TextLine tl = TextLine.obtain();
|
||||
tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops,
|
||||
getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
|
||||
isFallbackLineSpacingEnabled());
|
||||
if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
|
||||
horizontalBounds = new float[2 * lineLength];
|
||||
}
|
||||
|
||||
tl.measureAllBounds(horizontalBounds, null);
|
||||
TextLine.recycle(tl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the characters' bounds in the given range. The {@code bounds} array will be filled
|
||||
* starting from {@code boundsStart} (inclusive). The coordinates are in local text layout.
|
||||
@@ -1358,32 +1391,11 @@ public abstract class Layout {
|
||||
final int lineStart = getLineStart(line);
|
||||
final int lineEnd = getLineEnd(line);
|
||||
final int lineLength = lineEnd - lineStart;
|
||||
|
||||
final int dir = getParagraphDirection(line);
|
||||
final boolean hasTab = getLineContainsTab(line);
|
||||
final Directions directions = getLineDirections(line);
|
||||
|
||||
TabStops tabStops = null;
|
||||
if (hasTab && mText instanceof Spanned) {
|
||||
// Just checking this line should be good enough, tabs should be
|
||||
// consistent across all lines in a paragraph.
|
||||
TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, lineStart, lineEnd,
|
||||
TabStopSpan.class);
|
||||
if (tabs.length > 0) {
|
||||
tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
|
||||
}
|
||||
}
|
||||
|
||||
final TextLine tl = TextLine.obtain();
|
||||
tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops,
|
||||
getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
|
||||
isFallbackLineSpacingEnabled());
|
||||
if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
|
||||
horizontalBounds = new float[2 * lineLength];
|
||||
}
|
||||
fillHorizontalBoundsForLine(line, horizontalBounds);
|
||||
|
||||
tl.measureAllBounds(horizontalBounds, null);
|
||||
TextLine.recycle(tl);
|
||||
final int lineLeft = getParagraphLeft(line);
|
||||
final int lineRight = getParagraphRight(line);
|
||||
final int lineStartPos = getLineStartPos(line, lineLeft, lineRight);
|
||||
@@ -1801,6 +1813,380 @@ public abstract class Layout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the range of text which is inside the specified rectangle area. The start of the range
|
||||
* is the start of the first text segment inside the area, and the end of the range is the end
|
||||
* of the last text segment inside the area.
|
||||
*
|
||||
* <p>A text segment is considered to be inside the area if the center of its bounds is inside
|
||||
* the area. If a text segment spans multiple lines or multiple directional runs (e.g. a
|
||||
* hyphenated word), the text segment is divided into pieces at the line and run breaks, then
|
||||
* the text segment is considered to be inside the area if any of its pieces have their center
|
||||
* inside the area.
|
||||
*
|
||||
* <p>The returned range may also include text segments which are not inside the specified area,
|
||||
* if those text segments are in between text segments which are inside the area. For example,
|
||||
* the returned range may be "segment1 segment2 segment3" if "segment1" and "segment3" are
|
||||
* inside the area and "segment2" is not.
|
||||
*
|
||||
* @param area area for which the text range will be found
|
||||
* @param segmentIterator iterator for determining the ranges of text to be considered as a text
|
||||
* segment
|
||||
* @return int array of size 2 containing the start (inclusive) and end (exclusive) character
|
||||
* offsets of the range, or null if there are no text segments inside the area
|
||||
* @hide
|
||||
*/
|
||||
@Nullable
|
||||
public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentIterator segmentIterator) {
|
||||
// Find the first line whose vertical center is below the top of the area.
|
||||
int startLine = getLineForVertical((int) area.top);
|
||||
int startLineTop = getLineTop(startLine);
|
||||
int startLineBottom = getLineBottomWithoutSpacing(startLine);
|
||||
if (area.top > (startLineTop + startLineBottom) / 2f) {
|
||||
startLine++;
|
||||
if (startLine >= getLineCount()) {
|
||||
// The top of the area is below the vertical center of the last line, so the area
|
||||
// does not contain any text.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the last line whose vertical center is above the bottom of the area.
|
||||
int endLine = getLineForVertical((int) area.bottom);
|
||||
int endLineTop = getLineTop(endLine);
|
||||
int endLineBottom = getLineBottomWithoutSpacing(endLine);
|
||||
if (area.bottom < (endLineTop + endLineBottom) / 2f) {
|
||||
endLine--;
|
||||
}
|
||||
if (endLine < startLine) {
|
||||
// There are no lines with vertical centers between the top and bottom of the area, so
|
||||
// the area does not contain any text.
|
||||
return null;
|
||||
}
|
||||
|
||||
int start = getStartOrEndOffsetForHorizontalInterval(
|
||||
startLine, area.left, area.right, segmentIterator, /* getStart= */ true);
|
||||
// If the area does not contain any text on this line, keep trying subsequent lines until
|
||||
// the end line is reached.
|
||||
while (start == -1 && startLine < endLine) {
|
||||
startLine++;
|
||||
start = getStartOrEndOffsetForHorizontalInterval(
|
||||
startLine, area.left, area.right, segmentIterator, /* getStart= */ true);
|
||||
}
|
||||
if (start == -1) {
|
||||
// All lines were checked, the area does not contain any text.
|
||||
return null;
|
||||
}
|
||||
|
||||
int end = getStartOrEndOffsetForHorizontalInterval(
|
||||
endLine, area.left, area.right, segmentIterator, /* getStart= */ false);
|
||||
// If the area does not contain any text on this line, keep trying previous lines until
|
||||
// the start line is reached.
|
||||
while (end == -1 && startLine < endLine) {
|
||||
endLine--;
|
||||
end = getStartOrEndOffsetForHorizontalInterval(
|
||||
endLine, area.left, area.right, segmentIterator, /* getStart= */ false);
|
||||
}
|
||||
if (end == -1) {
|
||||
// All lines were checked, the area does not contain any text.
|
||||
return null;
|
||||
}
|
||||
|
||||
// If a text segment spans multiple lines or multiple directional runs (e.g. a hyphenated
|
||||
// word), then getStartOrEndOffsetForHorizontalInterval() can return an offset in the middle
|
||||
// of a text segment. Adjust the range to include the rest of any partial text segments. If
|
||||
// start is already the start boundary of a text segment, then this is a no-op.
|
||||
start = segmentIterator.previousStartBoundary(start + 1);
|
||||
end = segmentIterator.nextEndBoundary(end - 1);
|
||||
|
||||
return new int[] {start, end};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the start character offset of the first text segment inside a horizontal interval
|
||||
* within a line, or the end character offset of the last text segment inside the horizontal
|
||||
* interval.
|
||||
*
|
||||
* @param line index of the line to search
|
||||
* @param left left bound of the horizontal interval
|
||||
* @param right right bound of the horizontal interval
|
||||
* @param segmentIterator iterator for determining the ranges of text to be considered as a text
|
||||
* segment
|
||||
* @param getStart true to find the start of the first text segment inside the horizontal
|
||||
* interval, false to find the end of the last text segment
|
||||
* @return the start character offset of the first text segment inside the horizontal interval,
|
||||
* or the end character offset of the last text segment inside the horizontal interval.
|
||||
*/
|
||||
private int getStartOrEndOffsetForHorizontalInterval(
|
||||
@IntRange(from = 0) int line, float left, float right,
|
||||
@NonNull SegmentIterator segmentIterator, boolean getStart) {
|
||||
int lineStartOffset = getLineStart(line);
|
||||
int lineEndOffset = getLineEnd(line);
|
||||
if (lineStartOffset == lineEndOffset) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
float[] horizontalBounds = new float[2 * (lineEndOffset - lineStartOffset)];
|
||||
fillHorizontalBoundsForLine(line, horizontalBounds);
|
||||
|
||||
int lineStartPos = getLineStartPos(line, getParagraphLeft(line), getParagraphRight(line));
|
||||
|
||||
// Loop through the runs forwards or backwards depending on getStart value.
|
||||
Layout.Directions directions = getLineDirections(line);
|
||||
int runIndex = getStart ? 0 : directions.getRunCount() - 1;
|
||||
while ((getStart && runIndex < directions.getRunCount()) || (!getStart && runIndex >= 0)) {
|
||||
// runStartOffset and runEndOffset are offset indices within the line.
|
||||
int runStartOffset = directions.getRunStart(runIndex);
|
||||
int runEndOffset = Math.min(
|
||||
runStartOffset + directions.getRunLength(runIndex),
|
||||
lineEndOffset - lineStartOffset);
|
||||
boolean isRtl = directions.isRunRtl(runIndex);
|
||||
float runLeft = lineStartPos
|
||||
+ (isRtl
|
||||
? horizontalBounds[2 * (runEndOffset - 1)]
|
||||
: horizontalBounds[2 * runStartOffset]);
|
||||
float runRight = lineStartPos
|
||||
+ (isRtl
|
||||
? horizontalBounds[2 * runStartOffset + 1]
|
||||
: horizontalBounds[2 * (runEndOffset - 1) + 1]);
|
||||
|
||||
int result =
|
||||
getStart
|
||||
? getStartOffsetForHorizontalIntervalWithinRun(
|
||||
left, right, lineStartOffset, lineStartPos, horizontalBounds,
|
||||
runStartOffset, runEndOffset, runLeft, runRight, isRtl,
|
||||
segmentIterator)
|
||||
: getEndOffsetForHorizontalIntervalWithinRun(
|
||||
left, right, lineStartOffset, lineStartPos, horizontalBounds,
|
||||
runStartOffset, runEndOffset, runLeft, runRight, isRtl,
|
||||
segmentIterator);
|
||||
if (result >= 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
runIndex += getStart ? 1 : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the start character offset of the first text segment inside a horizontal interval
|
||||
* within a directional run.
|
||||
*
|
||||
* @param left left bound of the horizontal interval
|
||||
* @param right right bound of the horizontal interval
|
||||
* @param lineStartOffset start character offset of the line containing this run
|
||||
* @param lineStartPos start position of the line containing this run
|
||||
* @param horizontalBounds array containing the signed horizontal bounds of the characters in
|
||||
* the line. The left and right bounds of the character at offset i are stored at index (2 *
|
||||
* i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}.
|
||||
* @param runStartOffset start offset of the run relative to {@code lineStartOffset}
|
||||
* @param runEndOffset end offset of the run relative to {@code lineStartOffset}
|
||||
* @param runLeft left bound of the run
|
||||
* @param runRight right bound of the run
|
||||
* @param isRtl whether the run is right-to-left
|
||||
* @param segmentIterator iterator for determining the ranges of text to be considered as a text
|
||||
* segment
|
||||
* @return the start character offset of the first text segment inside the horizontal interval
|
||||
*/
|
||||
private static int getStartOffsetForHorizontalIntervalWithinRun(
|
||||
float left, float right,
|
||||
@IntRange(from = 0) int lineStartOffset,
|
||||
@IntRange(from = 0) int lineStartPos,
|
||||
@NonNull float[] horizontalBounds,
|
||||
@IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset,
|
||||
float runLeft, float runRight,
|
||||
boolean isRtl,
|
||||
@NonNull SegmentIterator segmentIterator) {
|
||||
if (runRight < left || runLeft > right) {
|
||||
// The run does not overlap the interval.
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Find the first character in the run whose bounds overlap with the interval.
|
||||
// firstCharOffset is an offset index within the line.
|
||||
int firstCharOffset;
|
||||
if ((!isRtl && left <= runLeft) || (isRtl && right >= runRight)) {
|
||||
firstCharOffset = runStartOffset;
|
||||
} else {
|
||||
int low = runStartOffset;
|
||||
int high = runEndOffset;
|
||||
int guess;
|
||||
while (high - low > 1) {
|
||||
guess = (high + low) / 2;
|
||||
// Left edge of the character at guess
|
||||
float pos = lineStartPos + horizontalBounds[2 * guess];
|
||||
if ((!isRtl && pos > left) || (isRtl && pos < right)) {
|
||||
high = guess;
|
||||
} else {
|
||||
low = guess;
|
||||
}
|
||||
}
|
||||
// The interval bound is between the left edge of the character at low and the left edge
|
||||
// of the character at high. For LTR text, this is within the character at low. For RTL
|
||||
// text, this is within the character at high.
|
||||
firstCharOffset = isRtl ? high : low;
|
||||
}
|
||||
|
||||
// Find the first text segment containing this character (or, if no text segment contains
|
||||
// this character, the first text segment after this character). All previous text segments
|
||||
// in this run are to the left (for LTR) of the interval.
|
||||
segmentIterator.setRunLimits(
|
||||
lineStartOffset + runStartOffset, lineStartOffset + runEndOffset);
|
||||
int segmentEndOffset =
|
||||
segmentIterator.nextEndBoundaryOrRunEnd(lineStartOffset + firstCharOffset);
|
||||
if (segmentEndOffset == SegmentIterator.DONE) {
|
||||
// There are no text segments containing or after firstCharOffset, so no text segments
|
||||
// in this run overlap the interval.
|
||||
return -1;
|
||||
}
|
||||
int segmentStartOffset = segmentIterator.previousStartBoundaryOrRunStart(segmentEndOffset);
|
||||
float segmentCenter = lineStartPos
|
||||
+ (horizontalBounds[2 * (segmentStartOffset - lineStartOffset)]
|
||||
+ horizontalBounds[2 * (segmentEndOffset - lineStartOffset) - 1]) / 2;
|
||||
if ((!isRtl && segmentCenter > right) || (isRtl && segmentCenter < left)) {
|
||||
// The entire interval is to the left (for LTR) of the text segment's center. So the
|
||||
// interval does not contain any text segments within this run.
|
||||
return -1;
|
||||
}
|
||||
if ((!isRtl && segmentCenter >= left) || (isRtl && segmentCenter <= right)) {
|
||||
// The center is within the interval, so return the start offset of this text segment.
|
||||
return segmentStartOffset;
|
||||
}
|
||||
|
||||
// If first text segment's center is not within the interval, try the next text segment.
|
||||
segmentStartOffset = segmentIterator.nextStartBoundaryWithinRunLimits(segmentStartOffset);
|
||||
if (segmentStartOffset == SegmentIterator.DONE
|
||||
|| segmentStartOffset == lineStartOffset + runEndOffset) {
|
||||
// No more text segments within this run.
|
||||
return -1;
|
||||
}
|
||||
segmentEndOffset = segmentIterator.nextEndBoundaryOrRunEnd(segmentStartOffset);
|
||||
segmentCenter = lineStartPos
|
||||
+ (horizontalBounds[2 * (segmentStartOffset - lineStartOffset)]
|
||||
+ horizontalBounds[2 * (segmentEndOffset - lineStartOffset) - 1]) / 2;
|
||||
// We already know that segmentCenter >= left (for LTR) since the previous word contains the
|
||||
// left point.
|
||||
if ((!isRtl && segmentCenter <= right) || (isRtl && segmentCenter >= left)) {
|
||||
return segmentStartOffset;
|
||||
}
|
||||
|
||||
// If the second text segment is also not within the interval, then this means that the
|
||||
// interval is between the centers of the first and second text segments, so it does not
|
||||
// contain any text segments on this line.
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the end character offset of the last text segment inside a horizontal interval within a
|
||||
* directional run.
|
||||
*
|
||||
* @param left left bound of the horizontal interval
|
||||
* @param right right bound of the horizontal interval
|
||||
* @param lineStartOffset start character offset of the line containing this run
|
||||
* @param lineStartPos start position of the line containing this run
|
||||
* @param horizontalBounds array containing the signed horizontal bounds of the characters in
|
||||
* the line. The left and right bounds of the character at offset i are stored at index (2 *
|
||||
* i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}.
|
||||
* @param runStartOffset start offset of the run relative to {@code lineStartOffset}
|
||||
* @param runEndOffset end offset of the run relative to {@code lineStartOffset}
|
||||
* @param runLeft left bound of the run
|
||||
* @param runRight right bound of the run
|
||||
* @param isRtl whether the run is right-to-left
|
||||
* @param segmentIterator iterator for determining the ranges of text to be considered as a text
|
||||
* segment
|
||||
* @return the end character offset of the last text segment inside the horizontal interval
|
||||
*/
|
||||
private static int getEndOffsetForHorizontalIntervalWithinRun(
|
||||
float left, float right,
|
||||
@IntRange(from = 0) int lineStartOffset,
|
||||
@IntRange(from = 0) int lineStartPos,
|
||||
@NonNull float[] horizontalBounds,
|
||||
@IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset,
|
||||
float runLeft, float runRight,
|
||||
boolean isRtl,
|
||||
@NonNull SegmentIterator segmentIterator) {
|
||||
if (runRight < left || runLeft > right) {
|
||||
// The run does not overlap the interval.
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Find the last character in the run whose bounds overlap with the interval.
|
||||
// firstCharOffset is an offset index within the line.
|
||||
int lastCharOffset;
|
||||
if ((!isRtl && right >= runRight) || (isRtl && left <= runLeft)) {
|
||||
lastCharOffset = runEndOffset - 1;
|
||||
} else {
|
||||
int low = runStartOffset;
|
||||
int high = runEndOffset;
|
||||
int guess;
|
||||
while (high - low > 1) {
|
||||
guess = (high + low) / 2;
|
||||
// Left edge of the character at guess
|
||||
float pos = lineStartPos + horizontalBounds[2 * guess];
|
||||
if ((!isRtl && pos > right) || (isRtl && pos < left)) {
|
||||
high = guess;
|
||||
} else {
|
||||
low = guess;
|
||||
}
|
||||
}
|
||||
// The interval bound is between the left edge of the character at low and the left edge
|
||||
// of the character at high. For LTR text, this is within the character at low. For RTL
|
||||
// text, this is within the character at high.
|
||||
lastCharOffset = isRtl ? high : low;
|
||||
}
|
||||
|
||||
// Find the last text segment containing this character (or, if no text segment
|
||||
// contains this character, the first text segment before this character). All
|
||||
// following text segments in this run are to the right (for LTR) of the interval.
|
||||
segmentIterator.setRunLimits(
|
||||
lineStartOffset + runStartOffset, lineStartOffset + runEndOffset);
|
||||
// + 1 to allow segmentStartOffset = lineStartOffset + lastCharOffset
|
||||
int segmentStartOffset =
|
||||
segmentIterator.previousStartBoundaryOrRunStart(
|
||||
lineStartOffset + lastCharOffset + 1);
|
||||
if (segmentStartOffset == SegmentIterator.DONE) {
|
||||
// There are no text segments containing or before lastCharOffset, so no text segments
|
||||
// in this run overlap the interval.
|
||||
return -1;
|
||||
}
|
||||
int segmentEndOffset = segmentIterator.nextEndBoundaryOrRunEnd(segmentStartOffset);
|
||||
float segmentCenter = lineStartPos
|
||||
+ (horizontalBounds[2 * (segmentStartOffset - lineStartOffset)]
|
||||
+ horizontalBounds[2 * (segmentEndOffset - lineStartOffset) - 1]) / 2;
|
||||
if ((!isRtl && segmentCenter < left) || (isRtl && segmentCenter > right)) {
|
||||
// The entire interval is to the right (for LTR) of the text segment's center. So the
|
||||
// interval does not contain any text segments within this run.
|
||||
return -1;
|
||||
}
|
||||
if ((!isRtl && segmentCenter <= right) || (isRtl && segmentCenter >= left)) {
|
||||
// The center is within the interval, so return the end offset of this text segment.
|
||||
return segmentEndOffset;
|
||||
}
|
||||
|
||||
// If first text segment's center is not within the interval, try the next text segment.
|
||||
segmentEndOffset = segmentIterator.previousEndBoundaryWithinRunLimits(segmentEndOffset);
|
||||
if (segmentEndOffset == SegmentIterator.DONE
|
||||
|| segmentEndOffset == lineStartOffset + runStartOffset) {
|
||||
// No more text segments within this run.
|
||||
return -1;
|
||||
}
|
||||
segmentStartOffset = segmentIterator.previousStartBoundaryOrRunStart(segmentEndOffset);
|
||||
segmentCenter = lineStartPos
|
||||
+ (horizontalBounds[2 * (segmentStartOffset - lineStartOffset)]
|
||||
+ horizontalBounds[2 * (segmentEndOffset - lineStartOffset) - 1]) / 2;
|
||||
// We already know that segmentCenter <= right (for LTR) since the following word
|
||||
// contains the right point.
|
||||
if ((!isRtl && segmentCenter >= left) || (isRtl && segmentCenter <= right)) {
|
||||
return segmentEndOffset;
|
||||
}
|
||||
|
||||
// If the second text segment is also not within the interval, then this means that the
|
||||
// interval is between the centers of the first and second text segments, so it does not
|
||||
// contain any text segments on this line.
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the text offset after the last character on the specified line.
|
||||
*/
|
||||
|
||||
111
core/java/android/text/SegmentIterator.java
Normal file
111
core/java/android/text/SegmentIterator.java
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
import android.annotation.IntRange;
|
||||
|
||||
/**
|
||||
* Finds text segment boundaries within text. Subclasses can implement different types of text
|
||||
* segments. Grapheme clusters and words are examples of possible text segments.
|
||||
*
|
||||
* <p>Granular units may not overlap, so every character belongs to at most one text segment. A
|
||||
* character may belong to no text segments.
|
||||
*
|
||||
* <p>For example, a word level text segment iterator may subdivide the text "Hello, World!" into
|
||||
* four text segments: "Hello", ",", "World", "!". The space character does not belong to any text
|
||||
* segments.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public abstract class SegmentIterator {
|
||||
public static final int DONE = -1;
|
||||
|
||||
private int mRunStartOffset;
|
||||
private int mRunEndOffset;
|
||||
|
||||
/**
|
||||
* Returns the character offset of the previous text segment start boundary before the specified
|
||||
* character offset, or {@code DONE} if there are none.
|
||||
*/
|
||||
public abstract int previousStartBoundary(@IntRange(from = 0) int offset);
|
||||
|
||||
/**
|
||||
* Returns the character offset of the previous text segment end boundary before the specified
|
||||
* character offset, or {@code DONE} if there are none.
|
||||
*/
|
||||
public abstract int previousEndBoundary(@IntRange(from = 0) int offset);
|
||||
|
||||
/**
|
||||
* Returns the character offset of the next text segment start boundary after the specified
|
||||
* character offset, or {@code DONE} if there are none.
|
||||
*/
|
||||
public abstract int nextStartBoundary(@IntRange(from = 0) int offset);
|
||||
|
||||
/**
|
||||
* Returns the character offset of the next text segment end boundary after the specified
|
||||
* character offset, or {@code DONE} if there are none.
|
||||
*/
|
||||
public abstract int nextEndBoundary(@IntRange(from = 0) int offset);
|
||||
|
||||
/**
|
||||
* Sets the start and end of a run which can be used to constrain the scope of the iterator's
|
||||
* search.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
void setRunLimits(
|
||||
@IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset) {
|
||||
mRunStartOffset = runStartOffset;
|
||||
mRunEndOffset = runEndOffset;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
int previousStartBoundaryOrRunStart(@IntRange(from = 0) int offset) {
|
||||
int start = previousStartBoundary(offset);
|
||||
if (start == DONE) {
|
||||
return DONE;
|
||||
}
|
||||
return Math.max(start, mRunStartOffset);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
int previousEndBoundaryWithinRunLimits(@IntRange(from = 0) int offset) {
|
||||
int end = previousEndBoundary(offset);
|
||||
if (end <= mRunStartOffset) {
|
||||
return DONE;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
int nextStartBoundaryWithinRunLimits(@IntRange(from = 0) int offset) {
|
||||
int start = nextStartBoundary(offset);
|
||||
if (start >= mRunEndOffset) {
|
||||
return DONE;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
int nextEndBoundaryOrRunEnd(@IntRange(from = 0) int offset) {
|
||||
int end = nextEndBoundary(offset);
|
||||
if (end == DONE) {
|
||||
return DONE;
|
||||
}
|
||||
return Math.min(end, mRunEndOffset);
|
||||
}
|
||||
}
|
||||
87
core/java/android/text/WordSegmentIterator.java
Normal file
87
core/java/android/text/WordSegmentIterator.java
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.icu.text.BreakIterator;
|
||||
import android.text.method.WordIterator;
|
||||
|
||||
/**
|
||||
* Implementation of {@code SegmentIterator} using words as the text segment. Word boundaries are
|
||||
* found using {@code WordIterator}. Whitespace characters are excluded, so they are not included in
|
||||
* any text segments.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class WordSegmentIterator extends SegmentIterator {
|
||||
private final CharSequence mText;
|
||||
private final WordIterator mWordIterator;
|
||||
|
||||
public WordSegmentIterator(@NonNull CharSequence text, @NonNull WordIterator wordIterator) {
|
||||
mText = text;
|
||||
mWordIterator = wordIterator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int previousStartBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = offset;
|
||||
do {
|
||||
boundary = mWordIterator.prevBoundary(boundary);
|
||||
if (boundary == BreakIterator.DONE) {
|
||||
return DONE;
|
||||
}
|
||||
} while (Character.isWhitespace(mText.charAt(boundary)));
|
||||
return boundary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int previousEndBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = offset;
|
||||
do {
|
||||
boundary = mWordIterator.prevBoundary(boundary);
|
||||
if (boundary == BreakIterator.DONE || boundary == 0) {
|
||||
return DONE;
|
||||
}
|
||||
} while (Character.isWhitespace(mText.charAt(boundary - 1)));
|
||||
return boundary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextStartBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = offset;
|
||||
do {
|
||||
boundary = mWordIterator.nextBoundary(boundary);
|
||||
if (boundary == BreakIterator.DONE || boundary == mText.length()) {
|
||||
return DONE;
|
||||
}
|
||||
} while (Character.isWhitespace(mText.charAt(boundary)));
|
||||
return boundary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextEndBoundary(@IntRange(from = 0) int offset) {
|
||||
int boundary = offset;
|
||||
do {
|
||||
boundary = mWordIterator.nextBoundary(boundary);
|
||||
if (boundary == BreakIterator.DONE) {
|
||||
return DONE;
|
||||
}
|
||||
} while (Character.isWhitespace(mText.charAt(boundary - 1)));
|
||||
return boundary;
|
||||
}
|
||||
}
|
||||
@@ -98,12 +98,14 @@ import android.text.BoringLayout;
|
||||
import android.text.DynamicLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.GetChars;
|
||||
import android.text.GraphemeClusterSegmentIterator;
|
||||
import android.text.GraphicsOperations;
|
||||
import android.text.InputFilter;
|
||||
import android.text.InputType;
|
||||
import android.text.Layout;
|
||||
import android.text.ParcelableSpan;
|
||||
import android.text.PrecomputedText;
|
||||
import android.text.SegmentIterator;
|
||||
import android.text.Selection;
|
||||
import android.text.SpanWatcher;
|
||||
import android.text.Spannable;
|
||||
@@ -117,6 +119,7 @@ import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextUtils.TruncateAt;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.WordSegmentIterator;
|
||||
import android.text.method.AllCapsTransformationMethod;
|
||||
import android.text.method.ArrowKeyMovementMethod;
|
||||
import android.text.method.DateKeyListener;
|
||||
@@ -183,11 +186,14 @@ import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.CompletionInfo;
|
||||
import android.view.inputmethod.CorrectionInfo;
|
||||
import android.view.inputmethod.CursorAnchorInfo;
|
||||
import android.view.inputmethod.DeleteGesture;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.ExtractedText;
|
||||
import android.view.inputmethod.ExtractedTextRequest;
|
||||
import android.view.inputmethod.HandwritingGesture;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.inputmethod.SelectGesture;
|
||||
import android.view.inspector.InspectableProperty;
|
||||
import android.view.inspector.InspectableProperty.EnumEntry;
|
||||
import android.view.inspector.InspectableProperty.FlagEntry;
|
||||
@@ -9282,6 +9288,58 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public int performHandwritingSelectGesture(@NonNull SelectGesture gesture) {
|
||||
int[] range = getRangeForRect(gesture.getSelectionArea(), gesture.getGranularity());
|
||||
if (range == null) {
|
||||
return handleGestureFailure(gesture);
|
||||
}
|
||||
Selection.setSelection(getEditableText(), range[0], range[1]);
|
||||
mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) {
|
||||
int[] range = getRangeForRect(gesture.getDeletionArea(), gesture.getGranularity());
|
||||
if (range == null) {
|
||||
return handleGestureFailure(gesture);
|
||||
}
|
||||
getEditableText().delete(range[0], range[1]);
|
||||
Selection.setSelection(getEditableText(), range[0]);
|
||||
// TODO: Delete extra spaces.
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int handleGestureFailure(HandwritingGesture gesture) {
|
||||
if (!TextUtils.isEmpty(gesture.getFallbackText())) {
|
||||
getEditableText()
|
||||
.replace(getSelectionStart(), getSelectionEnd(), gesture.getFallbackText());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private int[] getRangeForRect(@NonNull RectF area, int granularity) {
|
||||
// The coordinates provided are screen coordinates - transform to content coordinates.
|
||||
int[] screenToViewport = getLocationOnScreen();
|
||||
area = new RectF(area);
|
||||
area.offset(
|
||||
-(screenToViewport[0] + viewportToContentHorizontalOffset()),
|
||||
-(screenToViewport[1] + viewportToContentVerticalOffset()));
|
||||
|
||||
SegmentIterator segmentIterator;
|
||||
if (granularity == HandwritingGesture.GRANULARITY_WORD) {
|
||||
WordIterator wordIterator = getWordIterator();
|
||||
wordIterator.setCharSequence(mText, 0, mText.length());
|
||||
segmentIterator = new WordSegmentIterator(mText, wordIterator);
|
||||
} else {
|
||||
segmentIterator = new GraphemeClusterSegmentIterator(mText, mTextPaint);
|
||||
}
|
||||
|
||||
return mLayout.getRangeForRect(area, segmentIterator);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@VisibleForTesting
|
||||
@UnsupportedAppUsage
|
||||
|
||||
@@ -22,6 +22,9 @@ import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT;
|
||||
import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT_END;
|
||||
import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT_START;
|
||||
|
||||
import android.annotation.CallbackExecutor;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Selection;
|
||||
@@ -31,12 +34,18 @@ import android.util.proto.ProtoOutputStream;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.CompletionInfo;
|
||||
import android.view.inputmethod.CorrectionInfo;
|
||||
import android.view.inputmethod.DeleteGesture;
|
||||
import android.view.inputmethod.DumpableInputConnection;
|
||||
import android.view.inputmethod.ExtractedText;
|
||||
import android.view.inputmethod.ExtractedTextRequest;
|
||||
import android.view.inputmethod.HandwritingGesture;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.SelectGesture;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
/**
|
||||
* Base class for an editable InputConnection instance. This is created by {@link TextView} or
|
||||
* {@link android.widget.EditText}.
|
||||
@@ -256,6 +265,23 @@ public final class EditableInputConnection extends BaseInputConnection
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performHandwritingGesture(
|
||||
@NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
|
||||
@Nullable IntConsumer consumer) {
|
||||
int result;
|
||||
if (gesture instanceof SelectGesture) {
|
||||
result = mTextView.performHandwritingSelectGesture((SelectGesture) gesture);
|
||||
} else if (gesture instanceof DeleteGesture) {
|
||||
result = mTextView.performHandwritingDeleteGesture((DeleteGesture) gesture);
|
||||
} else {
|
||||
result = 0;
|
||||
}
|
||||
if (executor != null && consumer != null) {
|
||||
executor.execute(() -> consumer.accept(result));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
|
||||
final long token = proto.start(fieldId);
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.method.WordIterator;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class LayoutGetRangeForRectTest {
|
||||
|
||||
private static final int WIDTH = 200;
|
||||
private static final int HEIGHT = 1000;
|
||||
private static final String DEFAULT_TEXT = ""
|
||||
// Line 0 (offset 0 to 18)
|
||||
// - Word 0 (offset 0 to 4) has bounds [0, 40], center 20
|
||||
// - Word 1 (offset 5 to 11) has bounds [50, 110], center 80
|
||||
// - Word 2 (offset 12 to 17) has bounds [120, 170], center 145
|
||||
+ "XXXX XXXXXX XXXXX "
|
||||
// Line 1 (offset 18 to 36)
|
||||
// - Word 3 (offset 18 to 23) has bounds [0, 50], center 25
|
||||
// - Word 4 (offset 24 to 26, RTL) has bounds [100, 110], center 105
|
||||
// - Word 5 (offset 27 to 29, RTL) has bounds [80, 90], center 85
|
||||
// - Word 6 start part (offset 30 to 32, RTL) has bounds [60, 70], center 65
|
||||
// - Word 6 end part (offset 32 to 35) has bounds [110, 140], center 125
|
||||
+ "XXXXX \u05D1\u05D1 \u05D1\u05D1 \u05D1\u05D1XXX\n"
|
||||
// Line 2 (offset 36 to 38)
|
||||
// - Word 7 start part (offset 36 to 38) has bounds [0, 150], center 75
|
||||
// Line 3 (offset 38 to 40)
|
||||
// - Word 7 middle part (offset 38 to 40) has bounds [0, 150], center 75
|
||||
// Line 4 (offset 40 to 46)
|
||||
// - Word 7 end part (offset 40 to 41) has bounds [0, 100], center 50
|
||||
// - Word 8 (offset 42 to 44) has bounds [110, 130], center 120
|
||||
+ "CLCLC XX \n";
|
||||
|
||||
private Layout mLayout;
|
||||
private float[] mLineCenters;
|
||||
private GraphemeClusterSegmentIterator mGraphemeClusterSegmentIterator;
|
||||
private WordSegmentIterator mWordSegmentIterator;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
// The test font includes the following characters:
|
||||
// U+0020 ( ): 10em
|
||||
// U+002E (.): 10em
|
||||
// U+0049 (I): 1em
|
||||
// U+0056 (V): 5em
|
||||
// U+0058 (X): 10em
|
||||
// U+004C (L): 50em
|
||||
// U+0043 (C): 100em
|
||||
// U+005F (_): 0em
|
||||
// U+05D0 : 1em // HEBREW LETTER ALEF
|
||||
// U+05D1 : 5em // HEBREW LETTER BET
|
||||
// U+FFFD (invalid surrogate will be replaced to this): 7em
|
||||
// U+10331 (\uD800\uDF31): 10em
|
||||
// Undefined : 0.5em
|
||||
TextPaint textPaint = new TextPaint();
|
||||
textPaint.setTypeface(
|
||||
Typeface.createFromAsset(
|
||||
InstrumentationRegistry.getInstrumentation().getTargetContext().getAssets(),
|
||||
"fonts/StaticLayoutLineBreakingTestFont.ttf"));
|
||||
// Make 1 em equal to 1 pixel.
|
||||
textPaint.setTextSize(1.0f);
|
||||
|
||||
mLayout = StaticLayout.Builder.obtain(
|
||||
DEFAULT_TEXT, 0, DEFAULT_TEXT.length(), textPaint, WIDTH).build();
|
||||
|
||||
mLineCenters = new float[mLayout.getLineCount()];
|
||||
for (int i = 0; i < mLayout.getLineCount(); ++i) {
|
||||
mLineCenters[i] = (mLayout.getLineTop(i) + mLayout.getLineBottomWithoutSpacing(i)) / 2f;
|
||||
}
|
||||
|
||||
mGraphemeClusterSegmentIterator =
|
||||
new GraphemeClusterSegmentIterator(DEFAULT_TEXT, textPaint);
|
||||
WordIterator wordIterator = new WordIterator();
|
||||
wordIterator.setCharSequence(DEFAULT_TEXT, 0, DEFAULT_TEXT.length());
|
||||
mWordSegmentIterator = new WordSegmentIterator(DEFAULT_TEXT, wordIterator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character() {
|
||||
// Character 1 on line 0 has center 15.
|
||||
// Character 2 on line 0 has center 25.
|
||||
RectF area = new RectF(14f, mLineCenters[0] - 1f, 26f, mLineCenters[0] + 1f);
|
||||
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(1, 3).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_partialCharacterButNotCenter() {
|
||||
// Character 0 on line 0 has center 5.
|
||||
// Character 1 on line 0 has center 15.
|
||||
// Character 2 on line 0 has center 25.
|
||||
// Character 3 on line 0 has center 35.
|
||||
RectF area = new RectF(6f, mLineCenters[0] - 1f, 34f, mLineCenters[0] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
// Area partially overlaps characters 0 and 3 but does not contain their centers.
|
||||
assertThat(range).asList().containsExactly(1, 3).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_rtl() {
|
||||
// Character 25 on line 1 has center 102.5.
|
||||
// Character 26 on line 1 has center 95.
|
||||
RectF area = new RectF(94f, mLineCenters[1] - 1f, 103f, mLineCenters[1] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(25, 27).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_ltrAndRtl() {
|
||||
// Character 22 on line 1 has center 45.
|
||||
// The end of the RTL run (offset 24 to 32) on line 1 is at 60.
|
||||
RectF area = new RectF(44f, mLineCenters[1] - 1f, 93f, mLineCenters[1] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(22, 32).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_rtlAndLtr() {
|
||||
// The start of the RTL run (offset 24 to 32) on line 1 is at 110.
|
||||
// Character 33 on line 1 has center 125.
|
||||
RectF area = new RectF(93f, mLineCenters[1] - 1f, 131f, mLineCenters[1] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(24, 34).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_betweenCharacters_shouldFallback() {
|
||||
// Character 1 on line 0 has center 15.
|
||||
// Character 2 on line 0 has center 25.
|
||||
RectF area = new RectF(16f, mLineCenters[0] - 1f, 24f, mLineCenters[0] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
assertThat(range).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_betweenLines_shouldFallback() {
|
||||
// Area top is below the center of line 0.
|
||||
// Area bottom is above the center of line 1.
|
||||
RectF area = new RectF(0f, mLineCenters[0] + 1f, WIDTH, mLineCenters[1] - 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
// Area partially covers two lines but does not contain the center of any characters.
|
||||
assertThat(range).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_multiLine() {
|
||||
// Character 9 on line 0 has center 95.
|
||||
// Character 42 on line 4 has center 115.
|
||||
RectF area = new RectF(93f, 0, 118f, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(9, 43).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_multiLine_betweenCharactersOnSomeLines() {
|
||||
// Character 6 on line 0 has center 65.
|
||||
// Character 7 on line 0 has center 75.
|
||||
// Character 30 on line 1 has center 67.5.
|
||||
// Character 36 on line 2 has center 50.
|
||||
// Character 37 on line 2 has center 125.
|
||||
// Character 38 on line 3 has center 50.
|
||||
// Character 39 on line 3 has center 125.
|
||||
// Character 40 on line 4 has center 50.
|
||||
// Character 41 on line 4 has center 105.
|
||||
RectF area = new RectF(66f, 0, 69f, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
// Area crosses all lines but does not contain the center of any characters on lines 0, 2,
|
||||
// 3, or 4. So the only included character is character 30 on line 1.
|
||||
assertThat(range).asList().containsExactly(30, 31).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_multiLine_betweenCharactersOnAllLines_shouldFallback() {
|
||||
// Character 6 on line 0 has center 65.
|
||||
// Character 7 on line 0 has center 75.
|
||||
// Character 30 on line 1 has center 67.5.
|
||||
// Character 31 on line 1 has center 62.5.
|
||||
// Character 36 on line 2 has center 50.
|
||||
// Character 37 on line 2 has center 125.
|
||||
// Character 38 on line 3 has center 50.
|
||||
// Character 39 on line 3 has center 125.
|
||||
// Character 40 on line 4 has center 50.
|
||||
// Character 41 on line 4 has center 105.
|
||||
RectF area = new RectF(66f, 0, 67f, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
// Area crosses all lines but does not contain the center of any characters.
|
||||
assertThat(range).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_character_all() {
|
||||
// Entire area, should include all text.
|
||||
RectF area = new RectF(0f, 0f, WIDTH, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mGraphemeClusterSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(0, DEFAULT_TEXT.length()).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word() {
|
||||
// Word 1 (offset 5 to 11) on line 0 has center 80.
|
||||
RectF area = new RectF(79f, mLineCenters[0] - 1f, 81f, mLineCenters[0] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(5, 11).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_partialWordButNotCenter() {
|
||||
// Word 0 (offset 0 to 4) on line 0 has center 20.
|
||||
// Word 1 (offset 5 to 11) on line 0 has center 80.
|
||||
// Word 2 (offset 12 to 17) on line 0 center 145
|
||||
RectF area = new RectF(21f, mLineCenters[0] - 1f, 144f, mLineCenters[0] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Area partially overlaps words 0 and 2 but does not contain their centers, so only word 1
|
||||
// is included. Whitespace between words is not included.
|
||||
assertThat(range).asList().containsExactly(5, 11).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_rtl() {
|
||||
// Word 4 (offset 24 to 26, RTL) on line 1 center 105
|
||||
RectF area = new RectF(88f, mLineCenters[1] - 1f, 119f, mLineCenters[1] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(24, 26).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_ltrAndRtl() {
|
||||
// Word 3 (offset 18 to 23) on line 1 has center 25
|
||||
// The end of the RTL run (offset 24 to 32) on line 1 is at 60.
|
||||
RectF area = new RectF(24f, mLineCenters[1] - 1f, 93f, mLineCenters[1] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Selects all of word 6, not just the first RTL part.
|
||||
assertThat(range).asList().containsExactly(18, 35).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_rtlAndLtr() {
|
||||
// The start of the RTL run (offset 24 to 32) on line 1 is at 110.
|
||||
// End part of word 6 (offset 32 to 35) on line 1 has center 125.
|
||||
RectF area = new RectF(93f, mLineCenters[1] - 1f, 174f, mLineCenters[1] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(24, 35).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_betweenWords_shouldFallback() {
|
||||
// Word 1 on line 0 has center 80.
|
||||
// Word 2 on line 0 has center 145.
|
||||
RectF area = new RectF(81f, mLineCenters[0] - 1f, 144f, mLineCenters[0] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
assertThat(range).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_betweenLines_shouldFallback() {
|
||||
// Area top is below the center of line 0.
|
||||
// Area bottom is above the center of line 1.
|
||||
RectF area = new RectF(0f, mLineCenters[0] + 1f, WIDTH, mLineCenters[1] - 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Area partially covers two lines but does not contain the center of any words.
|
||||
assertThat(range).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_multiLine() {
|
||||
// Word 1 (offset 5 to 11) on line 0 has center 80.
|
||||
// End part of word 7 (offset 40 to 41) on line 4 has center 50.
|
||||
RectF area = new RectF(42f, 0, 91f, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(5, 41).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_multiLine_betweenWordsOnSomeLines() {
|
||||
// Word 1 on line 0 has center 80.
|
||||
// Word 2 on line 0 has center 145.
|
||||
// Word 5 (offset 27 to 29) on line 1 has center 85.
|
||||
// Word 7 on line 2 has center 50.
|
||||
// Word 37 on line 2 has center 125.
|
||||
// Word 38 on line 3 has center 50.
|
||||
// Word 39 on line 3 has center 125.
|
||||
// Word 40 on line 4 has center 50.
|
||||
// Word 41 on line 4 has center 105.
|
||||
RectF area = new RectF(84f, 0, 86f, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Area crosses all lines but does not contain the center of any words on lines 0, 2, 3, or
|
||||
// 4. So the only included word is word 5 on line 1.
|
||||
assertThat(range).asList().containsExactly(27, 29).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_multiLine_betweenCharactersOnAllLines_shouldFallback() {
|
||||
// Word 1 on line 0 has center 80.
|
||||
// Word 2 on line 0 has center 145.
|
||||
// Word 4 on line 1 has center 105.
|
||||
// Word 5 on line 1 has center 85.
|
||||
// Word 7 on line 2 has center 50.
|
||||
// Word 37 on line 2 has center 125.
|
||||
// Word 38 on line 3 has center 50.
|
||||
// Word 39 on line 3 has center 125.
|
||||
// Word 40 on line 4 has center 50.
|
||||
// Word 41 on line 4 has center 105.
|
||||
RectF area = new RectF(86f, 0, 89f, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Area crosses all lines but does not contain the center of any words.
|
||||
assertThat(range).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_wordSpansMultipleLines_firstPartInsideArea() {
|
||||
// Word 5 (offset 27 to 29) on line 1 has center 85.
|
||||
// First part of word 7 (offset 36 to 38) on line 2 has center 75.
|
||||
RectF area = new RectF(74f, mLineCenters[1] - 1f, 86f, mLineCenters[2] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Selects all of word 7, not just the first part on line 2.
|
||||
assertThat(range).asList().containsExactly(27, 41).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_wordSpansMultipleLines_middlePartInsideArea() {
|
||||
// Middle part of word 7 (offset 38 to 40) on line 2 has center 75.
|
||||
RectF area = new RectF(74f, mLineCenters[3] - 1f, 75f, mLineCenters[3] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Selects all of word 7, not just the middle part on line 3.
|
||||
assertThat(range).asList().containsExactly(36, 41).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_wordSpansMultipleLines_endPartInsideArea() {
|
||||
// End part of word 7 (offset 40 to 41) on line 4 has center 50.
|
||||
// Word 8 (offset 42 to 44) on line 4 has center 120
|
||||
RectF area = new RectF(49f, mLineCenters[4] - 1f, 121f, mLineCenters[4] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Selects all of word 7, not just the middle part on line 3.
|
||||
assertThat(range).asList().containsExactly(36, 44).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_wordSpansMultipleRuns_firstPartInsideArea() {
|
||||
// Word 5 (offset 27 to 29) on line 1 has center 85.
|
||||
// First part of word 6 (offset 30 to 32) on line 1 has center 65.
|
||||
RectF area = new RectF(64f, mLineCenters[1] - 1f, 86f, mLineCenters[1] + 1f);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
// Selects all of word 6, not just the first RTL part.
|
||||
assertThat(range).asList().containsExactly(27, 35).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRangeForRect_word_all() {
|
||||
// Entire area, should include all text except the last two whitespace characters.
|
||||
RectF area = new RectF(0f, 0f, WIDTH, HEIGHT);
|
||||
int[] range = mLayout.getRangeForRect(area, mWordSegmentIterator);
|
||||
|
||||
assertThat(range).asList().containsExactly(0, DEFAULT_TEXT.length() - 2).inOrder();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user