• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import android.app.Activity;
22 import android.app.Instrumentation;
23 import android.graphics.Matrix;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.graphics.Typeface;
27 import android.graphics.drawable.Drawable;
28 import android.graphics.drawable.ShapeDrawable;
29 import android.util.TypedValue;
30 import android.view.Gravity;
31 import android.view.View;
32 import android.view.inputmethod.CursorAnchorInfo;
33 import android.view.inputmethod.EditorBoundsInfo;
34 
35 import androidx.test.ext.junit.runners.AndroidJUnit4;
36 import androidx.test.platform.app.InstrumentationRegistry;
37 import androidx.test.rule.ActivityTestRule;
38 
39 import com.google.common.collect.ImmutableList;
40 
41 import org.junit.Before;
42 import org.junit.BeforeClass;
43 import org.junit.Rule;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 @RunWith(AndroidJUnit4.class)
51 public class EditTextCursorAnchorInfoTest {
52     private static final CursorAnchorInfo.Builder sCursorAnchorInfoBuilder =
53             new CursorAnchorInfo.Builder();
54     private static final Matrix sMatrix = new Matrix();
55     private static final int[] sLocationOnScreen = new int[2];
56     private static Typeface sTypeface;
57     private static final float TEXT_SIZE = 1f;
58     // The line height of the test font is 1.2 * textSize.
59     private static final int LINE_HEIGHT = 12;
60     private static final int HW_BOUNDS_OFFSET_LEFT = 10;
61     private static final int HW_BOUNDS_OFFSET_TOP = 20;
62     private static final int HW_BOUNDS_OFFSET_RIGHT = 30;
63     private static final int HW_BOUNDS_OFFSET_BOTTOM = 40;
64 
65 
66     // Default text has 5 lines of text. The needed width is 50px and the needed height is 60px.
67     private static final CharSequence DEFAULT_TEXT = "X\nXX\nXXX\nXXXX\nXXXXX";
68     private static final ImmutableList<RectF> DEFAULT_LINE_BOUNDS = ImmutableList.of(
69             new RectF(0f, 0f, 10f, LINE_HEIGHT),
70             new RectF(0f, LINE_HEIGHT, 20f, 2 * LINE_HEIGHT),
71             new RectF(0f, 2 * LINE_HEIGHT, 30f, 3 * LINE_HEIGHT),
72             new RectF(0f, 3 * LINE_HEIGHT, 40f, 4 * LINE_HEIGHT),
73             new RectF(0f, 4 * LINE_HEIGHT, 50f, 5 * LINE_HEIGHT));
74 
75     @Rule
76     public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>(
77             TextViewActivity.class);
78     private Activity mActivity;
79     private TextView mEditText;
80 
81     @BeforeClass
setupClass()82     public static void setupClass() {
83         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
84 
85         // The test font has following coverage and width.
86         // U+0020: 10em
87         // U+002E (.): 10em
88         // U+0043 (C): 100em
89         // U+0049 (I): 1em
90         // U+004C (L): 50em
91         // U+0056 (V): 5em
92         // U+0058 (X): 10em
93         // U+005F (_): 0em
94         // U+05D0    : 1em  // HEBREW LETTER ALEF
95         // U+05D1    : 5em  // HEBREW LETTER BET
96         // U+FFFD (invalid surrogate will be replaced to this): 7em
97         // U+10331 (\uD800\uDF31): 10em
98         // Undefined : 0.5em
99         sTypeface = Typeface.createFromAsset(instrumentation.getTargetContext().getAssets(),
100                 "fonts/StaticLayoutLineBreakingTestFont.ttf");
101     }
102 
103     @Before
setup()104     public void setup() {
105         mActivity = mActivityRule.getActivity();
106     }
107 
108     @Test
testMatrix()109     public void testMatrix() {
110         setupEditText("", /* height= */ 100);
111         CursorAnchorInfo cursorAnchorInfo =
112                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
113 
114         Matrix actualMatrix = cursorAnchorInfo.getMatrix();
115         Matrix expectedMatrix = new Matrix();
116         expectedMatrix.setTranslate(sLocationOnScreen[0], sLocationOnScreen[1]);
117 
118         assertThat(actualMatrix).isEqualTo(expectedMatrix);
119     }
120 
121     @Test
testMatrix_withTranslation()122     public void testMatrix_withTranslation() {
123         float translationX = 10f;
124         float translationY = 20f;
125         createEditText("");
126         mEditText.setTranslationX(translationX);
127         mEditText.setTranslationY(translationY);
128         measureEditText(100);
129 
130         CursorAnchorInfo cursorAnchorInfo =
131                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
132 
133         Matrix actualMatrix = cursorAnchorInfo.getMatrix();
134         Matrix expectedMatrix = new Matrix();
135         expectedMatrix.setTranslate(sLocationOnScreen[0] + translationX,
136                 sLocationOnScreen[1] + translationY);
137 
138         assertThat(actualMatrix).isEqualTo(expectedMatrix);
139     }
140 
141     @Test
testEditorBoundsInfo_allVisible()142     public void testEditorBoundsInfo_allVisible() {
143         // The needed width and height of the DEFAULT_TEXT are 50 px and 60 px respectfully.
144         int width = 100;
145         int height = 200;
146         setupEditText(DEFAULT_TEXT, width, height);
147         CursorAnchorInfo cursorAnchorInfo =
148                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
149         EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo();
150         assertThat(editorBoundsInfo).isNotNull();
151         assertThat(editorBoundsInfo.getEditorBounds()).isEqualTo(new RectF(0, 0, width, height));
152         assertThat(editorBoundsInfo.getHandwritingBounds())
153                 .isEqualTo(new RectF(-HW_BOUNDS_OFFSET_LEFT, -HW_BOUNDS_OFFSET_TOP,
154                         width + HW_BOUNDS_OFFSET_RIGHT, height + HW_BOUNDS_OFFSET_BOTTOM));
155     }
156 
157     @Test
testEditorBoundsInfo_scrolled()158     public void testEditorBoundsInfo_scrolled() {
159         // The height of the editor will be 60 px.
160         int width = 100;
161         int visibleTop = 10;
162         int visibleBottom = 30;
163         setupVerticalClippedEditText(width, visibleTop, visibleBottom);
164         CursorAnchorInfo cursorAnchorInfo =
165                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
166         EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo();
167         assertThat(editorBoundsInfo).isNotNull();
168         assertThat(editorBoundsInfo.getEditorBounds())
169                 .isEqualTo(new RectF(0, visibleTop, width, visibleBottom));
170         assertThat(editorBoundsInfo.getHandwritingBounds())
171                 .isEqualTo(new RectF(-HW_BOUNDS_OFFSET_LEFT, visibleTop - HW_BOUNDS_OFFSET_TOP,
172                         width + HW_BOUNDS_OFFSET_RIGHT, visibleBottom + HW_BOUNDS_OFFSET_BOTTOM));
173     }
174 
175     @Test
testEditorBoundsInfo_invisible()176     public void testEditorBoundsInfo_invisible() {
177         // The height of the editor will be 60px. Scroll it to 70px will make it invisible.
178         int width = 100;
179         int visibleTop = 70;
180         int visibleBottom = 70;
181         setupVerticalClippedEditText(width, visibleTop, visibleBottom);
182         CursorAnchorInfo cursorAnchorInfo =
183                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
184         EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo();
185         assertThat(editorBoundsInfo).isNotNull();
186         assertThat(editorBoundsInfo.getEditorBounds()).isEqualTo(new RectF(0, 0, 0, 0));
187         assertThat(editorBoundsInfo.getHandwritingBounds()).isEqualTo(new RectF(0, 0, 0, 0));
188     }
189 
190     @Test
testVisibleLineBounds_allVisible()191     public void testVisibleLineBounds_allVisible() {
192         setupEditText(DEFAULT_TEXT, /* height= */ 100);
193         CursorAnchorInfo cursorAnchorInfo =
194                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
195 
196         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
197 
198         assertThat(lineBounds).isEqualTo(DEFAULT_LINE_BOUNDS);
199     }
200 
201     @Test
testVisibleLineBounds_allVisible_withLineSpacing()202     public void testVisibleLineBounds_allVisible_withLineSpacing() {
203         float lineSpacing = 10f;
204         setupEditText("X\nXX\nXXX", /* height= */ 100, lineSpacing,
205                 /* lineMultiplier=*/ 1f);
206         CursorAnchorInfo cursorAnchorInfo =
207                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
208 
209         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
210 
211         assertThat(lineBounds.size()).isEqualTo(3);
212         assertThat(lineBounds.get(0)).isEqualTo(new RectF(0f, 0f, 10f, LINE_HEIGHT));
213 
214         float line1Top = LINE_HEIGHT + lineSpacing;
215         float line1Bottom = line1Top + LINE_HEIGHT;
216         assertThat(lineBounds.get(1)).isEqualTo(new RectF(0f, line1Top, 20f, line1Bottom));
217 
218         float line2Top = 2 * (LINE_HEIGHT + lineSpacing);
219         float line2Bottom = line2Top + LINE_HEIGHT;
220         assertThat(lineBounds.get(2)).isEqualTo(new RectF(0f, line2Top, 30f, line2Bottom));
221     }
222 
223     @Test
testVisibleLineBounds_allVisible_withLineMultiplier()224     public void testVisibleLineBounds_allVisible_withLineMultiplier() {
225         float lineMultiplier = 2f;
226         setupEditText("X\nXX\nXXX", /* height= */ 100, /* lineSpacing= */ 0f,
227                 /* lineMultiplier=*/ lineMultiplier);
228         CursorAnchorInfo cursorAnchorInfo =
229                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
230 
231         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
232 
233         assertThat(lineBounds.size()).isEqualTo(3);
234         assertThat(lineBounds.get(0)).isEqualTo(new RectF(0f, 0f, 10f, LINE_HEIGHT));
235 
236         float line1Top = LINE_HEIGHT * lineMultiplier;
237         float line1Bottom = line1Top + LINE_HEIGHT;
238         assertThat(lineBounds.get(1)).isEqualTo(new RectF(0f, line1Top, 20f, line1Bottom));
239 
240         float line2Top = 2 * LINE_HEIGHT * lineMultiplier;
241         float line2Bottom = line2Top + LINE_HEIGHT;
242         assertThat(lineBounds.get(2)).isEqualTo(new RectF(0f, line2Top, 30f, line2Bottom));
243     }
244 
245     @Test
testVisibleLineBounds_cutBottomLines()246     public void testVisibleLineBounds_cutBottomLines() {
247         // Line top is inclusive and line bottom is exclusive. And if the visible area's
248         // bottom equals to the line top, this line is still visible. So the line height is
249         // 3 * LINE_HEIGHT - 1 to avoid including the line 3.
250         setupEditText(DEFAULT_TEXT, /* height= */ 3 * LINE_HEIGHT - 1);
251         CursorAnchorInfo cursorAnchorInfo =
252                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
253 
254         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
255 
256         assertThat(lineBounds).isEqualTo(DEFAULT_LINE_BOUNDS.subList(0, 3));
257     }
258 
259     @Test
testVisibleLineBounds_scrolled_cutTopLines()260     public void testVisibleLineBounds_scrolled_cutTopLines() {
261         // First 2 lines are cut.
262         int scrollY = 2 * LINE_HEIGHT;
263         setupEditText(/* height= */ 3 * LINE_HEIGHT,
264                 /* scrollY= */ scrollY);
265         CursorAnchorInfo cursorAnchorInfo =
266                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
267 
268         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
269 
270         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 2, 5);
271         expectedLineBounds.forEach(rectF -> rectF.offset(0, -scrollY));
272 
273         assertThat(lineBounds).isEqualTo(expectedLineBounds);
274     }
275 
276     @Test
testVisibleLineBounds_scrolled_cutTopAndBottomLines()277     public void testVisibleLineBounds_scrolled_cutTopAndBottomLines() {
278         // Line top is inclusive and line bottom is exclusive. And if the visible area's
279         // bottom equals to the line top, this line is still visible. So the line height is
280         // 2 * LINE_HEIGHT - 1 which only shows 2 lines.
281         int scrollY = 2 * LINE_HEIGHT;
282         setupEditText(/* height= */ 2 * LINE_HEIGHT - 1,
283                 /* scrollY= */ scrollY);
284         CursorAnchorInfo cursorAnchorInfo =
285                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
286 
287         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
288 
289         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 2, 4);
290         expectedLineBounds.forEach(rectF -> rectF.offset(0, -scrollY));
291 
292         assertThat(lineBounds).isEqualTo(expectedLineBounds);
293     }
294 
295     @Test
testVisibleLineBounds_scrolled_partiallyVisibleLines()296     public void testVisibleLineBounds_scrolled_partiallyVisibleLines() {
297         // The first 2 lines are completely cut, line 2 and 3 are partially visible.
298         int scrollY = 2 * LINE_HEIGHT + LINE_HEIGHT / 2;
299         setupEditText(/* height= */ LINE_HEIGHT,
300                 /* scrollY= */ scrollY);
301         CursorAnchorInfo cursorAnchorInfo =
302                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
303 
304         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
305 
306         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 2, 4);
307         expectedLineBounds.forEach(rectF -> rectF.offset(0f, -scrollY));
308 
309         assertThat(lineBounds).isEqualTo(expectedLineBounds);
310     }
311 
312     @Test
testVisibleLineBounds_withCompoundDrawable_allVisible()313     public void testVisibleLineBounds_withCompoundDrawable_allVisible() {
314         int topDrawableHeight = LINE_HEIGHT;
315         Drawable topDrawable = createDrawable(topDrawableHeight);
316         Drawable bottomDrawable = createDrawable(2 * LINE_HEIGHT);
317         setupEditText(/* height= */ 100,
318                 /* scrollY= */ 0, topDrawable, bottomDrawable);
319         CursorAnchorInfo cursorAnchorInfo =
320                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
321 
322         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
323 
324         List<RectF> expectedLineBounds = copy(DEFAULT_LINE_BOUNDS);
325         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topDrawableHeight));
326 
327         assertThat(lineBounds).isEqualTo(expectedLineBounds);
328     }
329 
330     @Test
testVisibleLineBounds_withCompoundDrawable_cutBottomLines()331     public void testVisibleLineBounds_withCompoundDrawable_cutBottomLines() {
332         // The view's totally height is 5 * LINE_HEIGHT, and drawables take 3 * LINE_HEIGHT.
333         // Only first 2 lines are visible.
334         int topDrawableHeight = LINE_HEIGHT;
335         Drawable topDrawable = createDrawable(topDrawableHeight);
336         Drawable bottomDrawable = createDrawable(2 * LINE_HEIGHT + 1);
337         setupEditText(/* height= */ 5 * LINE_HEIGHT,
338                 /* scrollY= */ 0, topDrawable, bottomDrawable);
339         CursorAnchorInfo cursorAnchorInfo =
340                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
341 
342         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
343 
344         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 0, 2);
345         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topDrawableHeight));
346 
347         assertThat(lineBounds).isEqualTo(expectedLineBounds);
348     }
349 
350     @Test
testVisibleLineBounds_withCompoundDrawable_scrolled()351     public void testVisibleLineBounds_withCompoundDrawable_scrolled() {
352         // The view's totally height is 5 * LINE_HEIGHT, and drawables take 3 * LINE_HEIGHT.
353         // So 2 lines are visible. Because the view is scrolled vertically by LINE_HEIGHT,
354         // the line 1 and 2 are visible.
355         int topDrawableHeight = LINE_HEIGHT;
356         Drawable topDrawable = createDrawable(topDrawableHeight);
357         Drawable bottomDrawable = createDrawable(2 * LINE_HEIGHT + 1);
358         int scrollY = LINE_HEIGHT;
359         setupEditText(/* height= */ 5 * LINE_HEIGHT, scrollY,
360                 topDrawable, bottomDrawable);
361         CursorAnchorInfo cursorAnchorInfo =
362                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
363 
364         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
365 
366         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 1, 3);
367         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topDrawableHeight - scrollY));
368 
369         assertThat(lineBounds).isEqualTo(expectedLineBounds);
370     }
371 
372     @Test
testVisibleLineBounds_withCompoundDrawable_partiallyVisible()373     public void testVisibleLineBounds_withCompoundDrawable_partiallyVisible() {
374         // The view's totally height is 5 * LINE_HEIGHT, and drawables take 3 * LINE_HEIGHT.
375         // And because the view is scrolled vertically by 0.5 * LINE_HEIGHT,
376         // the line 0, 1 and 2 are visible.
377         int topDrawableHeight = LINE_HEIGHT;
378         Drawable topDrawable = createDrawable(topDrawableHeight);
379         Drawable bottomDrawable = createDrawable(2 * LINE_HEIGHT + 1);
380         int scrollY = LINE_HEIGHT / 2;
381         setupEditText(/* height= */ 5 * LINE_HEIGHT, scrollY,
382                 topDrawable, bottomDrawable);
383         CursorAnchorInfo cursorAnchorInfo =
384                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
385 
386         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
387 
388         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 0, 3);
389         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topDrawableHeight - scrollY));
390 
391         assertThat(lineBounds).isEqualTo(expectedLineBounds);
392     }
393 
394     @Test
testVisibleLineBounds_withPaddings_allVisible()395     public void testVisibleLineBounds_withPaddings_allVisible() {
396         int topPadding = LINE_HEIGHT;
397         int bottomPadding = LINE_HEIGHT;
398         setupEditText(/* height= */ 100, /* scrollY= */ 0, topPadding, bottomPadding);
399         CursorAnchorInfo cursorAnchorInfo =
400                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
401 
402         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
403 
404         List<RectF> expectedLineBounds = copy(DEFAULT_LINE_BOUNDS);
405         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topPadding));
406 
407         assertThat(lineBounds).isEqualTo(expectedLineBounds);
408     }
409 
410     @Test
testVisibleLineBounds_withPaddings_cutBottomLines()411     public void testVisibleLineBounds_withPaddings_cutBottomLines() {
412         // The view's totally height is 5 * LINE_HEIGHT, and paddings take 3 * LINE_HEIGHT.
413         // So 2 lines are visible.
414         int topPadding = LINE_HEIGHT;
415         int bottomPadding = 2 * LINE_HEIGHT + 1;
416         setupEditText(/* height= */ 5 * LINE_HEIGHT, /* scrollY= */ 0, topPadding, bottomPadding);
417         CursorAnchorInfo cursorAnchorInfo =
418                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
419 
420         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
421 
422         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 0, 2);
423         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topPadding));
424 
425         assertThat(lineBounds).isEqualTo(expectedLineBounds);
426     }
427 
428     @Test
testVisibleLineBounds_withPaddings_scrolled()429     public void testVisibleLineBounds_withPaddings_scrolled() {
430         // The view's totally height is 5 * LINE_HEIGHT, and paddings take 3 * LINE_HEIGHT.
431         // So 2 lines are visible. Because the view is scrolled vertically by LINE_HEIGHT,
432         // the line 1 and 2 are visible.
433         int topPadding = LINE_HEIGHT;
434         int bottomPadding = 2 * LINE_HEIGHT + 1;
435         int scrollY = LINE_HEIGHT;
436         setupEditText(/* height= */ 5 * LINE_HEIGHT, scrollY,
437                 topPadding, bottomPadding);
438         CursorAnchorInfo cursorAnchorInfo =
439                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
440 
441         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
442 
443         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 1, 3);
444         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topPadding - scrollY));
445 
446         assertThat(lineBounds).isEqualTo(expectedLineBounds);
447     }
448 
449     @Test
testVisibleLineBounds_withPadding_partiallyVisible()450     public void testVisibleLineBounds_withPadding_partiallyVisible() {
451         // The view's totally height is 5 * LINE_HEIGHT, and paddings take 3 * LINE_HEIGHT.
452         // And because the view is scrolled vertically by 0.5 * LINE_HEIGHT, the line 0, 1 and 2
453         // are visible.
454         int topPadding = LINE_HEIGHT;
455         int bottomPadding = 2 * LINE_HEIGHT + 1;
456         int scrollY = LINE_HEIGHT / 2;
457         setupEditText(/* height= */ 5 * LINE_HEIGHT, scrollY,
458                 topPadding, bottomPadding);
459         CursorAnchorInfo cursorAnchorInfo =
460                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
461 
462         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
463 
464         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 0, 3);
465         expectedLineBounds.forEach(rectF -> rectF.offset(0f, topPadding - scrollY));
466 
467         assertThat(lineBounds).isEqualTo(expectedLineBounds);
468     }
469 
470     @Test
testVisibleLineBounds_clippedTop()471     public void testVisibleLineBounds_clippedTop() {
472         // The first line is clipped off.
473         setupVerticalClippedEditText(LINE_HEIGHT, 5 * LINE_HEIGHT);
474         CursorAnchorInfo cursorAnchorInfo =
475                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
476 
477         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
478 
479         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 1, 5);
480         assertThat(lineBounds).isEqualTo(expectedLineBounds);
481     }
482 
483     @Test
testVisibleLineBounds_clippedBottom()484     public void testVisibleLineBounds_clippedBottom() {
485         // The last line is clipped off.
486         setupVerticalClippedEditText(0, 4 * LINE_HEIGHT - 1);
487         CursorAnchorInfo cursorAnchorInfo =
488                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
489 
490         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
491 
492         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 0, 4);
493         assertThat(lineBounds).isEqualTo(expectedLineBounds);
494     }
495 
496     @Test
testVisibleLineBounds_clippedTopAndBottom()497     public void testVisibleLineBounds_clippedTopAndBottom() {
498         // The first and last line are clipped off.
499         setupVerticalClippedEditText(LINE_HEIGHT, 4 * LINE_HEIGHT - 1);
500         CursorAnchorInfo cursorAnchorInfo =
501                 mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
502 
503         List<RectF> lineBounds = cursorAnchorInfo.getVisibleLineBounds();
504 
505         List<RectF> expectedLineBounds = subList(DEFAULT_LINE_BOUNDS, 1, 4);
506         assertThat(lineBounds).isEqualTo(expectedLineBounds);
507     }
508 
copy(List<RectF> rectFList)509     private List<RectF> copy(List<RectF> rectFList) {
510         List<RectF> result = new ArrayList<>();
511         for (RectF rectF : rectFList) {
512             result.add(new RectF(rectF));
513         }
514         return result;
515     }
subList(List<RectF> rectFList, int start, int end)516     private List<RectF> subList(List<RectF> rectFList, int start, int end) {
517         List<RectF> result = new ArrayList<>();
518         for (int index = start; index < end; ++index) {
519             result.add(new RectF(rectFList.get(index)));
520         }
521         return result;
522     }
523 
setupVerticalClippedEditText(int visibleTop, int visibleBottom)524     private void setupVerticalClippedEditText(int visibleTop, int visibleBottom) {
525         setupVerticalClippedEditText(1000, visibleTop, visibleBottom);
526     }
527 
528     /**
529      * Helper method to create an EditText in a vertical ScrollView so that its visible bounds
530      * is Rect(0, visibleTop, width, visibleBottom) in the EditText's coordinates. Both ScrollView
531      * and EditText's width is set to the given width.
532      */
setupVerticalClippedEditText(int width, int visibleTop, int visibleBottom)533     private void setupVerticalClippedEditText(int width, int visibleTop, int visibleBottom) {
534         ScrollView scrollView = new ScrollView(mActivity);
535         createEditText();
536         int scrollViewHeight = visibleBottom - visibleTop;
537 
538         scrollView.addView(mEditText, new FrameLayout.LayoutParams(
539                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
540                 View.MeasureSpec.makeMeasureSpec(5 * LINE_HEIGHT, View.MeasureSpec.EXACTLY)));
541         scrollView.measure(
542                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
543                 View.MeasureSpec.makeMeasureSpec(scrollViewHeight, View.MeasureSpec.EXACTLY));
544         scrollView.layout(0, 0, width, scrollViewHeight);
545         scrollView.scrollTo(0, visibleTop);
546     }
547 
setupEditText(CharSequence text, int height)548     private void setupEditText(CharSequence text, int height) {
549         createEditText(text);
550         measureEditText(height);
551     }
552 
setupEditText(CharSequence text, int width, int height)553     private void setupEditText(CharSequence text, int width, int height) {
554         createEditText(text);
555         measureEditText(width, height);
556     }
557 
setupEditText(CharSequence text, int height, float lineSpacing, float lineMultiplier)558     private void setupEditText(CharSequence text, int height, float lineSpacing,
559             float lineMultiplier) {
560         createEditText(text);
561         mEditText.setLineSpacing(lineSpacing, lineMultiplier);
562         measureEditText(height);
563     }
564 
setupEditText(int height, int scrollY)565     private void setupEditText(int height, int scrollY) {
566         createEditText();
567         mEditText.scrollTo(0, scrollY);
568         measureEditText(height);
569     }
570 
setupEditText(int height, int scrollY, Drawable drawableTop, Drawable drawableBottom)571     private void setupEditText(int height, int scrollY, Drawable drawableTop,
572             Drawable drawableBottom) {
573         createEditText();
574         mEditText.scrollTo(0, scrollY);
575         mEditText.setCompoundDrawables(null, drawableTop, null, drawableBottom);
576         measureEditText(height);
577     }
578 
setupEditText(int height, int scrollY, int paddingTop, int paddingBottom)579     private void setupEditText(int height, int scrollY, int paddingTop,
580             int paddingBottom) {
581         createEditText();
582         mEditText.scrollTo(0, scrollY);
583         mEditText.setPadding(0, paddingTop, 0, paddingBottom);
584         measureEditText(height);
585     }
586 
createEditText()587     private void createEditText() {
588         createEditText(DEFAULT_TEXT);
589     }
590 
createEditText(CharSequence text)591     private void createEditText(CharSequence text) {
592         mEditText = new EditText(mActivity);
593         mEditText.setTypeface(sTypeface);
594         mEditText.setText(text);
595         mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, TEXT_SIZE);
596         mEditText.setHandwritingBoundsOffsets(HW_BOUNDS_OFFSET_LEFT, HW_BOUNDS_OFFSET_TOP,
597                 HW_BOUNDS_OFFSET_RIGHT, HW_BOUNDS_OFFSET_BOTTOM);
598 
599         mEditText.setPadding(0, 0, 0, 0);
600         mEditText.setCompoundDrawables(null, null, null, null);
601         mEditText.setCompoundDrawablePadding(0);
602 
603         mEditText.scrollTo(0, 0);
604         mEditText.setLineSpacing(0f, 1f);
605 
606         // Place the text layout top to the view's top.
607         mEditText.setGravity(Gravity.TOP);
608     }
609 
measureEditText(int height)610     private void measureEditText(int height) {
611         // width equals to 1000 is enough to avoid line break for all test cases.
612         measureEditText(1000, height);
613     }
614 
measureEditText(int width, int height)615     private void measureEditText(int width, int height) {
616         mEditText.measure(
617                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
618                 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
619         mEditText.layout(0, 0, width, height);
620 
621         mEditText.getLocationOnScreen(sLocationOnScreen);
622     }
623 
createDrawable(int height)624     private Drawable createDrawable(int height) {
625         // width is not important for this drawable, make it 1 pixel.
626         return createDrawable(1, height);
627     }
628 
createDrawable(int width, int height)629     private Drawable createDrawable(int width, int height) {
630         ShapeDrawable drawable = new ShapeDrawable();
631         drawable.setBounds(new Rect(0, 0, width, height));
632         return drawable;
633     }
634 }
635