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.widget.FrameLayout; 22 import android.widget.TextView; 23 24 import androidx.annotation.NonNull; 25 import androidx.annotation.Nullable; 26 27 import com.android.permissioncontroller.R; 28 29 /** 30 * Configured to draw a set of contiguous partial circles via {@link PartialCircleView}, which 31 * are generated from the relative weight of values and corresponding colors given to 32 * {@link #configure(float, int[], int[], int)}. 33 */ 34 public class CompositeCircleView extends FrameLayout { 35 36 /** Spacing between circle segments in degrees. */ 37 private static final int SEGMENT_ANGLE_SPACING_DEG = 2; 38 39 /** How far apart to bump labels so that they have more space. */ 40 private static final float LABEL_BUMP_DEGREES = 15; 41 42 /** Values being represented by this circle. */ 43 private int[] mValues; 44 45 /** 46 * Angles toward the middle of each colored partial circle, calculated in 47 * {@link #configure(float, int[], int[], int)}. Can be used to position text relative to the 48 * partial circles, by index. 49 */ 50 private float[] mPartialCircleCenterAngles; 51 CompositeCircleView(@onNull Context context)52 public CompositeCircleView(@NonNull Context context) { 53 super(context); 54 } 55 CompositeCircleView(@onNull Context context, @Nullable AttributeSet attrs)56 public CompositeCircleView(@NonNull Context context, @Nullable AttributeSet attrs) { 57 super(context, attrs); 58 } 59 CompositeCircleView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)60 public CompositeCircleView(@NonNull Context context, @Nullable AttributeSet attrs, 61 int defStyleAttr) { 62 super(context, attrs, defStyleAttr); 63 } 64 CompositeCircleView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)65 public CompositeCircleView(@NonNull Context context, @Nullable AttributeSet attrs, 66 int defStyleAttr, int defStyleRes) { 67 super(context, attrs, defStyleAttr, defStyleRes); 68 } 69 70 /** 71 * Configures the {@link CompositeCircleView} to draw a set of contiguous partial circles that 72 * are generated from the relative weight of the given values and corresponding colors. The 73 * first segment starts at the top, and drawing proceeds clockwise from there. 74 * 75 * @param startAngle the angle at which to start segments 76 * @param values relative weights, used to size the partial circles 77 * @param colors colors corresponding to relative weights 78 * @param strokeWidth stroke width to apply to all contained partial circles 79 * @param labels the permission labels to set the ContentDescription with % value 80 */ configure(float startAngle, int[] values, int[] colors, int strokeWidth, TextView[] labels)81 public void configure(float startAngle, int[] values, int[] colors, int strokeWidth, 82 TextView[] labels) { 83 removeAllViews(); 84 mValues = values; 85 86 // Get total values and number of values over 0. 87 float total = 0; 88 int numValidValues = 0; 89 for (int i = 0; i < values.length; i++) { 90 total += values[i]; 91 if (values[i] > 0) { 92 numValidValues++; 93 } 94 } 95 96 // Add small spacing to the first angle to make the little space between segments, but only 97 // if we have more than one segment. 98 if (values.length > 1) { 99 startAngle = startAngle + (SEGMENT_ANGLE_SPACING_DEG * 0.5f); 100 } 101 mPartialCircleCenterAngles = new float[values.length]; 102 103 // Number of degrees allocated to drawing circle segments. 104 float allocatedDegrees = 360; 105 if (values.length > 1) { 106 allocatedDegrees -= (numValidValues * SEGMENT_ANGLE_SPACING_DEG); 107 } 108 109 // Total label bump degrees so far. 110 float totalBumpDegrees = 0; 111 int labelBumps = 0; 112 113 for (int i = 0; i < values.length; i++) { 114 if (values[i] <= 0) { 115 continue; 116 } 117 118 PartialCircleView pcv = new PartialCircleView(getContext()); 119 addView(pcv); 120 pcv.setStartAngle(startAngle); 121 pcv.setColor(colors[i]); 122 pcv.setStrokeWidth(strokeWidth); 123 124 // Calculate sweep, which is (value / total) * 360, keep track of segment center 125 // angles for later reference. 126 float sweepAngle = (values[i] / total) * allocatedDegrees; 127 pcv.setSweepAngle(sweepAngle); 128 129 if (labels[i] != null) { 130 int percentage = Math.round((values[i] / total) * 100); 131 String contextDescription = getContext().getString( 132 R.string.privdash_usage_percent, labels[i].getText(), percentage); 133 labels[i].setContentDescription(contextDescription); 134 } 135 136 mPartialCircleCenterAngles[i] = (startAngle + (sweepAngle * 0.5f)) % 360; 137 if (i > 0) { 138 float angleDiff = 139 ((mPartialCircleCenterAngles[i] - mPartialCircleCenterAngles[i - 1]) 140 + 360) % 360; 141 if (angleDiff < LABEL_BUMP_DEGREES) { 142 float bump = LABEL_BUMP_DEGREES - angleDiff; 143 mPartialCircleCenterAngles[i] += bump; 144 totalBumpDegrees += bump; 145 labelBumps++; 146 } else { 147 spreadPreviousLabelBumps(labelBumps, totalBumpDegrees, i); 148 totalBumpDegrees = 0; 149 labelBumps = 0; 150 } 151 } 152 153 // Move to next segment. 154 startAngle += sweepAngle; 155 startAngle += SEGMENT_ANGLE_SPACING_DEG; 156 startAngle %= 360; 157 } 158 159 // If any label bumps remaining, spread now. 160 spreadPreviousLabelBumps(labelBumps, totalBumpDegrees, values.length); 161 } 162 163 /** 164 * If we've been bumping labels further from previous labels to make space, we use this method 165 * to spread the bumps back along the circle, so that labels are as close as possible to their 166 * corresponding segments. 167 * 168 * @param labelBumps total number of previous segments under the size threshold 169 * @param totalBumpDegrees the total degrees to spread along previous labels 170 * @param behindIndex the index behind which we were bumping labels 171 */ spreadPreviousLabelBumps(int labelBumps, float totalBumpDegrees, int behindIndex)172 private void spreadPreviousLabelBumps(int labelBumps, float totalBumpDegrees, int behindIndex) { 173 if (labelBumps > 0) { 174 float spread = totalBumpDegrees * 0.5f; 175 for (int i = 1; i <= labelBumps + 1; i++) { 176 int index = behindIndex - i; 177 float angle = mPartialCircleCenterAngles[index]; 178 angle -= spread; 179 angle += 360; 180 angle %= 360; 181 mPartialCircleCenterAngles[index] = angle; 182 } 183 } 184 } 185 186 /** Returns the value for the given index. */ getValue(int index)187 public int getValue(int index) { 188 return mValues[index]; 189 } 190 191 /** Returns the center angle for the given partial circle index. */ getPartialCircleCenterAngle(int index)192 public float getPartialCircleCenterAngle(int index) { 193 return mPartialCircleCenterAngles[index]; 194 } 195 } 196