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