1 /* 2 * Copyright (C) 2021 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.permissioncontroller.permission.ui.handheld.v31; 18 19 import android.content.Context; 20 import android.util.AttributeSet; 21 import android.view.View; 22 import android.view.WindowManager; 23 import android.view.WindowMetrics; 24 import android.widget.RelativeLayout; 25 import android.widget.TextView; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 /** 31 * Encapsulates a {@link CompositeCircleView}, labeling each of its colored partial circles. 32 */ 33 public class CompositeCircleViewLabeler extends RelativeLayout { 34 35 private int mCircleId; 36 private TextView mCenterLabel; 37 private TextView[] mLabels; 38 private float mLabelRadiusScalar; 39 CompositeCircleViewLabeler(@onNull Context context)40 public CompositeCircleViewLabeler(@NonNull Context context) { 41 super(context); 42 } 43 CompositeCircleViewLabeler(@onNull Context context, @Nullable AttributeSet attrs)44 public CompositeCircleViewLabeler(@NonNull Context context, @Nullable AttributeSet attrs) { 45 super(context, attrs); 46 } 47 CompositeCircleViewLabeler(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)48 public CompositeCircleViewLabeler(@NonNull Context context, @Nullable AttributeSet attrs, 49 int defStyleAttr) { 50 super(context, attrs, defStyleAttr); 51 } 52 CompositeCircleViewLabeler(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)53 public CompositeCircleViewLabeler(@NonNull Context context, @Nullable AttributeSet attrs, 54 int defStyleAttr, int defStyleRes) { 55 super(context, attrs, defStyleAttr, defStyleRes); 56 } 57 58 /** 59 * Sets labels to surround the contained {@link CompositeCircleView} with, and the radius 60 * scalar to place them at. 61 * 62 * @param circleId view ID of the circle being labeled 63 * @param centerLabel the center label 64 * @param labels labels labels to position next to each circle value segment 65 * @param labelRadiusScalar scalar to multiply the contained circle radius by to get the 66 * radius at which we want to show labels 67 */ configure(int circleId, TextView centerLabel, TextView[] labels, float labelRadiusScalar)68 public void configure(int circleId, TextView centerLabel, TextView[] labels, 69 float labelRadiusScalar) { 70 // Remove previous text content first. 71 for (int i = 0; i < getChildCount(); i++) { 72 if (getChildAt(i) instanceof TextView) { 73 removeViewAt(i); 74 i--; 75 } 76 } 77 mCircleId = circleId; 78 mCenterLabel = centerLabel; 79 if (centerLabel != null) { 80 addView(centerLabel); 81 } 82 mLabels = labels; 83 for (int i = 0; i < labels.length; i++) { 84 if (labels[i] != null) { 85 addView(labels[i]); 86 } 87 } 88 mLabelRadiusScalar = labelRadiusScalar; 89 } 90 91 @Override onLayout(boolean changed, int l, int t, int r, int b)92 protected void onLayout(boolean changed, int l, int t, int r, int b) { 93 super.onLayout(changed, l, t, r, b); 94 95 // Gather CCV geometry. 96 CompositeCircleView ccv = findViewById(mCircleId); 97 int ccvWidth = ccv.getWidth(); 98 int ccvHeight = ccv.getHeight(); 99 float ccvCenterX = ccv.getX() + (ccvWidth * 0.5f); 100 float ccvCenterY = ccv.getY() + (ccvHeight * 0.5f); 101 float ccvRadius = Math.min(ccvWidth, ccvHeight) * 0.5f; 102 float labelRadius = ccvRadius * mLabelRadiusScalar; 103 int centerLabelX = (int) (ccvCenterX - (mCenterLabel.getWidth() * 0.5f)); 104 int centerLabelY = (int) (ccvCenterY - (mCenterLabel.getHeight() * 0.5f)); 105 106 // Position center label. 107 if (mCenterLabel != null) { 108 mCenterLabel.setX(centerLabelX); 109 mCenterLabel.setY(centerLabelY); 110 } 111 112 // For each provided label, determine position angle. 113 for (int i = 0; i < mLabels.length; i++) { 114 TextView label = mLabels[i]; 115 if (label == null) { 116 continue; 117 } 118 label.setVisibility((ccv.getValue(i) > 0) ? View.VISIBLE : View.GONE); 119 label.measure(0, 0); 120 int width = label.getMeasuredWidth(); 121 int height = label.getMeasuredHeight(); 122 123 // For circle path, top angle is 270d. Convert to unit circle rads. 124 double angle = Math.toRadians(360 - ccv.getPartialCircleCenterAngle(i)); 125 double x = ccvCenterX + (Math.cos(angle) * labelRadius); 126 double y = ccvCenterY - (Math.sin(angle) * labelRadius); 127 128 // Determine anchor corner for text, adjust accordingly. 129 if (angle < (Math.PI * 0.5d)) { 130 y -= height; 131 } else if (angle < Math.PI) { 132 x -= width; 133 y -= height; 134 } else if (angle < (Math.PI * 1.5d)) { 135 x -= width; 136 } 137 WindowManager wm = getContext().getSystemService(WindowManager.class); 138 WindowMetrics metrics = wm.getCurrentWindowMetrics(); 139 int maxX = metrics.getBounds().right; 140 141 double offset = 0; 142 if (x < 0) { 143 x = 0; 144 } else if ((x + width) > maxX) { 145 offset = x + width - maxX; 146 x -= offset; 147 } 148 149 double labelMinX = x; 150 double labelMaxX = x + width; 151 double labelMinY = y; 152 double labelMaxY = y + height; 153 double centerLabelMinX = centerLabelX; 154 double centerLabelMaxX = centerLabelX + mCenterLabel.getWidth(); 155 double centerLabelMinY = centerLabelY; 156 double centerLabelMaxY = centerLabelY + mCenterLabel.getHeight(); 157 158 if (isOverlapping(labelMinX, labelMaxX, labelMinY, labelMaxY, 159 centerLabelMinX, centerLabelMaxX, centerLabelMinY, centerLabelMaxY)) { 160 if (shouldMoveLabelUp(labelMinY, labelMaxY, centerLabelMinY, centerLabelMaxY)) { 161 y += centerLabelMaxY - labelMinY; 162 } else { 163 y -= labelMaxY - centerLabelMinY; 164 } 165 } 166 167 label.setX((int) x); 168 label.setY((int) y); 169 } 170 } 171 172 /** 173 * Given the minimum and maximum X and Y values of the label and center label, 174 * determine whether they overlap. 175 * @return whether the label overlaps with the center label 176 */ isOverlapping( double labelMinX, double labelMaxX, double labelMinY, double labelMaxY, double centerLabelMinX, double centerLabelMaxX, double centerLabelMinY, double centerLabelMaxY)177 private boolean isOverlapping( 178 double labelMinX, double labelMaxX, double labelMinY, double labelMaxY, 179 double centerLabelMinX, double centerLabelMaxX, 180 double centerLabelMinY, double centerLabelMaxY) { 181 // If they overlap, the condition inside the parentheses will not be true 182 return !(labelMinY > centerLabelMaxY || labelMaxY < centerLabelMinY 183 || labelMinX > centerLabelMaxX || labelMaxX < centerLabelMinX); 184 } 185 186 /** 187 * Determines the minimum distance to move the label along the Y axis in order to make it 188 * not overlap with the center label. Up means the positive direction in Java. 189 * @return whether we should move the label up 190 */ shouldMoveLabelUp( double labelMinY, double labelMaxY, double centerLabelMinY, double centerLabelMaxY)191 private boolean shouldMoveLabelUp( 192 double labelMinY, double labelMaxY, double centerLabelMinY, double centerLabelMaxY) { 193 // this returns (the distance to move the label up) < (the distance to move the label down) 194 return centerLabelMaxY - labelMinY < labelMaxY - centerLabelMinY; 195 } 196 } 197