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