1 /*
2  * Copyright (C) 2019 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.settings.gestures;
18 
19 import android.animation.TimeAnimator;
20 import android.annotation.IntRange;
21 import android.content.Context;
22 import android.graphics.Canvas;
23 import android.graphics.ColorFilter;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.os.Message;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.settings.R;
36 
37 /** A drawable to animate the inset back gesture in both edges of the screen */
38 public class BackGestureIndicatorDrawable extends Drawable {
39 
40     private static final String TAG = "BackGestureIndicatorDrawable";
41 
42     private static final int MSG_SET_INDICATOR_WIDTH = 1;
43     private static final int MSG_HIDE_INDICATOR = 3;
44 
45     private static final long ANIMATION_DURATION_MS = 200L;
46     private static final long HIDE_DELAY_MS = 700L;
47 
48     private static final int ALPHA_MAX = 64;
49 
50     private Context mContext;
51 
52     private Paint mPaint = new Paint();
53     private boolean mReversed;
54 
55     private float mFinalWidth;
56     private float mCurrentWidth;
57     private float mWidthChangePerMs;
58 
59     private TimeAnimator mTimeAnimator = new TimeAnimator();
60 
61     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
62         @Override
63         public void handleMessage(Message msg) {
64             switch(msg.what) {
65                 case MSG_SET_INDICATOR_WIDTH:
66                     mTimeAnimator.end();
67                     mFinalWidth = msg.arg1;
68                     mWidthChangePerMs = Math.abs(mCurrentWidth - mFinalWidth)
69                             / ANIMATION_DURATION_MS;
70                     mTimeAnimator.start();
71                     break;
72                 case MSG_HIDE_INDICATOR:
73                     mCurrentWidth = mFinalWidth;
74                     removeMessages(MSG_SET_INDICATOR_WIDTH);
75                     sendMessageDelayed(obtainMessage(MSG_SET_INDICATOR_WIDTH, 0, 0), HIDE_DELAY_MS);
76                     invalidateSelf();
77                     break;
78                 default:
79                     break;
80             }
81         }
82     };
83 
84     /**
85      * Creates an indicator drawable that responds to back gesture inset size change
86      * @param reversed If false, indicator will expand right. If true, indicator will expand left
87      */
BackGestureIndicatorDrawable(Context context, boolean reversed)88     public BackGestureIndicatorDrawable(Context context, boolean reversed) {
89         mContext = context;
90         mReversed = reversed;
91 
92         // Restart the timer whenever a change is detected, so we can shrink/fade the indicators
93         mTimeAnimator.setTimeListener((TimeAnimator animation, long totalTime, long deltaTime) -> {
94             updateCurrentWidth(totalTime, deltaTime);
95             invalidateSelf();
96         });
97     }
98 
updateCurrentWidth(long totalTime, long deltaTime)99     private void updateCurrentWidth(long totalTime, long deltaTime) {
100         synchronized (mTimeAnimator) {
101             float step = deltaTime * mWidthChangePerMs;
102             if (totalTime >= ANIMATION_DURATION_MS
103                     || step >= Math.abs(mFinalWidth - mCurrentWidth)) {
104                 mCurrentWidth = mFinalWidth;
105                 mTimeAnimator.end();
106             } else {
107                 float direction = mCurrentWidth < mFinalWidth ? 1 : -1;
108                 mCurrentWidth += direction * step;
109             }
110         }
111     }
112 
113     @Override
114     public void draw(@NonNull Canvas canvas) {
115 
116         mPaint.setAntiAlias(true);
117         mPaint.setColor(mContext.getResources().getColor(R.color.back_gesture_indicator));
118         mPaint.setAlpha(ALPHA_MAX);
119 
120         final int top = 0;
121         final int bottom = canvas.getHeight();
122         final int width = (int) mCurrentWidth;
123 
124         Rect rect = new Rect(0, top, width, bottom);
125         if (mReversed) {
126             rect.offset(canvas.getWidth() - width, 0);
127         }
128 
129         canvas.drawRect(rect, mPaint);
130     }
131 
132     @Override
133     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
134 
135     }
136 
137     @Override
138     public void setColorFilter(@Nullable ColorFilter colorFilter) {
139 
140     }
141 
142     @Override
143     public int getOpacity() {
144         return 0;
145     }
146 
147     /**
148      * Sets the visible width of the indicator in pixels.
149      */
150     public void setWidth(int width) {
151         if (width == 0) {
152             mHandler.sendEmptyMessage(MSG_HIDE_INDICATOR);
153         } else {
154             mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_INDICATOR_WIDTH, width, 0));
155         }
156     }
157 
158     @VisibleForTesting
159     public int getWidth() {
160         return (int) mFinalWidth;
161     }
162 }
163