1 /*
2  * Copyright (C) 2012 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.policy;
18 
19 import android.animation.ObjectAnimator;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.os.SystemClock;
24 import android.util.AttributeSet;
25 import android.util.Slog;
26 import android.view.MotionEvent;
27 import android.view.Surface;
28 import android.view.View;
29 
30 import com.android.systemui.R;
31 
32 /**
33  * The "dead zone" consumes unintentional taps along the top edge of the navigation bar.
34  * When users are typing quickly on an IME they may attempt to hit the space bar, overshoot, and
35  * accidentally hit the home button. The DeadZone expands temporarily after each tap in the UI
36  * outside the navigation bar (since this is when accidental taps are more likely), then contracts
37  * back over time (since a later tap might be intended for the top of the bar).
38  */
39 public class DeadZone extends View {
40     public static final String TAG = "DeadZone";
41 
42     public static final boolean DEBUG = false;
43     public static final int HORIZONTAL = 0;  // Consume taps along the top edge.
44     public static final int VERTICAL = 1;  // Consume taps along the left edge.
45 
46     private static final boolean CHATTY = true; // print to logcat when we eat a click
47 
48     private boolean mShouldFlash;
49     private float mFlashFrac = 0f;
50 
51     private int mSizeMax;
52     private int mSizeMin;
53     // Upon activity elsewhere in the UI, the dead zone will hold steady for
54     // mHold ms, then move back over the course of mDecay ms
55     private int mHold, mDecay;
56     private boolean mVertical;
57     private long mLastPokeTime;
58     private int mDisplayRotation;
59 
60     private final Runnable mDebugFlash = new Runnable() {
61         @Override
62         public void run() {
63             ObjectAnimator.ofFloat(DeadZone.this, "flash", 1f, 0f).setDuration(150).start();
64         }
65     };
66 
DeadZone(Context context, AttributeSet attrs)67     public DeadZone(Context context, AttributeSet attrs) {
68         this(context, attrs, 0);
69     }
70 
DeadZone(Context context, AttributeSet attrs, int defStyle)71     public DeadZone(Context context, AttributeSet attrs, int defStyle) {
72         super(context, attrs);
73 
74         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DeadZone,
75                 defStyle, 0);
76 
77         mHold = a.getInteger(R.styleable.DeadZone_holdTime, 0);
78         mDecay = a.getInteger(R.styleable.DeadZone_decayTime, 0);
79 
80         mSizeMin = a.getDimensionPixelSize(R.styleable.DeadZone_minSize, 0);
81         mSizeMax = a.getDimensionPixelSize(R.styleable.DeadZone_maxSize, 0);
82 
83         int index = a.getInt(R.styleable.DeadZone_orientation, -1);
84         mVertical = (index == VERTICAL);
85 
86         if (DEBUG)
87             Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
88                     + (mVertical ? " vertical" : " horizontal"));
89 
90         setFlashOnTouchCapture(context.getResources().getBoolean(R.bool.config_dead_zone_flash));
91     }
92 
lerp(float a, float b, float f)93     static float lerp(float a, float b, float f) {
94         return (b - a) * f + a;
95     }
96 
getSize(long now)97     private float getSize(long now) {
98         if (mSizeMax == 0)
99             return 0;
100         long dt = (now - mLastPokeTime);
101         if (dt > mHold + mDecay)
102             return mSizeMin;
103         if (dt < mHold)
104             return mSizeMax;
105         return (int) lerp(mSizeMax, mSizeMin, (float) (dt - mHold) / mDecay);
106     }
107 
setFlashOnTouchCapture(boolean dbg)108     public void setFlashOnTouchCapture(boolean dbg) {
109         mShouldFlash = dbg;
110         mFlashFrac = 0f;
111         postInvalidate();
112     }
113 
114     // I made you a touch event...
115     @Override
onTouchEvent(MotionEvent event)116     public boolean onTouchEvent(MotionEvent event) {
117         if (DEBUG) {
118             Slog.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
119         }
120 
121         // Don't consume events for high precision pointing devices. For this purpose a stylus is
122         // considered low precision (like a finger), so its events may be consumed.
123         if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
124             return false;
125         }
126 
127         final int action = event.getAction();
128         if (action == MotionEvent.ACTION_OUTSIDE) {
129             poke(event);
130             return true;
131         } else if (action == MotionEvent.ACTION_DOWN) {
132             if (DEBUG) {
133                 Slog.v(TAG, this + " ACTION_DOWN: " + event.getX() + "," + event.getY());
134             }
135             int size = (int) getSize(event.getEventTime());
136             // In the vertical orientation consume taps along the left edge.
137             // In horizontal orientation consume taps along the top edge.
138             final boolean consumeEvent;
139             if (mVertical) {
140                 if (mDisplayRotation == Surface.ROTATION_270) {
141                     consumeEvent = event.getX() > getWidth() - size;
142                 } else {
143                     consumeEvent = event.getX() < size;
144                 }
145             } else {
146                 consumeEvent = event.getY() < size;
147             }
148             if (consumeEvent) {
149                 if (CHATTY) {
150                     Slog.v(TAG, "consuming errant click: (" + event.getX() + "," + event.getY() + ")");
151                 }
152                 if (mShouldFlash) {
153                     post(mDebugFlash);
154                     postInvalidate();
155                 }
156                 return true; // ...but I eated it
157             }
158         }
159         return false;
160     }
161 
162     private void poke(MotionEvent event) {
163         mLastPokeTime = event.getEventTime();
164         if (DEBUG)
165             Slog.v(TAG, "poked! size=" + getSize(mLastPokeTime));
166         if (mShouldFlash) postInvalidate();
167     }
168 
169     public void setFlash(float f) {
170         mFlashFrac = f;
171         postInvalidate();
172     }
173 
174     public float getFlash() {
175         return mFlashFrac;
176     }
177 
178     @Override
179     public void onDraw(Canvas can) {
180         if (!mShouldFlash || mFlashFrac <= 0f) {
181             return;
182         }
183 
184         final int size = (int) getSize(SystemClock.uptimeMillis());
185         if (mVertical) {
186             if (mDisplayRotation == Surface.ROTATION_270) {
187                 can.clipRect(can.getWidth() - size, 0, can.getWidth(), can.getHeight());
188             } else {
189                 can.clipRect(0, 0, size, can.getHeight());
190             }
191         } else {
192             can.clipRect(0, 0, can.getWidth(), size);
193         }
194 
195         final float frac = DEBUG ? (mFlashFrac - 0.5f) + 0.5f : mFlashFrac;
196         can.drawARGB((int) (frac * 0xFF), 0xDD, 0xEE, 0xAA);
197 
198         if (DEBUG && size > mSizeMin)
199             // crazy aggressive redrawing here, for debugging only
200             postInvalidateDelayed(100);
201     }
202 
203     public void setDisplayRotation(int rotation) {
204         mDisplayRotation = rotation;
205     }
206 }
207