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