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;
18 
19 import android.animation.ArgbEvaluator;
20 import android.content.Context;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.graphics.Path;
24 import android.graphics.RectF;
25 import android.util.AttributeSet;
26 import android.util.DisplayMetrics;
27 import android.view.ContextThemeWrapper;
28 import android.view.View;
29 
30 import com.android.settingslib.Utils;
31 
32 /**
33  * CornerHandleView draws an inset arc intended to be displayed within the screen decoration
34  * corners.
35  */
36 public class CornerHandleView extends View {
37     private static final float STROKE_DP_LARGE = 2f;
38     private static final float STROKE_DP_SMALL = 1.95f;
39     // Radius to use if none is available.
40     private static final int FALLBACK_RADIUS_DP = 15;
41     private static final float MARGIN_DP = 8;
42     private static final int MAX_ARC_DEGREES = 90;
43     // Arc length along the phone's perimeter used to measure the desired angle.
44     private static final float ARC_LENGTH_DP = 31f;
45 
46     private Paint mPaint;
47     private int mLightColor;
48     private int mDarkColor;
49     private Path mPath;
50     private boolean mRequiresInvalidate;
51 
CornerHandleView(Context context, AttributeSet attrs)52     public CornerHandleView(Context context, AttributeSet attrs) {
53         super(context, attrs);
54 
55         mPaint = new Paint();
56         mPaint.setAntiAlias(true);
57         mPaint.setStyle(Paint.Style.STROKE);
58         mPaint.setStrokeCap(Paint.Cap.ROUND);
59         mPaint.setStrokeWidth(getStrokePx());
60 
61         final int dualToneDarkTheme = Utils.getThemeAttr(mContext, R.attr.darkIconTheme);
62         final int dualToneLightTheme = Utils.getThemeAttr(mContext, R.attr.lightIconTheme);
63         Context lightContext = new ContextThemeWrapper(mContext, dualToneLightTheme);
64         Context darkContext = new ContextThemeWrapper(mContext, dualToneDarkTheme);
65         mLightColor = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor);
66         mDarkColor = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor);
67 
68         updatePath();
69     }
70 
71     @Override
setAlpha(float alpha)72     public void setAlpha(float alpha) {
73         super.setAlpha(alpha);
74         if (alpha > 0f && mRequiresInvalidate) {
75             mRequiresInvalidate = false;
76             invalidate();
77         }
78     }
79 
updatePath()80     private void updatePath() {
81         mPath = new Path();
82 
83         float marginPx = getMarginPx();
84         float radiusPx = getInnerRadiusPx();
85         float halfStrokePx = getStrokePx() / 2f;
86         float angle = getAngle();
87         float startAngle = 180 + ((90 - angle) / 2);
88         RectF circle = new RectF(marginPx + halfStrokePx,
89                 marginPx + halfStrokePx,
90                 marginPx + 2 * radiusPx - halfStrokePx,
91                 marginPx + 2 * radiusPx - halfStrokePx);
92 
93         if (angle >= 90f) {
94             float innerCircumferenceDp = convertPixelToDp(radiusPx * 2 * (float) Math.PI,
95                     mContext);
96             float arcDp = innerCircumferenceDp * getAngle() / 360f;
97             // Add additional "arms" to the two ends of the arc. The length computation is
98             // hand-tuned.
99             float lineLengthPx = convertDpToPixel((ARC_LENGTH_DP - arcDp - MARGIN_DP) / 2,
100                     mContext);
101 
102             mPath.moveTo(marginPx + halfStrokePx, marginPx + radiusPx + lineLengthPx);
103             mPath.lineTo(marginPx + halfStrokePx, marginPx + radiusPx);
104             mPath.arcTo(circle, startAngle, angle);
105             mPath.moveTo(marginPx + radiusPx, marginPx + halfStrokePx);
106             mPath.lineTo(marginPx + radiusPx + lineLengthPx, marginPx + halfStrokePx);
107         } else {
108             mPath.arcTo(circle, startAngle, angle);
109         }
110     }
111 
112     /**
113      * Receives an intensity from 0 (lightest) to 1 (darkest) and sets the handle color
114      * appropriately. Intention is to match the home handle color.
115      */
updateDarkness(float darkIntensity)116     public void updateDarkness(float darkIntensity) {
117         // Handle color is same as home handle color.
118         int color = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity,
119                 mLightColor, mDarkColor);
120         if (mPaint.getColor() != color) {
121             mPaint.setColor(color);
122             if (getVisibility() == VISIBLE && getAlpha() > 0) {
123                 invalidate();
124             } else {
125                 // If we are currently invisible, then invalidate when we are next made visible
126                 mRequiresInvalidate = true;
127             }
128         }
129     }
130 
131     @Override
onDraw(Canvas canvas)132     public void onDraw(Canvas canvas) {
133         super.onDraw(canvas);
134         canvas.drawPath(mPath, mPaint);
135     }
136 
convertDpToPixel(float dp, Context context)137     private static float convertDpToPixel(float dp, Context context) {
138         return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
139                 / DisplayMetrics.DENSITY_DEFAULT);
140     }
141 
convertPixelToDp(float px, Context context)142     private static float convertPixelToDp(float px, Context context) {
143         return px * DisplayMetrics.DENSITY_DEFAULT
144                 / ((float) context.getResources().getDisplayMetrics().densityDpi);
145     }
146 
getAngle()147     private float getAngle() {
148         // Measure a length of ARC_LENGTH_DP along the *screen's* perimeter, get the angle and cap
149         // it at 90.
150         float circumferenceDp = convertPixelToDp((
151                 getOuterRadiusPx()) * 2 * (float) Math.PI, mContext);
152         float angleDeg = (ARC_LENGTH_DP / circumferenceDp) * 360;
153         if (angleDeg > MAX_ARC_DEGREES) {
154             angleDeg = MAX_ARC_DEGREES;
155         }
156         return angleDeg;
157     }
158 
getMarginPx()159     private float getMarginPx() {
160         return convertDpToPixel(MARGIN_DP, mContext);
161     }
162 
getInnerRadiusPx()163     private float getInnerRadiusPx() {
164         return getOuterRadiusPx() - getMarginPx();
165     }
166 
getOuterRadiusPx()167     private float getOuterRadiusPx() {
168         // Attempt to get the bottom corner radius, otherwise fall back on the generic or top
169         // values. If none are available, use the FALLBACK_RADIUS_DP.
170         int radius = getResources().getDimensionPixelSize(
171                 com.android.systemui.R.dimen.config_rounded_mask_size_bottom);
172         if (radius == 0) {
173             radius = getResources().getDimensionPixelSize(
174                     com.android.systemui.R.dimen.config_rounded_mask_size);
175         }
176         if (radius == 0) {
177             radius = getResources().getDimensionPixelSize(
178                     com.android.systemui.R.dimen.config_rounded_mask_size_top);
179         }
180         if (radius == 0) {
181             radius = (int) convertDpToPixel(FALLBACK_RADIUS_DP, mContext);
182         }
183         return radius;
184     }
185 
getStrokePx()186     private float getStrokePx() {
187         // Use a slightly smaller stroke if we need to cover the full corner angle.
188         return convertDpToPixel((getAngle() < 90) ? STROKE_DP_LARGE : STROKE_DP_SMALL,
189                 getContext());
190     }
191 }
192