1 /* 2 * Copyright 2024 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.pdf.util; 18 19 import android.content.Context; 20 import android.util.AttributeSet; 21 import android.view.GestureDetector.OnGestureListener; 22 import android.view.MotionEvent; 23 import android.widget.FrameLayout; 24 25 import androidx.annotation.RestrictTo; 26 import androidx.pdf.util.GestureTracker.Gesture; 27 28 import org.jspecify.annotations.NonNull; 29 import org.jspecify.annotations.Nullable; 30 31 /** 32 * A {@link FrameLayout} plus helper methods to reliably share gestures with its hierarchy, by: 33 * 34 * <ul> 35 * <li>providing a {@link GestureTracker} that will detect what gesture is happening, regardless 36 * of where it is aimed at 37 * <li>handling generic gesture detection and passing it on in simple callbacks 38 * <li>forcing a priority order of handling an event bottom up (from the actual target View up to 39 * any containing View and finally the Activity), including when intercepts trigger. 40 * </ul> 41 */ 42 @RestrictTo(RestrictTo.Scope.LIBRARY) 43 public abstract class GestureTrackingView extends FrameLayout { 44 45 protected final GestureTracker mGestureTracker; 46 GestureTrackingView(@onNull Context context)47 public GestureTrackingView(@NonNull Context context) { 48 super(context); 49 } 50 GestureTrackingView(@onNull Context context, @Nullable AttributeSet attrs)51 public GestureTrackingView(@NonNull Context context, @Nullable AttributeSet attrs) { 52 super(context, attrs); 53 } 54 GestureTrackingView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)55 public GestureTrackingView(@NonNull Context context, @Nullable AttributeSet attrs, 56 int defStyleAttr) { 57 super(context, attrs, defStyleAttr); 58 } 59 GestureTrackingView(@onNull Context ctx, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)60 public GestureTrackingView(@NonNull Context ctx, @Nullable AttributeSet attrs, int defStyleAttr, 61 int defStyleRes) { 62 super(ctx, attrs, defStyleAttr, defStyleRes); 63 } 64 65 { 66 mGestureTracker = new GestureTracker(getContext()); 67 } 68 69 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)70 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 71 // We need to override this because when a child wants to release an event (i.e. stop 72 // holding on it, by using requestDisallowInterceptTouchEvent(false)), the next in line for 73 // getting this event should be its closest parent, as opposed to the highest container 74 // View, amongst the set of Views in its hierarchy that would like to intercept it. 75 super.requestDisallowInterceptTouchEvent(disallowIntercept); 76 mGestureTracker.interrupt(disallowIntercept); 77 if (!disallowIntercept) { 78 // Give ourself at least one chance to get the event next time. 79 getParent().requestDisallowInterceptTouchEvent(true); 80 } 81 } 82 83 @Override onInterceptTouchEvent(MotionEvent ev)84 public final boolean onInterceptTouchEvent(MotionEvent ev) { 85 if (mGestureTracker.feed(ev, false) && mGestureTracker.matches(Gesture.TOUCH)) { 86 // Until further notice (i.e. releaseEvent()), we want to keep tracking the gesture. 87 getParent().requestDisallowInterceptTouchEvent(true); 88 } 89 boolean intercept = interceptGesture(mGestureTracker); 90 if (intercept && mGestureTracker.matches(Gesture.DOUBLE_TAP)) { 91 mGestureTracker.handleDoubleTap(ev); 92 // Can't intercept this for real, as nested views need to know this is a double-tap, 93 // not a single one. 94 return false; 95 } else { 96 return intercept; 97 } 98 } 99 100 @Override onTouchEvent(MotionEvent event)101 public final boolean onTouchEvent(MotionEvent event) { 102 mGestureTracker.feed(event, true); 103 104 // We are interested in receiving further events. This matters only for ACTION_DOWN. 105 return true; 106 } 107 108 @Override onGenericMotionEvent(MotionEvent event)109 public boolean onGenericMotionEvent(MotionEvent event) { 110 if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { 111 // Support for mouse-wheel scroll events. 112 mGestureTracker.feed(event, true); 113 } 114 return true; 115 } 116 117 /** 118 * Releases any captured gesture, so that container Views can get a chance to handle it. Once a 119 * gesture is released, it's not coming back to this View. 120 */ releaseGesture()121 protected void releaseGesture() { 122 getParent().requestDisallowInterceptTouchEvent(false); 123 } 124 125 /** 126 * Hook method called during {@link #onInterceptTouchEvent} in order to determine whether the 127 * current gesture should be captured by this View. 128 * 129 * @param gestureTracker The {@link GestureTracker} with the current gesture. 130 * @return True if this View should capture the gesture, false if it doesn't bother. 131 */ interceptGesture(@onNull GestureTracker gestureTracker)132 protected abstract boolean interceptGesture(@NonNull GestureTracker gestureTracker); 133 patchGestureListener(@onNull OnGestureListener original)134 protected @NonNull OnGestureListener patchGestureListener(@NonNull OnGestureListener original) { 135 return new PatchedSimpleGestureHandler(original); 136 } 137 138 /** 139 * A wrapping {@link OnGestureListener} that corrects the method {@link #onScroll} so that it's 140 * not called with absurd values for distanceX and distanceY. 141 */ 142 protected static class PatchedSimpleGestureHandler implements OnGestureListener { 143 144 private final OnGestureListener mHandler; 145 146 /** Initial {@link #onScroll} events can have inaccurate distance values */ 147 private boolean mDiscardFirstScroll = true; 148 PatchedSimpleGestureHandler(OnGestureListener listener)149 private PatchedSimpleGestureHandler(OnGestureListener listener) { 150 mHandler = listener; 151 } 152 153 @Override onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY)154 public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, 155 float distanceY) { 156 if (mDiscardFirstScroll) { 157 mDiscardFirstScroll = false; 158 return true; 159 } 160 161 return mHandler.onScroll(e1, e2, distanceX, distanceY); 162 } 163 onEndGesture()164 protected void onEndGesture() { 165 mDiscardFirstScroll = true; 166 } 167 168 @Override onDown(@onNull MotionEvent e)169 public boolean onDown(@NonNull MotionEvent e) { 170 return mHandler.onDown(e); 171 } 172 173 @Override onShowPress(@onNull MotionEvent e)174 public void onShowPress(@NonNull MotionEvent e) { 175 mHandler.onShowPress(e); 176 } 177 178 @Override onSingleTapUp(@onNull MotionEvent e)179 public boolean onSingleTapUp(@NonNull MotionEvent e) { 180 return mHandler.onSingleTapUp(e); 181 } 182 183 @Override onLongPress(@onNull MotionEvent e)184 public void onLongPress(@NonNull MotionEvent e) { 185 mHandler.onLongPress(e); 186 } 187 188 @Override onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY)189 public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, 190 float velocityY) { 191 return mHandler.onFling(e1, e2, velocityX, velocityY); 192 } 193 } 194 } 195