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