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