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                             if (!mDoubleTapListener.onDoubleTap()) {
104                                 return false;
105                             }
106                         } else {
107                             makeInactive();
108                             mTrackTouch = false;
109                         }
110                     }
111                 } else {
112                     makeInactive();
113                     mTrackTouch = false;
114                 }
115                 break;
116             case MotionEvent.ACTION_CANCEL:
117                 makeInactive();
118                 mTrackTouch = false;
119                 break;
120             default:
121                 break;
122         }
123         return mTrackTouch;
124     }
125 
makeActive()126     private void makeActive() {
127         if (!mActivated) {
128             mActivated = true;
129             mActivationListener.onActiveChanged(true);
130         }
131     }
132 
makeInactive()133     private void makeInactive() {
134         if (mActivated) {
135             mActivated = false;
136             mActivationListener.onActiveChanged(false);
137         }
138     }
139 
isWithinTouchSlop(MotionEvent event)140     private boolean isWithinTouchSlop(MotionEvent event) {
141         return Math.abs(event.getX() - mDownX) < mTouchSlop
142                 && Math.abs(event.getY() - mDownY) < mTouchSlop;
143     }
144 
isWithinDoubleTapSlop(MotionEvent event)145     public boolean isWithinDoubleTapSlop(MotionEvent event) {
146         if (!mActivated) {
147             // If we're not activated there's no double tap slop to satisfy.
148             return true;
149         }
150 
151         return Math.abs(event.getX() - mActivationX) < mDoubleTapSlop
152                 && Math.abs(event.getY() - mActivationY) < mDoubleTapSlop;
153     }
154 
155     @FunctionalInterface
156     public interface ActivationListener {
onActiveChanged(boolean active)157         void onActiveChanged(boolean active);
158     }
159 
160     @FunctionalInterface
161     public interface DoubleTapListener {
onDoubleTap()162         boolean onDoubleTap();
163     }
164 
165     @FunctionalInterface
166     public interface SlideBackListener {
onSlideBack()167         boolean onSlideBack();
168     }
169 
170     @FunctionalInterface
171     public interface DoubleTapLogListener {
onDoubleTapLog(boolean accepted, float dx, float dy)172         void onDoubleTapLog(boolean accepted, float dx, float dy);
173     }
174 }
175