1 /*
2  * Copyright (C) 2019 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 package com.android.launcher3.touch;
17 
18 import android.content.Context;
19 import android.graphics.PointF;
20 import android.util.Log;
21 import android.view.MotionEvent;
22 import android.view.ViewConfiguration;
23 
24 import androidx.annotation.NonNull;
25 import androidx.annotation.VisibleForTesting;
26 
27 import com.android.launcher3.Utilities;
28 import com.android.launcher3.testing.TestProtocol;
29 
30 /**
31  * One dimensional scroll/drag/swipe gesture detector (either HORIZONTAL or VERTICAL).
32  */
33 public class SingleAxisSwipeDetector extends BaseSwipeDetector {
34 
35     public static final int DIRECTION_POSITIVE = 1 << 0;
36     public static final int DIRECTION_NEGATIVE = 1 << 1;
37     public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
38 
39     public static final Direction VERTICAL = new Direction() {
40 
41         @Override
42         boolean isPositive(float displacement) {
43             // Up
44             return displacement < 0;
45         }
46 
47         @Override
48         boolean isNegative(float displacement) {
49             // Down
50             return displacement > 0;
51         }
52 
53         @Override
54         float extractDirection(PointF direction) {
55             return direction.y;
56         }
57 
58         @Override
59         float extractOrthogonalDirection(PointF direction) {
60             return direction.x;
61         }
62 
63     };
64 
65     public static final Direction HORIZONTAL = new Direction() {
66 
67         @Override
68         boolean isPositive(float displacement) {
69             // Right
70             return displacement > 0;
71         }
72 
73         @Override
74         boolean isNegative(float displacement) {
75             // Left
76             return displacement < 0;
77         }
78 
79         @Override
80         float extractDirection(PointF direction) {
81             return direction.x;
82         }
83 
84         @Override
85         float extractOrthogonalDirection(PointF direction) {
86             return direction.y;
87         }
88 
89     };
90 
91     private final Direction mDir;
92     /* Client of this gesture detector can register a callback. */
93     private final Listener mListener;
94 
95     private int mScrollDirections;
96 
SingleAxisSwipeDetector(@onNull Context context, @NonNull Listener l, @NonNull Direction dir)97     public SingleAxisSwipeDetector(@NonNull Context context, @NonNull Listener l,
98             @NonNull Direction dir) {
99         this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
100     }
101 
102     @VisibleForTesting
SingleAxisSwipeDetector(@onNull ViewConfiguration config, @NonNull Listener l, @NonNull Direction dir, boolean isRtl)103     protected SingleAxisSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
104             @NonNull Direction dir, boolean isRtl) {
105         super(config, isRtl);
106         mListener = l;
107         mDir = dir;
108         if (TestProtocol.sDebugTracing) {
109             Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector.ctor "
110                     + l.getClass().getSimpleName()
111                     + " @ " + android.util.Log.getStackTraceString(new Throwable()));
112         }
113     }
114 
setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop)115     public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
116         mScrollDirections = scrollDirectionFlags;
117         mIgnoreSlopWhenSettling = ignoreSlop;
118     }
119 
getScrollDirections()120     public int getScrollDirections() {
121         return mScrollDirections;
122     }
123 
124     /**
125      * Returns if the start drag was towards the positive direction or negative.
126      *
127      * @see #setDetectableScrollConditions(int, boolean)
128      * @see #DIRECTION_BOTH
129      */
wasInitialTouchPositive()130     public boolean wasInitialTouchPositive() {
131         return mDir.isPositive(mDir.extractDirection(mSubtractDisplacement));
132     }
133 
134     @Override
shouldScrollStart(PointF displacement)135     protected boolean shouldScrollStart(PointF displacement) {
136         // Reject cases where the angle or slop condition is not met.
137         float minDisplacement = Math.max(mTouchSlop,
138                 Math.abs(mDir.extractOrthogonalDirection(displacement)));
139         if (Math.abs(mDir.extractDirection(displacement)) < minDisplacement) {
140             return false;
141         }
142 
143         // Check if the client is interested in scroll in current direction.
144         float displacementComponent = mDir.extractDirection(displacement);
145         return canScrollNegative(displacementComponent) || canScrollPositive(displacementComponent);
146     }
147 
canScrollNegative(float displacement)148     private boolean canScrollNegative(float displacement) {
149         return (mScrollDirections & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(displacement);
150     }
151 
canScrollPositive(float displacement)152     private boolean canScrollPositive(float displacement) {
153         return (mScrollDirections & DIRECTION_POSITIVE) > 0 && mDir.isPositive(displacement);
154     }
155 
156     @Override
reportDragStartInternal(boolean recatch)157     protected void reportDragStartInternal(boolean recatch) {
158         float startDisplacement = mDir.extractDirection(mSubtractDisplacement);
159         mListener.onDragStart(!recatch, startDisplacement);
160     }
161 
162     @Override
reportDraggingInternal(PointF displacement, MotionEvent event)163     protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
164         if (TestProtocol.sDebugTracing) {
165             Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector "
166                     + mListener.getClass().getSimpleName());
167         }
168         mListener.onDrag(mDir.extractDirection(displacement),
169                 mDir.extractOrthogonalDirection(displacement), event);
170     }
171 
172     @Override
reportDragEndInternal(PointF velocity)173     protected void reportDragEndInternal(PointF velocity) {
174         float velocityComponent = mDir.extractDirection(velocity);
175         mListener.onDragEnd(velocityComponent);
176     }
177 
178     /** Listener to receive updates on the swipe. */
179     public interface Listener {
180         /**
181          * TODO(b/150256055) consolidate all the different onDrag() methods into one
182          * @param start whether this was the original drag start, as opposed to a recatch.
183          * @param startDisplacement the initial touch displacement for the primary direction as
184          *        given by by {@link Direction#extractDirection(PointF)}
185          */
onDragStart(boolean start, float startDisplacement)186         void onDragStart(boolean start, float startDisplacement);
187 
onDrag(float displacement)188         boolean onDrag(float displacement);
189 
onDrag(float displacement, MotionEvent event)190         default boolean onDrag(float displacement, MotionEvent event) {
191             return onDrag(displacement);
192         }
193 
onDrag(float displacement, float orthogonalDisplacement, MotionEvent ev)194         default boolean onDrag(float displacement, float orthogonalDisplacement, MotionEvent ev) {
195             return onDrag(displacement, ev);
196         }
197 
onDragEnd(float velocity)198         void onDragEnd(float velocity);
199     }
200 
201     public abstract static class Direction {
202 
isPositive(float displacement)203         abstract boolean isPositive(float displacement);
204 
isNegative(float displacement)205         abstract boolean isNegative(float displacement);
206 
207         /** Returns the part of the given {@link PointF} that is relevant to this direction. */
extractDirection(PointF point)208         abstract float extractDirection(PointF point);
209 
extractOrthogonalDirection(PointF point)210         abstract float extractOrthogonalDirection(PointF point);
211 
212     }
213 }
214