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