1 /*
2  * Copyright (C) 2016 Google Inc.
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.google.android.setupdesign.gesture;
18 
19 import android.graphics.Rect;
20 import android.view.MotionEvent;
21 import android.view.View;
22 import android.view.ViewConfiguration;
23 
24 /**
25  * Helper class to detect the consective-tap gestures on a view.
26  *
27  * <p>This class is instantiated and used similar to a GestureDetector, where onTouchEvent should be
28  * called when there are MotionEvents this detector should know about.
29  */
30 public final class ConsecutiveTapsGestureDetector {
31 
32   public interface OnConsecutiveTapsListener {
33     /** Callback method when the user tapped on the target view X number of times. */
onConsecutiveTaps(int numOfConsecutiveTaps)34     void onConsecutiveTaps(int numOfConsecutiveTaps);
35   }
36 
37   private final View view;
38   private final OnConsecutiveTapsListener listener;
39   private final int consecutiveTapTouchSlopSquare;
40   private final int consecutiveTapTimeout;
41 
42   private int consecutiveTapsCounter = 0;
43   private MotionEvent previousTapEvent;
44 
45   /**
46    * @param listener The listener that responds to the gesture.
47    * @param view The target view that associated with consecutive-tap gesture.
48    */
ConsecutiveTapsGestureDetector(OnConsecutiveTapsListener listener, View view)49   public ConsecutiveTapsGestureDetector(OnConsecutiveTapsListener listener, View view) {
50     this(listener, view, ViewConfiguration.getDoubleTapTimeout());
51   }
52 
53   /**
54    * @param listener The listener that responds to the gesture.
55    * @param view The target view that associated with consecutive-tap gesture.
56    * @param consecutiveTapTimeout Maximum time in millis between two consecutive taps.
57    */
ConsecutiveTapsGestureDetector( OnConsecutiveTapsListener listener, View view, int consecutiveTapTimeout)58   public ConsecutiveTapsGestureDetector(
59       OnConsecutiveTapsListener listener, View view, int consecutiveTapTimeout) {
60     this.listener = listener;
61     this.view = view;
62     this.consecutiveTapTimeout = consecutiveTapTimeout;
63     int doubleTapSlop = ViewConfiguration.get(this.view.getContext()).getScaledDoubleTapSlop();
64     consecutiveTapTouchSlopSquare = doubleTapSlop * doubleTapSlop;
65   }
66 
67   /**
68    * This method should be called from the relevant activity or view, typically in onTouchEvent,
69    * onInterceptTouchEvent or dispatchTouchEvent.
70    *
71    * @param ev The motion event
72    */
onTouchEvent(MotionEvent ev)73   public void onTouchEvent(MotionEvent ev) {
74     if (ev.getAction() == MotionEvent.ACTION_UP) {
75       Rect viewRect = new Rect();
76       int[] leftTop = new int[2];
77       view.getLocationOnScreen(leftTop);
78       viewRect.set(
79           leftTop[0], leftTop[1], leftTop[0] + view.getWidth(), leftTop[1] + view.getHeight());
80       if (viewRect.contains((int) ev.getX(), (int) ev.getY())) {
81         if (isConsecutiveTap(ev)) {
82           consecutiveTapsCounter++;
83         } else {
84           consecutiveTapsCounter = 1;
85         }
86         listener.onConsecutiveTaps(consecutiveTapsCounter);
87       } else {
88         // Touch outside the target view. Reset counter.
89         consecutiveTapsCounter = 0;
90       }
91 
92       if (previousTapEvent != null) {
93         previousTapEvent.recycle();
94       }
95       previousTapEvent = MotionEvent.obtain(ev);
96     }
97   }
98 
99   /** Resets the consecutive-tap counter to zero. */
resetCounter()100   public void resetCounter() {
101     consecutiveTapsCounter = 0;
102   }
103 
104   /**
105    * Returns true if the distance between consecutive tap is within {@link
106    * #consecutiveTapTouchSlopSquare}. False, otherwise.
107    */
isConsecutiveTap(MotionEvent currentTapEvent)108   private boolean isConsecutiveTap(MotionEvent currentTapEvent) {
109     if (previousTapEvent == null) {
110       return false;
111     }
112 
113     double deltaX = previousTapEvent.getX() - currentTapEvent.getX();
114     double deltaY = previousTapEvent.getY() - currentTapEvent.getY();
115     long deltaTime = currentTapEvent.getEventTime() - previousTapEvent.getEventTime();
116     return (deltaX * deltaX + deltaY * deltaY <= consecutiveTapTouchSlopSquare)
117         && deltaTime < consecutiveTapTimeout;
118   }
119 }
120