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 // 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, lightLength); 144 setLight(1, leftStart + lightLength, lightLength); 145 setLight(2, rightStart - (lightLength * 2), lightLength); 146 setLight(3, rightStart - lightLength, lightLength); 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.setLength(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 offset, float length)238 protected void setLight(int index, float offset, float length) { 239 if (index < 0 || index >= 4) { 240 Log.w(TAG, "invalid invocation light index: " + index); 241 } 242 mAssistInvocationLights.get(index).setOffset(offset); 243 mAssistInvocationLights.get(index).setLength(length); 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 for (EdgeLight light : mAssistInvocationLights) { 264 light.setColor(invocationColor); 265 } 266 invalidate(); 267 } 268 } 269 renderLight(EdgeLight light, Canvas canvas)270 private void renderLight(EdgeLight light, Canvas canvas) { 271 mGuide.strokeSegment(mPath, light.getOffset(), light.getOffset() + light.getLength()); 272 mPaint.setColor(light.getColor()); 273 canvas.drawPath(mPath, mPaint); 274 } 275 attemptRegisterNavBarListener()276 private void attemptRegisterNavBarListener() { 277 if (!mRegistered) { 278 NavigationBarController controller = Dependency.get(NavigationBarController.class); 279 if (controller == null) { 280 return; 281 } 282 283 NavigationBarFragment navBar = controller.getDefaultNavigationBarFragment(); 284 if (navBar == null) { 285 return; 286 } 287 288 updateDarkness(navBar.getBarTransitions().addDarkIntensityListener(this)); 289 mRegistered = true; 290 } 291 } 292 attemptUnregisterNavBarListener()293 private void attemptUnregisterNavBarListener() { 294 if (mRegistered) { 295 NavigationBarController controller = Dependency.get(NavigationBarController.class); 296 if (controller == null) { 297 return; 298 } 299 300 NavigationBarFragment navBar = controller.getDefaultNavigationBarFragment(); 301 if (navBar == null) { 302 return; 303 } 304 305 navBar.getBarTransitions().removeDarkIntensityListener(this); 306 mRegistered = false; 307 } 308 } 309 } 310