1 /*
2  * Copyright (C) 2017 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 com.android.systemui.statusbar.phone;
18 
19 import android.view.MotionEvent;
20 import android.view.View;
21 import android.view.ViewConfiguration;
22 
23 import com.android.systemui.R;
24 
25 /**
26  * Detects a double tap.
27  */
28 public class DoubleTapHelper {
29 
30     private static final long DOUBLETAP_TIMEOUT_MS = 1200;
31 
32     private final View mView;
33     private final ActivationListener mActivationListener;
34     private final DoubleTapListener mDoubleTapListener;
35     private final SlideBackListener mSlideBackListener;
36     private final DoubleTapLogListener mDoubleTapLogListener;
37 
38     private float mTouchSlop;
39     private float mDoubleTapSlop;
40 
41     private boolean mActivated;
42 
43     private float mDownX;
44     private float mDownY;
45     private boolean mTrackTouch;
46 
47     private float mActivationX;
48     private float mActivationY;
49     private Runnable mTapTimeoutRunnable = this::makeInactive;
50 
DoubleTapHelper(View view, ActivationListener activationListener, DoubleTapListener doubleTapListener, SlideBackListener slideBackListener, DoubleTapLogListener doubleTapLogListener)51     public DoubleTapHelper(View view, ActivationListener activationListener,
52             DoubleTapListener doubleTapListener, SlideBackListener slideBackListener,
53             DoubleTapLogListener doubleTapLogListener) {
54         mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
55         mDoubleTapSlop = view.getResources().getDimension(R.dimen.double_tap_slop);
56         mView = view;
57 
58         mActivationListener = activationListener;
59         mDoubleTapListener = doubleTapListener;
60         mSlideBackListener = slideBackListener;
61         mDoubleTapLogListener = doubleTapLogListener;
62     }
63 
onTouchEvent(MotionEvent event)64     public boolean onTouchEvent(MotionEvent event) {
65         return onTouchEvent(event, Integer.MAX_VALUE);
66     }
67 
onTouchEvent(MotionEvent event, int maxTouchableHeight)68     public boolean onTouchEvent(MotionEvent event, int maxTouchableHeight) {
69         int action = event.getActionMasked();
70         switch (action) {
71             case MotionEvent.ACTION_DOWN:
72                 mDownX = event.getX();
73                 mDownY = event.getY();
74                 mTrackTouch = true;
75                 if (mDownY > maxTouchableHeight) {
76                     mTrackTouch = false;
77                 }
78                 break;
79             case MotionEvent.ACTION_MOVE:
80                 if (!isWithinTouchSlop(event)) {
81                     makeInactive();
82                     mTrackTouch = false;
83                 }
84                 break;
85             case MotionEvent.ACTION_UP:
86                 if (isWithinTouchSlop(event)) {
87                     if (mSlideBackListener != null && mSlideBackListener.onSlideBack()) {
88                         return true;
89                     }
90                     if (!mActivated) {
91                         makeActive();
92                         mView.postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
93                         mActivationX = event.getX();
94                         mActivationY = event.getY();
95                     } else {
96                         boolean withinDoubleTapSlop = isWithinDoubleTapSlop(event);
97                         if (mDoubleTapLogListener != null) {
98                             mDoubleTapLogListener.onDoubleTapLog(withinDoubleTapSlop,
99                                     event.getX() - mActivationX,
100                                     event.getY() - mActivationY);
101                         }
102                         if (withinDoubleTapSlop) {
103                             makeInactive();
104                             if (!mDoubleTapListener.onDoubleTap()) {
105                                 return false;
106                             }
107                         } else {
108                             makeInactive();
109                             mTrackTouch = false;
110                         }
111                     }
112                 } else {
113                     makeInactive();
114                     mTrackTouch = false;
115                 }
116                 break;
117             case MotionEvent.ACTION_CANCEL:
118                 makeInactive();
119                 mTrackTouch = false;
120                 break;
121             default:
122                 break;
123         }
124         return mTrackTouch;
125     }
126 
makeActive()127     private void makeActive() {
128         if (!mActivated) {
129             mActivated = true;
130             mActivationListener.onActiveChanged(true);
131         }
132     }
133 
makeInactive()134     private void makeInactive() {
135         if (mActivated) {
136             mActivated = false;
137             mActivationListener.onActiveChanged(false);
138             mView.removeCallbacks(mTapTimeoutRunnable);
139         }
140     }
141 
isWithinTouchSlop(MotionEvent event)142     private boolean isWithinTouchSlop(MotionEvent event) {
143         return Math.abs(event.getX() - mDownX) < mTouchSlop
144                 && Math.abs(event.getY() - mDownY) < mTouchSlop;
145     }
146 
isWithinDoubleTapSlop(MotionEvent event)147     public boolean isWithinDoubleTapSlop(MotionEvent event) {
148         if (!mActivated) {
149             // If we're not activated there's no double tap slop to satisfy.
150             return true;
151         }
152 
153         return Math.abs(event.getX() - mActivationX) < mDoubleTapSlop
154                 && Math.abs(event.getY() - mActivationY) < mDoubleTapSlop;
155     }
156 
157     @FunctionalInterface
158     public interface ActivationListener {
onActiveChanged(boolean active)159         void onActiveChanged(boolean active);
160     }
161 
162     @FunctionalInterface
163     public interface DoubleTapListener {
onDoubleTap()164         boolean onDoubleTap();
165     }
166 
167     @FunctionalInterface
168     public interface SlideBackListener {
onSlideBack()169         boolean onSlideBack();
170     }
171 
172     @FunctionalInterface
173     public interface DoubleTapLogListener {
onDoubleTapLog(boolean accepted, float dx, float dy)174         void onDoubleTapLog(boolean accepted, float dx, float dy);
175     }
176 }
177