| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.assist.ui; |
| |
| import android.animation.ArgbEvaluator; |
| import android.annotation.ColorInt; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.MathUtils; |
| import android.view.ContextThemeWrapper; |
| import android.view.View; |
| |
| import com.android.settingslib.Utils; |
| import com.android.systemui.Dependency; |
| import com.android.systemui.R; |
| import com.android.systemui.statusbar.NavigationBarController; |
| import com.android.systemui.statusbar.phone.NavigationBarFragment; |
| import com.android.systemui.statusbar.phone.NavigationBarTransitions; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Shows lights at the bottom of the phone, marking the invocation progress. |
| */ |
| public class InvocationLightsView extends View |
| implements NavigationBarTransitions.DarkIntensityListener { |
| |
| private static final String TAG = "InvocationLightsView"; |
| |
| private static final int LIGHT_HEIGHT_DP = 3; |
| // minimum light length as a fraction of the corner length |
| private static final float MINIMUM_CORNER_RATIO = .6f; |
| |
| protected final ArrayList<EdgeLight> mAssistInvocationLights = new ArrayList<>(); |
| protected final PerimeterPathGuide mGuide; |
| |
| private final Paint mPaint = new Paint(); |
| // Path used to render lights. One instance is used to draw all lights and is cached to avoid |
| // allocation on each frame. |
| private final Path mPath = new Path(); |
| private final int mViewHeight; |
| private final int mStrokeWidth; |
| @ColorInt |
| private final int mLightColor; |
| @ColorInt |
| private final int mDarkColor; |
| |
| // Allocate variable for screen location lookup to avoid memory alloc onDraw() |
| private int[] mScreenLocation = new int[2]; |
| private boolean mRegistered = false; |
| private boolean mUseNavBarColor = true; |
| |
| public InvocationLightsView(Context context) { |
| this(context, null); |
| } |
| |
| public InvocationLightsView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| |
| mStrokeWidth = DisplayUtils.convertDpToPx(LIGHT_HEIGHT_DP, context); |
| mPaint.setStrokeWidth(mStrokeWidth); |
| mPaint.setStyle(Paint.Style.STROKE); |
| mPaint.setStrokeJoin(Paint.Join.MITER); |
| mPaint.setAntiAlias(true); |
| |
| |
| int displayWidth = DisplayUtils.getWidth(context); |
| int displayHeight = DisplayUtils.getHeight(context); |
| mGuide = new PerimeterPathGuide(context, createCornerPathRenderer(context), |
| mStrokeWidth / 2, displayWidth, displayHeight); |
| |
| int cornerRadiusBottom = DisplayUtils.getCornerRadiusBottom(context); |
| int cornerRadiusTop = DisplayUtils.getCornerRadiusTop(context); |
| mViewHeight = Math.max(cornerRadiusBottom, cornerRadiusTop); |
| |
| final int dualToneDarkTheme = Utils.getThemeAttr(mContext, R.attr.darkIconTheme); |
| final int dualToneLightTheme = Utils.getThemeAttr(mContext, R.attr.lightIconTheme); |
| Context lightContext = new ContextThemeWrapper(mContext, dualToneLightTheme); |
| Context darkContext = new ContextThemeWrapper(mContext, dualToneDarkTheme); |
| mLightColor = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor); |
| mDarkColor = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor); |
| |
| for (int i = 0; i < 4; i++) { |
| mAssistInvocationLights.add(new EdgeLight(Color.TRANSPARENT, 0, 0)); |
| } |
| } |
| |
| /** |
| * Updates positions of the invocation lights based on the progress (a float between 0 and 1). |
| * The lights begin at the device corners and expand inward until they meet at the center. |
| */ |
| public void onInvocationProgress(float progress) { |
| if (progress == 0) { |
| setVisibility(View.GONE); |
| } else { |
| attemptRegisterNavBarListener(); |
| |
| float cornerLengthNormalized = |
| mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM_LEFT); |
| float arcLengthNormalized = cornerLengthNormalized * MINIMUM_CORNER_RATIO; |
| float arcOffsetNormalized = (cornerLengthNormalized - arcLengthNormalized) / 2f; |
| |
| float minLightLength = 0; |
| float maxLightLength = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) / 4f; |
| |
| float lightLength = MathUtils.lerp(minLightLength, maxLightLength, progress); |
| |
| float leftStart = (-cornerLengthNormalized + arcOffsetNormalized) * (1 - progress); |
| float rightStart = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) |
| + (cornerLengthNormalized - arcOffsetNormalized) * (1 - progress); |
| |
| setLight(0, leftStart, lightLength); |
| setLight(1, leftStart + lightLength, lightLength); |
| setLight(2, rightStart - (lightLength * 2), lightLength); |
| setLight(3, rightStart - lightLength, lightLength); |
| setVisibility(View.VISIBLE); |
| } |
| invalidate(); |
| } |
| |
| /** |
| * Hides and resets the invocation lights. |
| */ |
| public void hide() { |
| setVisibility(GONE); |
| for (EdgeLight light : mAssistInvocationLights) { |
| light.setLength(0); |
| } |
| attemptUnregisterNavBarListener(); |
| } |
| |
| /** |
| * Sets all invocation lights to a single color. If color is null, uses the navigation bar |
| * color (updated when the nav bar color changes). |
| */ |
| public void setColors(@Nullable @ColorInt Integer color) { |
| if (color == null) { |
| mUseNavBarColor = true; |
| mPaint.setStrokeCap(Paint.Cap.BUTT); |
| attemptRegisterNavBarListener(); |
| } else { |
| setColors(color, color, color, color); |
| } |
| } |
| |
| /** |
| * Sets the invocation light colors, from left to right. |
| */ |
| public void setColors(@ColorInt int color1, @ColorInt int color2, |
| @ColorInt int color3, @ColorInt int color4) { |
| mUseNavBarColor = false; |
| attemptUnregisterNavBarListener(); |
| mAssistInvocationLights.get(0).setColor(color1); |
| mAssistInvocationLights.get(1).setColor(color2); |
| mAssistInvocationLights.get(2).setColor(color3); |
| mAssistInvocationLights.get(3).setColor(color4); |
| } |
| |
| /** |
| * Reacts to changes in the navigation bar color |
| * |
| * @param darkIntensity 0 is the lightest color, 1 is the darkest. |
| */ |
| @Override // NavigationBarTransitions.DarkIntensityListener |
| public void onDarkIntensity(float darkIntensity) { |
| updateDarkness(darkIntensity); |
| } |
| |
| |
| @Override |
| protected void onFinishInflate() { |
| getLayoutParams().height = mViewHeight; |
| requestLayout(); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| super.onLayout(changed, left, top, right, bottom); |
| |
| int rotation = getContext().getDisplay().getRotation(); |
| mGuide.setRotation(rotation); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| // If the view doesn't take up the whole screen, offset the canvas by its translation |
| // distance such that PerimeterPathGuide's paths are drawn properly based upon the actual |
| // screen edges. |
| getLocationOnScreen(mScreenLocation); |
| canvas.translate(-mScreenLocation[0], -mScreenLocation[1]); |
| |
| if (mUseNavBarColor) { |
| for (EdgeLight light : mAssistInvocationLights) { |
| renderLight(light, canvas); |
| } |
| } else { |
| mPaint.setStrokeCap(Paint.Cap.ROUND); |
| renderLight(mAssistInvocationLights.get(0), canvas); |
| renderLight(mAssistInvocationLights.get(3), canvas); |
| |
| mPaint.setStrokeCap(Paint.Cap.BUTT); |
| renderLight(mAssistInvocationLights.get(1), canvas); |
| renderLight(mAssistInvocationLights.get(2), canvas); |
| } |
| } |
| |
| protected void setLight(int index, float offset, float length) { |
| if (index < 0 || index >= 4) { |
| Log.w(TAG, "invalid invocation light index: " + index); |
| } |
| mAssistInvocationLights.get(index).setOffset(offset); |
| mAssistInvocationLights.get(index).setLength(length); |
| } |
| |
| /** |
| * Returns CornerPathRenderer to be used for rendering invocation lights. |
| * |
| * To render corners that aren't circular, override this method in a subclass. |
| */ |
| protected CornerPathRenderer createCornerPathRenderer(Context context) { |
| return new CircularCornerPathRenderer(context); |
| } |
| |
| /** |
| * Receives an intensity from 0 (lightest) to 1 (darkest) and sets the handle color |
| * appropriately. Intention is to match the home handle color. |
| */ |
| protected void updateDarkness(float darkIntensity) { |
| if (mUseNavBarColor) { |
| @ColorInt int invocationColor = (int) ArgbEvaluator.getInstance().evaluate( |
| darkIntensity, mLightColor, mDarkColor); |
| for (EdgeLight light : mAssistInvocationLights) { |
| light.setColor(invocationColor); |
| } |
| invalidate(); |
| } |
| } |
| |
| private void renderLight(EdgeLight light, Canvas canvas) { |
| mGuide.strokeSegment(mPath, light.getOffset(), light.getOffset() + light.getLength()); |
| mPaint.setColor(light.getColor()); |
| canvas.drawPath(mPath, mPaint); |
| } |
| |
| private void attemptRegisterNavBarListener() { |
| if (!mRegistered) { |
| NavigationBarController controller = Dependency.get(NavigationBarController.class); |
| if (controller == null) { |
| return; |
| } |
| |
| NavigationBarFragment navBar = controller.getDefaultNavigationBarFragment(); |
| if (navBar == null) { |
| return; |
| } |
| |
| updateDarkness(navBar.getBarTransitions().addDarkIntensityListener(this)); |
| mRegistered = true; |
| } |
| } |
| |
| private void attemptUnregisterNavBarListener() { |
| if (mRegistered) { |
| NavigationBarController controller = Dependency.get(NavigationBarController.class); |
| if (controller == null) { |
| return; |
| } |
| |
| NavigationBarFragment navBar = controller.getDefaultNavigationBarFragment(); |
| if (navBar == null) { |
| return; |
| } |
| |
| navBar.getBarTransitions().removeDarkIntensityListener(this); |
| mRegistered = false; |
| } |
| } |
| } |