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