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