blob: 6eda8d3021284130c2afcac0b1dc3d8c73f68263 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.permissioncontroller.permission.ui.handheld.dashboard;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Configured to draw a set of contiguous partial circles via {@link PartialCircleView}, which
* are generated from the relative weight of values and corresponding colors given to
* {@link #configure(float, int[], int[], int)}.
*/
public class CompositeCircleView extends FrameLayout {
/** Spacing between circle segments in degrees. */
private static final int SEGMENT_ANGLE_SPACING_DEG = 2;
/** How far apart to bump labels so that they have more space. */
private static final float LABEL_BUMP_DEGREES = 15;
/** Values being represented by this circle. */
private int[] mValues;
/**
* Angles toward the middle of each colored partial circle, calculated in
* {@link #configure(float, int[], int[], int)}. Can be used to position text relative to the
* partial circles, by index.
*/
private float[] mPartialCircleCenterAngles;
public CompositeCircleView(@NonNull Context context) {
super(context);
}
public CompositeCircleView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CompositeCircleView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CompositeCircleView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* Configures the {@link CompositeCircleView} to draw a set of contiguous partial circles that
* are generated from the relative weight of the given values and corresponding colors. The
* first segment starts at the top, and drawing proceeds clockwise from there.
*
* @param startAngle the angle at which to start segments
* @param values relative weights, used to size the partial circles
* @param colors colors corresponding to relative weights
* @param strokeWidth stroke width to apply to all contained partial circles
*/
public void configure(float startAngle, int[] values, int[] colors, int strokeWidth) {
removeAllViews();
mValues = values;
// Get total values and number of values over 0.
float total = 0;
int numValidValues = 0;
for (int i = 0; i < values.length; i++) {
total += values[i];
if (values[i] > 0) {
numValidValues++;
}
}
// Add small spacing to the first angle to make the little space between segments, but only
// if we have more than one segment.
if (values.length > 1) {
startAngle = startAngle + (SEGMENT_ANGLE_SPACING_DEG * 0.5f);
}
mPartialCircleCenterAngles = new float[values.length];
// Number of degrees allocated to drawing circle segments.
float allocatedDegrees = 360;
if (values.length > 1) {
allocatedDegrees -= (numValidValues * SEGMENT_ANGLE_SPACING_DEG);
}
// Total label bump degrees so far.
float totalBumpDegrees = 0;
int labelBumps = 0;
for (int i = 0; i < values.length; i++) {
if (values[i] <= 0) {
continue;
}
PartialCircleView pcv = new PartialCircleView(getContext());
addView(pcv);
pcv.setStartAngle(startAngle);
pcv.setColor(colors[i]);
pcv.setStrokeWidth(strokeWidth);
// Calculate sweep, which is (value / total) * 360, keep track of segment center
// angles for later reference.
float sweepAngle = (values[i] / total) * allocatedDegrees;
pcv.setSweepAngle(sweepAngle);
mPartialCircleCenterAngles[i] = (startAngle + (sweepAngle * 0.5f)) % 360;
if (i > 0) {
float angleDiff =
((mPartialCircleCenterAngles[i] - mPartialCircleCenterAngles[i - 1])
+ 360) % 360;
if (angleDiff < LABEL_BUMP_DEGREES) {
float bump = LABEL_BUMP_DEGREES - angleDiff;
mPartialCircleCenterAngles[i] += bump;
totalBumpDegrees += bump;
labelBumps++;
} else {
spreadPreviousLabelBumps(labelBumps, totalBumpDegrees, i);
totalBumpDegrees = 0;
labelBumps = 0;
}
}
// Move to next segment.
startAngle += sweepAngle;
startAngle += SEGMENT_ANGLE_SPACING_DEG;
startAngle %= 360;
}
// If any label bumps remaining, spread now.
spreadPreviousLabelBumps(labelBumps, totalBumpDegrees, values.length);
}
/**
* If we've been bumping labels further from previous labels to make space, we use this method
* to spread the bumps back along the circle, so that labels are as close as possible to their
* corresponding segments.
*
* @param labelBumps total number of previous segments under the size threshold
* @param totalBumpDegrees the total degrees to spread along previous labels
* @param behindIndex the index behind which we were bumping labels
*/
private void spreadPreviousLabelBumps(int labelBumps, float totalBumpDegrees, int behindIndex) {
if (labelBumps > 0) {
float spread = totalBumpDegrees * 0.5f;
for (int i = 1; i <= labelBumps + 1; i++) {
int index = behindIndex - i;
float angle = mPartialCircleCenterAngles[index];
angle -= spread;
angle += 360;
angle %= 360;
mPartialCircleCenterAngles[index] = angle;
}
}
}
/** Returns the value for the given index. */
public int getValue(int index) {
return mValues[index];
}
/** Returns the center angle for the given partial circle index. */
public float getPartialCircleCenterAngle(int index) {
return mPartialCircleCenterAngles[index];
}
}