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