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 androidx.core.text.method; 18 19 import android.os.Build; 20 import android.text.Layout; 21 import android.text.Selection; 22 import android.text.Spannable; 23 import android.text.method.LinkMovementMethod; 24 import android.text.method.Touch; 25 import android.view.MotionEvent; 26 import android.widget.TextView; 27 28 import org.jspecify.annotations.NonNull; 29 import org.jspecify.annotations.Nullable; 30 31 /** 32 * Backwards compatible version of {@link LinkMovementMethod} which fixes the issue that links can 33 * be triggered for touches outside of line bounds before Android V. 34 */ 35 public class LinkMovementMethodCompat extends LinkMovementMethod { 36 private static LinkMovementMethodCompat sInstance; 37 LinkMovementMethodCompat()38 private LinkMovementMethodCompat() {} 39 40 @Override onTouchEvent(@ullable TextView widget, @Nullable Spannable buffer, @Nullable MotionEvent event)41 public boolean onTouchEvent(@Nullable TextView widget, @Nullable Spannable buffer, 42 @Nullable MotionEvent event) { 43 if (Build.VERSION.SDK_INT < 35) { 44 int action = event.getAction(); 45 46 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 47 int x = (int) event.getX(); 48 int y = (int) event.getY(); 49 50 x -= widget.getTotalPaddingLeft(); 51 y -= widget.getTotalPaddingTop(); 52 53 x += widget.getScrollX(); 54 y += widget.getScrollY(); 55 56 Layout layout = widget.getLayout(); 57 boolean isOutOfLineBounds; 58 if (y < 0 || y > layout.getHeight()) { 59 isOutOfLineBounds = true; 60 } else { 61 int line = layout.getLineForVertical(y); 62 isOutOfLineBounds = x < layout.getLineLeft(line) 63 || x > layout.getLineRight(line); 64 } 65 66 if (isOutOfLineBounds) { 67 Selection.removeSelection(buffer); 68 69 // The same as super.onTouchEvent() in LinkMovementMethod.onTouchEvent(), i.e. 70 // ScrollingMovementMethod.onTouchEvent(). 71 return Touch.onTouchEvent(widget, buffer, event); 72 } 73 } 74 } 75 76 return super.onTouchEvent(widget, buffer, event); 77 } 78 79 /** 80 * Retrieves the singleton instance of {@link LinkMovementMethodCompat}. 81 * 82 * @return the singleton instance of {@link LinkMovementMethodCompat} 83 */ getInstance()84 public static @NonNull LinkMovementMethodCompat getInstance() { 85 if (sInstance == null) { 86 sInstance = new LinkMovementMethodCompat(); 87 } 88 89 return sInstance; 90 } 91 } 92