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.statusbar.NavigationBarController; 37 import com.android.systemui.statusbar.phone.NavigationBarFragment; 38 import com.android.systemui.statusbar.phone.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 mViewHeight = Math.max(cornerRadiusBottom, cornerRadiusTop); 104 105 final int dualToneDarkTheme = Utils.getThemeAttr(mContext, R.attr.darkIconTheme); 106 final int dualToneLightTheme = Utils.getThemeAttr(mContext, R.attr.lightIconTheme); 107 Context lightContext = new ContextThemeWrapper(mContext, dualToneLightTheme); 108 Context darkContext = new ContextThemeWrapper(mContext, dualToneDarkTheme); 109 mLightColor = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor); 110 mDarkColor = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor); 111 112 for (int i = 0; i < 4; i++) { 113 mAssistInvocationLights.add(new EdgeLight(Color.TRANSPARENT, 0, 0)); 114 } 115 } 116 117 /** 118 * Updates positions of the invocation lights based on the progress (a float between 0 and 1). 119 * The lights begin at the device corners and expand inward until they meet at the center. 120 */ onInvocationProgress(float progress)121 public void onInvocationProgress(float progress) { 122 if (progress == 0) { 123 setVisibility(View.GONE); 124 } else { 125 attemptRegisterNavBarListener(); 126 127 float cornerLengthNormalized = 128 mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM_LEFT); 129 float arcLengthNormalized = cornerLengthNormalized * MINIMUM_CORNER_RATIO; 130 float arcOffsetNormalized = (cornerLengthNormalized - arcLengthNormalized) / 2f; 131 132 float minLightLength = arcLengthNormalized / 2; 133 float maxLightLength = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) / 4f; 134 135 float lightLength = MathUtils.lerp(minLightLength, maxLightLength, progress); 136 137 float leftStart = (-cornerLengthNormalized + arcOffsetNormalized) * (1 - progress); 138 float rightStart = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) 139 + (cornerLengthNormalized - arcOffsetNormalized) * (1 - progress); 140 141 setLight(0, leftStart, lightLength); 142 setLight(1, leftStart + lightLength, lightLength); 143 setLight(2, rightStart - (lightLength * 2), lightLength); 144 setLight(3, rightStart - lightLength, lightLength); 145 setVisibility(View.VISIBLE); 146 } 147 invalidate(); 148 } 149 150 /** 151 * Hides and resets the invocation lights. 152 */ hide()153 public void hide() { 154 setVisibility(GONE); 155 for (EdgeLight light : mAssistInvocationLights) { 156 light.setLength(0); 157 } 158 attemptUnregisterNavBarListener(); 159 } 160 161 /** 162 * Sets all invocation lights to a single color. If color is null, uses the navigation bar 163 * color (updated when the nav bar color changes). 164 */ setColors(@ullable @olorInt Integer color)165 public void setColors(@Nullable @ColorInt Integer color) { 166 if (color == null) { 167 mUseNavBarColor = true; 168 mPaint.setStrokeCap(Paint.Cap.BUTT); 169 attemptRegisterNavBarListener(); 170 } else { 171 setColors(color, color, color, color); 172 } 173 } 174 175 /** 176 * Sets the invocation light colors, from left to right. 177 */ setColors(@olorInt int color1, @ColorInt int color2, @ColorInt int color3, @ColorInt int color4)178 public void setColors(@ColorInt int color1, @ColorInt int color2, 179 @ColorInt int color3, @ColorInt int color4) { 180 mUseNavBarColor = false; 181 attemptUnregisterNavBarListener(); 182 mAssistInvocationLights.get(0).setColor(color1); 183 mAssistInvocationLights.get(1).setColor(color2); 184 mAssistInvocationLights.get(2).setColor(color3); 185 mAssistInvocationLights.get(3).setColor(color4); 186 } 187 188 /** 189 * Reacts to changes in the navigation bar color 190 * 191 * @param darkIntensity 0 is the lightest color, 1 is the darkest. 192 */ 193 @Override // NavigationBarTransitions.DarkIntensityListener onDarkIntensity(float darkIntensity)194 public void onDarkIntensity(float darkIntensity) { 195 updateDarkness(darkIntensity); 196 } 197 198 199 @Override onFinishInflate()200 protected void onFinishInflate() { 201 getLayoutParams().height = mViewHeight; 202 requestLayout(); 203 } 204 205 @Override onLayout(boolean changed, int left, int top, int right, int bottom)206 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 207 super.onLayout(changed, left, top, right, bottom); 208 209 int rotation = getContext().getDisplay().getRotation(); 210 mGuide.setRotation(rotation); 211 } 212 213 @Override onDraw(Canvas canvas)214 protected void onDraw(Canvas canvas) { 215 // If the view doesn't take up the whole screen, offset the canvas by its translation 216 // distance such that PerimeterPathGuide's paths are drawn properly based upon the actual 217 // screen edges. 218 getLocationOnScreen(mScreenLocation); 219 canvas.translate(-mScreenLocation[0], -mScreenLocation[1]); 220 221 if (mUseNavBarColor) { 222 for (EdgeLight light : mAssistInvocationLights) { 223 renderLight(light, canvas); 224 } 225 } else { 226 mPaint.setStrokeCap(Paint.Cap.ROUND); 227 renderLight(mAssistInvocationLights.get(0), canvas); 228 renderLight(mAssistInvocationLights.get(3), canvas); 229 230 mPaint.setStrokeCap(Paint.Cap.BUTT); 231 renderLight(mAssistInvocationLights.get(1), canvas); 232 renderLight(mAssistInvocationLights.get(2), canvas); 233 } 234 } 235 setLight(int index, float offset, float length)236 protected void setLight(int index, float offset, float length) { 237 if (index < 0 || index >= 4) { 238 Log.w(TAG, "invalid invocation light index: " + index); 239 } 240 mAssistInvocationLights.get(index).setOffset(offset); 241 mAssistInvocationLights.get(index).setLength(length); 242 } 243 244 /** 245 * Returns CornerPathRenderer to be used for rendering invocation lights. 246 * 247 * To render corners that aren't circular, override this method in a subclass. 248 */ createCornerPathRenderer(Context context)249 protected CornerPathRenderer createCornerPathRenderer(Context context) { 250 int displayWidth = DisplayUtils.getWidth(context); 251 int displayHeight = DisplayUtils.getHeight(context); 252 int cornerRadiusBottom = DisplayUtils.getCornerRadiusBottom(context); 253 int cornerRadiusTop = DisplayUtils.getCornerRadiusTop(context); 254 return new CircularCornerPathRenderer( 255 cornerRadiusBottom, cornerRadiusTop, displayWidth, displayHeight); 256 } 257 /** 258 * Receives an intensity from 0 (lightest) to 1 (darkest) and sets the handle color 259 * appropriately. Intention is to match the home handle color. 260 */ updateDarkness(float darkIntensity)261 protected void updateDarkness(float darkIntensity) { 262 if (mUseNavBarColor) { 263 @ColorInt int invocationColor = (int) ArgbEvaluator.getInstance().evaluate( 264 darkIntensity, mLightColor, mDarkColor); 265 for (EdgeLight light : mAssistInvocationLights) { 266 light.setColor(invocationColor); 267 } 268 invalidate(); 269 } 270 } 271 renderLight(EdgeLight light, Canvas canvas)272 private void renderLight(EdgeLight light, Canvas canvas) { 273 mGuide.strokeSegment(mPath, light.getOffset(), light.getOffset() + light.getLength()); 274 mPaint.setColor(light.getColor()); 275 canvas.drawPath(mPath, mPaint); 276 } 277 attemptRegisterNavBarListener()278 private void attemptRegisterNavBarListener() { 279 if (!mRegistered) { 280 NavigationBarController controller = Dependency.get(NavigationBarController.class); 281 if (controller == null) { 282 return; 283 } 284 285 NavigationBarFragment navBar = controller.getDefaultNavigationBarFragment(); 286 if (navBar == null) { 287 return; 288 } 289 290 updateDarkness(navBar.getBarTransitions().addDarkIntensityListener(this)); 291 mRegistered = true; 292 } 293 } 294 attemptUnregisterNavBarListener()295 private void attemptUnregisterNavBarListener() { 296 if (mRegistered) { 297 NavigationBarController controller = Dependency.get(NavigationBarController.class); 298 if (controller == null) { 299 return; 300 } 301 302 NavigationBarFragment navBar = controller.getDefaultNavigationBarFragment(); 303 if (navBar == null) { 304 return; 305 } 306 307 navBar.getBarTransitions().removeDarkIntensityListener(this); 308 mRegistered = false; 309 } 310 } 311 } 312