• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.systemui.assist.ui;
18 
19 import android.animation.ArgbEvaluator;
20 import android.annotation.ColorInt;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.util.MathUtils;
30 import android.view.ContextThemeWrapper;
31 import android.view.View;
32 
33 import com.android.settingslib.Utils;
34 import com.android.systemui.navigationbar.NavigationBar;
35 import com.android.systemui.navigationbar.NavigationBarController;
36 import com.android.systemui.navigationbar.NavigationBarTransitions;
37 import com.android.systemui.res.R;
38 
39 import java.util.ArrayList;
40 
41 /**
42  * Shows lights at the bottom of the phone, marking the invocation progress.
43  */
44 public class InvocationLightsView extends View
45         implements NavigationBarTransitions.DarkIntensityListener {
46 
47     private static final String TAG = "InvocationLightsView";
48 
49     private static final int LIGHT_HEIGHT_DP = 3;
50     // minimum light length as a fraction of the corner length
51     private static final float MINIMUM_CORNER_RATIO = .6f;
52 
53     protected final ArrayList<EdgeLight> mAssistInvocationLights = new ArrayList<>();
54     protected final PerimeterPathGuide mGuide;
55 
56     private final Paint mPaint = new Paint();
57     // Path used to render lights. One instance is used to draw all lights and is cached to avoid
58     // allocation on each frame.
59     private final Path mPath = new Path();
60     private final int mViewHeight;
61     private final int mStrokeWidth;
62     @ColorInt
63     private final int mLightColor;
64     @ColorInt
65     private final int mDarkColor;
66     @Nullable
67     private NavigationBarController mNavigationBarController;
68 
69     // Allocate variable for screen location lookup to avoid memory alloc onDraw()
70     private int[] mScreenLocation = new int[2];
71     private boolean mRegistered = false;
72     private boolean mUseNavBarColor = true;
73 
InvocationLightsView(Context context)74     public InvocationLightsView(Context context) {
75         this(context, null);
76     }
77 
InvocationLightsView(Context context, AttributeSet attrs)78     public InvocationLightsView(Context context, AttributeSet attrs) {
79         this(context, attrs, 0);
80     }
81 
InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr)82     public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr) {
83         this(context, attrs, defStyleAttr, 0);
84     }
85 
InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)86     public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr,
87             int defStyleRes) {
88         super(context, attrs, defStyleAttr, defStyleRes);
89 
90         mStrokeWidth = DisplayUtils.convertDpToPx(LIGHT_HEIGHT_DP, context);
91         mPaint.setStrokeWidth(mStrokeWidth);
92         mPaint.setStyle(Paint.Style.STROKE);
93         mPaint.setStrokeJoin(Paint.Join.MITER);
94         mPaint.setAntiAlias(true);
95 
96 
97         int displayWidth = DisplayUtils.getWidth(context);
98         int displayHeight = DisplayUtils.getHeight(context);
99         mGuide = new PerimeterPathGuide(context, createCornerPathRenderer(context),
100                 mStrokeWidth / 2, displayWidth, displayHeight);
101 
102         int cornerRadiusBottom = DisplayUtils.getCornerRadiusBottom(context);
103         int cornerRadiusTop = DisplayUtils.getCornerRadiusTop(context);
104         // ensure that height is non-zero even for square corners
105         mViewHeight = Math.max(Math.max(cornerRadiusBottom, cornerRadiusTop),
106             DisplayUtils.convertDpToPx(LIGHT_HEIGHT_DP, context));
107 
108         final int dualToneDarkTheme = Utils.getThemeAttr(mContext, R.attr.darkIconTheme);
109         final int dualToneLightTheme = Utils.getThemeAttr(mContext, R.attr.lightIconTheme);
110         Context lightContext = new ContextThemeWrapper(mContext, dualToneLightTheme);
111         Context darkContext = new ContextThemeWrapper(mContext, dualToneDarkTheme);
112         mLightColor = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor);
113         mDarkColor = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor);
114 
115         for (int i = 0; i < 4; i++) {
116             mAssistInvocationLights.add(new EdgeLight(Color.TRANSPARENT, 0, 0));
117         }
118     }
119 
120     /**
121      * Updates positions of the invocation lights based on the progress (a float between 0 and 1).
122      * The lights begin at the device corners and expand inward until they meet at the center.
123      */
onInvocationProgress(float progress)124     public void onInvocationProgress(float progress) {
125         if (progress == 0) {
126             setVisibility(View.GONE);
127         } else {
128             attemptRegisterNavBarListener();
129 
130             float cornerLengthNormalized =
131                     mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM_LEFT);
132             float arcLengthNormalized = cornerLengthNormalized * MINIMUM_CORNER_RATIO;
133             float arcOffsetNormalized = (cornerLengthNormalized - arcLengthNormalized) / 2f;
134 
135             float minLightLength = 0;
136             float maxLightLength = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) / 4f;
137 
138             float lightLength = MathUtils.lerp(minLightLength, maxLightLength, progress);
139 
140             float leftStart = (-cornerLengthNormalized + arcOffsetNormalized) * (1 - progress);
141             float rightStart = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM)
142                     + (cornerLengthNormalized - arcOffsetNormalized) * (1 - progress);
143 
144             setLight(0, leftStart, leftStart + lightLength);
145             setLight(1, leftStart + lightLength, leftStart + lightLength * 2);
146             setLight(2, rightStart - (lightLength * 2), rightStart - lightLength);
147             setLight(3, rightStart - lightLength, rightStart);
148             setVisibility(View.VISIBLE);
149         }
150         invalidate();
151     }
152 
153     /**
154      * Hides and resets the invocation lights.
155      */
hide()156     public void hide() {
157         setVisibility(GONE);
158         for (EdgeLight light : mAssistInvocationLights) {
159             light.setEndpoints(0, 0);
160         }
161         attemptUnregisterNavBarListener();
162     }
163 
164     /**
165      * Sets all invocation lights to a single color. If color is null, uses the navigation bar
166      * color (updated when the nav bar color changes).
167      */
setColors(@ullable @olorInt Integer color)168     public void setColors(@Nullable @ColorInt Integer color) {
169         if (color == null) {
170             mUseNavBarColor = true;
171             mPaint.setStrokeCap(Paint.Cap.BUTT);
172             attemptRegisterNavBarListener();
173         } else {
174             setColors(color, color, color, color);
175         }
176     }
177 
178     /**
179      * Sets the invocation light colors, from left to right.
180      */
setColors(@olorInt int color1, @ColorInt int color2, @ColorInt int color3, @ColorInt int color4)181     public void setColors(@ColorInt int color1, @ColorInt int color2,
182             @ColorInt int color3, @ColorInt int color4) {
183         mUseNavBarColor = false;
184         attemptUnregisterNavBarListener();
185         mAssistInvocationLights.get(0).setColor(color1);
186         mAssistInvocationLights.get(1).setColor(color2);
187         mAssistInvocationLights.get(2).setColor(color3);
188         mAssistInvocationLights.get(3).setColor(color4);
189     }
190 
191     /**
192      * Reacts to changes in the navigation bar color
193      *
194      * @param darkIntensity 0 is the lightest color, 1 is the darkest.
195      */
196     @Override // NavigationBarTransitions.DarkIntensityListener
onDarkIntensity(float darkIntensity)197     public void onDarkIntensity(float darkIntensity) {
198         updateDarkness(darkIntensity);
199     }
200 
201 
202     @Override
onFinishInflate()203     protected void onFinishInflate() {
204         getLayoutParams().height = mViewHeight;
205         requestLayout();
206     }
207 
208     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)209     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
210         super.onLayout(changed, left, top, right, bottom);
211 
212         int rotation = getContext().getDisplay().getRotation();
213         mGuide.setRotation(rotation);
214     }
215 
216     @Override
onDraw(Canvas canvas)217     protected void onDraw(Canvas canvas) {
218         // If the view doesn't take up the whole screen, offset the canvas by its translation
219         // distance such that PerimeterPathGuide's paths are drawn properly based upon the actual
220         // screen edges.
221         getLocationOnScreen(mScreenLocation);
222         canvas.translate(-mScreenLocation[0], -mScreenLocation[1]);
223 
224         if (mUseNavBarColor) {
225             for (EdgeLight light : mAssistInvocationLights) {
226                 renderLight(light, canvas);
227             }
228         } else {
229             mPaint.setStrokeCap(Paint.Cap.ROUND);
230             renderLight(mAssistInvocationLights.get(0), canvas);
231             renderLight(mAssistInvocationLights.get(3), canvas);
232 
233             mPaint.setStrokeCap(Paint.Cap.BUTT);
234             renderLight(mAssistInvocationLights.get(1), canvas);
235             renderLight(mAssistInvocationLights.get(2), canvas);
236         }
237     }
238 
setLight(int index, float start, float end)239     protected void setLight(int index, float start, float end) {
240         if (index < 0 || index >= 4) {
241             Log.w(TAG, "invalid invocation light index: " + index);
242         }
243         mAssistInvocationLights.get(index).setEndpoints(start, end);
244     }
245 
246     /**
247      * Returns CornerPathRenderer to be used for rendering invocation lights.
248      *
249      * To render corners that aren't circular, override this method in a subclass.
250      */
createCornerPathRenderer(Context context)251     protected CornerPathRenderer createCornerPathRenderer(Context context) {
252         return new CircularCornerPathRenderer(context);
253     }
254 
255     /**
256      * Receives an intensity from 0 (lightest) to 1 (darkest) and sets the handle color
257      * appropriately. Intention is to match the home handle color.
258      */
updateDarkness(float darkIntensity)259     protected void updateDarkness(float darkIntensity) {
260         if (mUseNavBarColor) {
261             @ColorInt int invocationColor = (int) ArgbEvaluator.getInstance().evaluate(
262                     darkIntensity, mLightColor, mDarkColor);
263             boolean changed = true;
264             for (EdgeLight light : mAssistInvocationLights) {
265                 changed &= light.setColor(invocationColor);
266             }
267             if (changed) {
268                 invalidate();
269             }
270         }
271     }
272 
renderLight(EdgeLight light, Canvas canvas)273     private void renderLight(EdgeLight light, Canvas canvas) {
274         if (light.getLength() > 0) {
275             mGuide.strokeSegment(mPath, light.getStart(), light.getStart() + light.getLength());
276             mPaint.setColor(light.getColor());
277             canvas.drawPath(mPath, mPaint);
278         }
279     }
280 
attemptRegisterNavBarListener()281     private void attemptRegisterNavBarListener() {
282         if (!mRegistered) {
283             if (mNavigationBarController == null) {
284                 return;
285             }
286 
287             NavigationBar navBar = mNavigationBarController.getDefaultNavigationBar();
288             if (navBar == null) {
289                 return;
290             }
291 
292             updateDarkness(navBar.getBarTransitions().addDarkIntensityListener(this));
293             mRegistered = true;
294         }
295     }
296 
attemptUnregisterNavBarListener()297     private void attemptUnregisterNavBarListener() {
298         if (mRegistered) {
299             if (mNavigationBarController == null) {
300                 return;
301             }
302 
303             NavigationBar navBar = mNavigationBarController.getDefaultNavigationBar();
304             if (navBar == null) {
305                 return;
306             }
307 
308             navBar.getBarTransitions().removeDarkIntensityListener(this);
309             mRegistered = false;
310         }
311     }
312 
setNavigationBarController(NavigationBarController navigationBarController)313     public void setNavigationBarController(NavigationBarController navigationBarController) {
314         mNavigationBarController = navigationBarController;
315     }
316 }
317