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