Port new version battery usage chart implementation from master to tm-qpr-dev.
This cl is a merge of the following 36 cls: ag/19250259 Duplicate BatteryChartPreferenceController and BatteryChartView into new files for better diff review purpose ag/19279660 Use Mockito 4.6.1 API for BatteryChartPreferenceControllerV2Test ag/19267975 Add class BatteryLevelData used to parcel the battery timestamps and levels. It behaves as an interface between UI and data. ag/19289086 Refactor BatteryChartView X-axis labels. Instead of only timestamps, also support any string[] labels. ag/19238586 Add interpolation for the history data since last full charge loaded from database. ag/19331746 Return raw history map in function getHistoryMapSinceLastFullCharge. ag/19308838 In BatteryChartViewV2, use levels.length-1 to replace mTrapezoidCount. So the chartview could show any number of slots as the given levels length-1. ag/19332266 Add class BatteryDiffData used to parcel battery usage data ag/19331467 Refactor Battery Chart View State Controll ag/19358207 Add DataProcessor to process raw history map for UI. ag/19332276 Add battery chart view model. ag/19394744 Update trapezoid validation in battery chart view. ag/19379730 Support daily and hourly battery chartview. ag/19428426 Improve X axis labels in battery chart (1) ag/19446215 Improve X axis labels in battery chart (2) ag/19394745 Add the async task to compute diff usage data and load labels and icons. ag/19447624 Support showing app usage list for two battery charts ag/19500907 Updates battery usage messages from last 24hr to last full charge. (Part1: V2 files) ag/19505324 Update the selected period message in battery chart ag/19500905 Updates battery usage messages from last 24hr to last full charge. (Part2: non-V2 files) ag/19510363 Update usage data for EBS app usage list and App usage detail from 24 hours to last full charge. ag/19523184 Update usage data for EBS app usage list and App usage detail from 24 hours to last full charge. ag/19534864 Add margin between battery daily and hourly charts ag/19491093 Always do interpolation for battery level data in daily chart. ag/19565630 Avoid NullPointerException when batteryLevelData is null. ag/19561239 Fix b/241872474 Battery usage page will crash when selecting the last hour chart bar, going to app detail page, and going back ag/19565633 Fix b/241885070: Unexpected texts moving when going back to battery usage page ag/19534850 New way to draw battery chart axis labels ag/19561240 Switch Battery Usage Chart from V1 to V2. ag/19561338 Switch Battery Usage Chart from V1 to V2. ag/19600174 Fix b/242254055 Battery usage initial screen improvements (long data loading time) ag/19600284 Fix b/242252080: Add padding space on the top of the battery chart ag/19647338 Consider usage map valid even if [all][all] is null. ag/19634227 Use new content uri everytime to avoid cache ag/19600177 Fix b/242009481: Refine the battery usage chart timestamp label rule ag/19647337 Fix b/242809981 Charge battery to 100% when battery usage page opened, the chart will refresh, but the app list isn't refreshed in that case. Test: manual Bug: 239491373 Bug: 236101166 Bug: 236101687 Fix: 236101166 Change-Id: I7de8d9dcee14627da10752534991f1ec9f616020 Merged-In: I9142c0d4e00dea3771777ba9aedeab07b635fa1a
This commit is contained in:
@@ -18,6 +18,7 @@ package com.android.settings.fuelgauge.batteryusage;
|
||||
import static com.android.settings.Utils.formatPercentage;
|
||||
|
||||
import static java.lang.Math.round;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||
import android.content.Context;
|
||||
@@ -29,8 +30,6 @@ import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
@@ -39,6 +38,7 @@ import android.view.View;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
|
||||
@@ -46,7 +46,7 @@ import com.android.settings.R;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.Utils;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -58,36 +58,26 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
private static final List<String> ACCESSIBILITY_SERVICE_NAMES =
|
||||
Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService");
|
||||
|
||||
private static final int DEFAULT_TRAPEZOID_COUNT = 12;
|
||||
private static final int DEFAULT_TIMESTAMP_COUNT = 4;
|
||||
private static final int TIMESTAMP_GAPS_COUNT = DEFAULT_TIMESTAMP_COUNT - 1;
|
||||
private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
|
||||
private static final long UPDATE_STATE_DELAYED_TIME = 500L;
|
||||
|
||||
/** Selects all trapezoid shapes. */
|
||||
public static final int SELECTED_INDEX_ALL = -1;
|
||||
public static final int SELECTED_INDEX_INVALID = -2;
|
||||
|
||||
/** A callback listener for selected group index is updated. */
|
||||
public interface OnSelectListener {
|
||||
/** The callback function for selected group index is updated. */
|
||||
void onSelect(int trapezoidIndex);
|
||||
}
|
||||
|
||||
private BatteryChartViewModel mViewModel;
|
||||
|
||||
private int mDividerWidth;
|
||||
private int mDividerHeight;
|
||||
private int mTrapezoidCount;
|
||||
private float mTrapezoidVOffset;
|
||||
private float mTrapezoidHOffset;
|
||||
private boolean mIsSlotsClickabled;
|
||||
private String[] mPercentages = getPercentages();
|
||||
|
||||
@VisibleForTesting
|
||||
int mHoveredIndex = SELECTED_INDEX_INVALID;
|
||||
@VisibleForTesting
|
||||
int mSelectedIndex = SELECTED_INDEX_INVALID;
|
||||
@VisibleForTesting
|
||||
String[] mTimestamps;
|
||||
int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
|
||||
// Colors for drawing the trapezoid shape and dividers.
|
||||
private int mTrapezoidColor;
|
||||
@@ -98,25 +88,26 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
private final Rect mIndent = new Rect();
|
||||
private final Rect[] mPercentageBounds =
|
||||
new Rect[]{new Rect(), new Rect(), new Rect()};
|
||||
// For drawing the timestamp information.
|
||||
private final Rect[] mTimestampsBounds =
|
||||
new Rect[]{new Rect(), new Rect(), new Rect(), new Rect()};
|
||||
// For drawing the axis label information.
|
||||
private final List<Rect> mAxisLabelsBounds = new ArrayList<>();
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
Handler mHandler = new Handler();
|
||||
@VisibleForTesting
|
||||
final Runnable mUpdateClickableStateRun = () -> updateClickableState();
|
||||
|
||||
private int[] mLevels;
|
||||
private Paint mTextPaint;
|
||||
private Paint mDividerPaint;
|
||||
private Paint mTrapezoidPaint;
|
||||
|
||||
@VisibleForTesting
|
||||
Paint mTrapezoidCurvePaint = null;
|
||||
private TrapezoidSlot[] mTrapezoidSlots;
|
||||
@VisibleForTesting
|
||||
TrapezoidSlot[] mTrapezoidSlots;
|
||||
// Records the location to calculate selected index.
|
||||
private float mTouchUpEventX = Float.MIN_VALUE;
|
||||
@VisibleForTesting
|
||||
float mTouchUpEventX = Float.MIN_VALUE;
|
||||
private BatteryChartView.OnSelectListener mOnSelectListener;
|
||||
|
||||
public BatteryChartView(Context context) {
|
||||
@@ -128,57 +119,25 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
initializeColors(context);
|
||||
// Registers the click event listener.
|
||||
setOnClickListener(this);
|
||||
setSelectedIndex(SELECTED_INDEX_ALL);
|
||||
setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
|
||||
setClickable(false);
|
||||
setLatestTimestamp(0);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/** Sets the total trapezoid count for drawing. */
|
||||
public void setTrapezoidCount(int trapezoidCount) {
|
||||
Log.i(TAG, "trapezoidCount:" + trapezoidCount);
|
||||
mTrapezoidCount = trapezoidCount;
|
||||
mTrapezoidSlots = new TrapezoidSlot[trapezoidCount];
|
||||
// Allocates the trapezoid slot array.
|
||||
for (int index = 0; index < trapezoidCount; index++) {
|
||||
mTrapezoidSlots[index] = new TrapezoidSlot();
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/** Sets all levels value to draw the trapezoid shape */
|
||||
public void setLevels(int[] levels) {
|
||||
Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length));
|
||||
if (levels == null) {
|
||||
mLevels = null;
|
||||
return;
|
||||
}
|
||||
// We should provide trapezoid count + 1 data to draw all trapezoids.
|
||||
mLevels = levels.length == mTrapezoidCount + 1 ? levels : null;
|
||||
setClickable(false);
|
||||
invalidate();
|
||||
if (mLevels == null) {
|
||||
return;
|
||||
}
|
||||
// Sets the chart is clickable if there is at least one valid item in it.
|
||||
for (int index = 0; index < mLevels.length - 1; index++) {
|
||||
if (mLevels[index] != 0 && mLevels[index + 1] != 0) {
|
||||
setClickable(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the selected group index to draw highlight effect. */
|
||||
public void setSelectedIndex(int index) {
|
||||
if (mSelectedIndex != index) {
|
||||
mSelectedIndex = index;
|
||||
/** Sets the data model of this view. */
|
||||
public void setViewModel(BatteryChartViewModel viewModel) {
|
||||
if (viewModel == null) {
|
||||
mViewModel = null;
|
||||
invalidate();
|
||||
// Callbacks to the listener if we have.
|
||||
if (mOnSelectListener != null) {
|
||||
mOnSelectListener.onSelect(mSelectedIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
|
||||
viewModel.size(), viewModel.selectedIndex()));
|
||||
mViewModel = viewModel;
|
||||
initializeAxisLabelsBounds();
|
||||
initializeTrapezoidSlots(viewModel.size() - 1);
|
||||
setClickable(hasAnyValidTrapezoid(viewModel));
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/** Sets the callback to monitor the selected group index. */
|
||||
@@ -195,29 +154,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
} else {
|
||||
mTextPaint = null;
|
||||
}
|
||||
setVisibility(View.VISIBLE);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/** Sets the latest timestamp for drawing into x-axis information. */
|
||||
public void setLatestTimestamp(long latestTimestamp) {
|
||||
if (latestTimestamp == 0) {
|
||||
latestTimestamp = Clock.systemUTC().millis();
|
||||
}
|
||||
if (mTimestamps == null) {
|
||||
mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT];
|
||||
}
|
||||
final long timeSlotOffset =
|
||||
DateUtils.HOUR_IN_MILLIS * (/*total 24 hours*/ 24 / TIMESTAMP_GAPS_COUNT);
|
||||
final boolean is24HourFormat = DateFormat.is24HourFormat(getContext());
|
||||
for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
|
||||
mTimestamps[index] =
|
||||
ConvertUtils.utcToLocalTimeHour(
|
||||
getContext(),
|
||||
latestTimestamp - (TIMESTAMP_GAPS_COUNT - index)
|
||||
* timeSlotOffset,
|
||||
is24HourFormat);
|
||||
}
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@@ -226,6 +162,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
// Measures text bounds and updates indent configuration.
|
||||
if (mTextPaint != null) {
|
||||
mTextPaint.setTextAlign(Paint.Align.LEFT);
|
||||
for (int index = 0; index < mPercentages.length; index++) {
|
||||
mTextPaint.getTextBounds(
|
||||
mPercentages[index], 0, mPercentages[index].length(),
|
||||
@@ -235,15 +172,14 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
mIndent.top = mPercentageBounds[0].height();
|
||||
mIndent.right = mPercentageBounds[0].width() + mTextPadding;
|
||||
|
||||
if (mTimestamps != null) {
|
||||
int maxHeight = 0;
|
||||
for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
|
||||
mTextPaint.getTextBounds(
|
||||
mTimestamps[index], 0, mTimestamps[index].length(),
|
||||
mTimestampsBounds[index]);
|
||||
maxHeight = Math.max(maxHeight, mTimestampsBounds[index].height());
|
||||
if (mViewModel != null) {
|
||||
int maxTop = 0;
|
||||
for (int index = 0; index < mViewModel.size(); index++) {
|
||||
final String text = mViewModel.texts().get(index);
|
||||
mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index));
|
||||
maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top);
|
||||
}
|
||||
mIndent.bottom = maxHeight + round(mTextPadding * 1.5f);
|
||||
mIndent.bottom = maxTop + round(mTextPadding * 2f);
|
||||
}
|
||||
Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
|
||||
} else {
|
||||
@@ -254,7 +190,12 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
// Before mLevels initialized, the count of trapezoids is unknown. Only draws the
|
||||
// horizontal percentages and dividers.
|
||||
drawHorizontalDividers(canvas);
|
||||
if (mViewModel == null) {
|
||||
return;
|
||||
}
|
||||
drawVerticalDividers(canvas);
|
||||
drawTrapezoids(canvas);
|
||||
}
|
||||
@@ -294,7 +235,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
public void onHoverChanged(boolean hovered) {
|
||||
super.onHoverChanged(hovered);
|
||||
if (!hovered) {
|
||||
mHoveredIndex = SELECTED_INDEX_INVALID; // reset
|
||||
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -307,15 +248,15 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
}
|
||||
final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
|
||||
// Ignores the click event if the level is zero.
|
||||
if (trapezoidIndex == SELECTED_INDEX_INVALID
|
||||
|| !isValidToDraw(trapezoidIndex)) {
|
||||
if (trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
|
||||
|| !isValidToDraw(mViewModel, trapezoidIndex)) {
|
||||
return;
|
||||
}
|
||||
// Selects all if users click the same trapezoid item two times.
|
||||
if (trapezoidIndex == mSelectedIndex) {
|
||||
setSelectedIndex(SELECTED_INDEX_ALL);
|
||||
} else {
|
||||
setSelectedIndex(trapezoidIndex);
|
||||
if (mOnSelectListener != null) {
|
||||
// Selects all if users click the same trapezoid item two times.
|
||||
mOnSelectListener.onSelect(
|
||||
trapezoidIndex == mViewModel.selectedIndex()
|
||||
? BatteryChartViewModel.SELECTED_INDEX_ALL : trapezoidIndex);
|
||||
}
|
||||
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
}
|
||||
@@ -364,8 +305,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
|
||||
} else if (mIsSlotsClickabled) {
|
||||
mTrapezoidCurvePaint = null;
|
||||
// Sets levels again to force update the click state.
|
||||
setLevels(mLevels);
|
||||
// Sets view model again to force update the click state.
|
||||
setViewModel(mViewModel);
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
@@ -380,6 +321,13 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
super.setClickable(clickable);
|
||||
}
|
||||
|
||||
private void initializeTrapezoidSlots(int count) {
|
||||
mTrapezoidSlots = new TrapezoidSlot[count];
|
||||
for (int index = 0; index < mTrapezoidSlots.length; index++) {
|
||||
mTrapezoidSlots[index] = new TrapezoidSlot();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeColors(Context context) {
|
||||
setBackgroundColor(Color.TRANSPARENT);
|
||||
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
|
||||
@@ -434,10 +382,10 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
|
||||
private void drawPercentage(Canvas canvas, int index, float offsetY) {
|
||||
if (mTextPaint != null) {
|
||||
mTextPaint.setTextAlign(Paint.Align.RIGHT);
|
||||
canvas.drawText(
|
||||
mPercentages[index],
|
||||
getWidth() - mPercentageBounds[index].width()
|
||||
- mPercentageBounds[index].left,
|
||||
getWidth(),
|
||||
offsetY + mPercentageBounds[index].height() * .5f,
|
||||
mTextPaint);
|
||||
}
|
||||
@@ -445,9 +393,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
|
||||
private void drawVerticalDividers(Canvas canvas) {
|
||||
final int width = getWidth() - mIndent.right;
|
||||
final int dividerCount = mTrapezoidCount + 1;
|
||||
final int dividerCount = mTrapezoidSlots.length + 1;
|
||||
final float dividerSpace = dividerCount * mDividerWidth;
|
||||
final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
|
||||
final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
|
||||
final float bottomY = getHeight() - mIndent.bottom;
|
||||
final float startY = bottomY - mDividerHeight;
|
||||
final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
|
||||
@@ -463,66 +411,140 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
}
|
||||
startX = nextX;
|
||||
}
|
||||
// Draws the timestamp slot information.
|
||||
if (mTimestamps != null) {
|
||||
final float[] xOffsets = new float[DEFAULT_TIMESTAMP_COUNT];
|
||||
final float baselineX = mDividerWidth * .5f;
|
||||
final float offsetX = mDividerWidth + unitWidth;
|
||||
final int slotBarOffset = (/*total 12 bars*/ 12) / TIMESTAMP_GAPS_COUNT;
|
||||
for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
|
||||
xOffsets[index] = baselineX + index * offsetX * slotBarOffset;
|
||||
// Draws the axis label slot information.
|
||||
if (mViewModel != null) {
|
||||
final float baselineY = getHeight() - mTextPadding * 1.5f;
|
||||
Rect[] axisLabelDisplayAreas;
|
||||
switch (mViewModel.axisLabelPosition()) {
|
||||
case CENTER_OF_TRAPEZOIDS:
|
||||
axisLabelDisplayAreas = getAxisLabelDisplayAreas(
|
||||
/* size= */ mViewModel.size() - 1,
|
||||
/* baselineX= */ mDividerWidth + unitWidth * .5f,
|
||||
/* offsetX= */ mDividerWidth + unitWidth,
|
||||
baselineY,
|
||||
/* shiftFirstAndLast= */ false);
|
||||
break;
|
||||
case BETWEEN_TRAPEZOIDS:
|
||||
default:
|
||||
axisLabelDisplayAreas = getAxisLabelDisplayAreas(
|
||||
/* size= */ mViewModel.size(),
|
||||
/* baselineX= */ mDividerWidth * .5f,
|
||||
/* offsetX= */ mDividerWidth + unitWidth,
|
||||
baselineY,
|
||||
/* shiftFirstAndLast= */ true);
|
||||
break;
|
||||
}
|
||||
drawTimestamp(canvas, xOffsets);
|
||||
drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawTimestamp(Canvas canvas, float[] xOffsets) {
|
||||
// Draws the 1st timestamp info.
|
||||
canvas.drawText(
|
||||
mTimestamps[0],
|
||||
xOffsets[0] - mTimestampsBounds[0].left,
|
||||
getTimestampY(0), mTextPaint);
|
||||
final int latestIndex = DEFAULT_TIMESTAMP_COUNT - 1;
|
||||
// Draws the last timestamp info.
|
||||
canvas.drawText(
|
||||
mTimestamps[latestIndex],
|
||||
xOffsets[latestIndex] - mTimestampsBounds[latestIndex].width()
|
||||
- mTimestampsBounds[latestIndex].left,
|
||||
getTimestampY(latestIndex), mTextPaint);
|
||||
// Draws the rest of timestamp info since it is located in the center.
|
||||
for (int index = 1; index <= DEFAULT_TIMESTAMP_COUNT - 2; index++) {
|
||||
canvas.drawText(
|
||||
mTimestamps[index],
|
||||
xOffsets[index]
|
||||
- (mTimestampsBounds[index].width() - mTimestampsBounds[index].left)
|
||||
* .5f,
|
||||
getTimestampY(index), mTextPaint);
|
||||
/** Gets all the axis label texts displaying area positions if they are shown. */
|
||||
private Rect[] getAxisLabelDisplayAreas(final int size, final float baselineX,
|
||||
final float offsetX, final float baselineY, final boolean shiftFirstAndLast) {
|
||||
final Rect[] result = new Rect[size];
|
||||
for (int index = 0; index < result.length; index++) {
|
||||
final float width = mAxisLabelsBounds.get(index).width();
|
||||
float middle = baselineX + index * offsetX;
|
||||
if (shiftFirstAndLast) {
|
||||
if (index == 0) {
|
||||
middle += width * .5f;
|
||||
}
|
||||
if (index == size - 1) {
|
||||
middle -= width * .5f;
|
||||
}
|
||||
}
|
||||
final float left = middle - width * .5f;
|
||||
final float right = left + width;
|
||||
final float top = baselineY + mAxisLabelsBounds.get(index).top;
|
||||
final float bottom = top + mAxisLabelsBounds.get(index).height();
|
||||
result[index] = new Rect(round(left), round(top), round(right), round(bottom));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) {
|
||||
final int lastIndex = displayAreas.length - 1;
|
||||
// Suppose first and last labels are always able to draw.
|
||||
drawAxisLabelText(canvas, 0, displayAreas[0], baselineY);
|
||||
drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY);
|
||||
drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively draws axis labels between the start index and the end index. If the inner number
|
||||
* can be exactly divided into 2 parts, check and draw the middle index label and then
|
||||
* recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two
|
||||
* labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go
|
||||
* back to the uplevel of the recursion.
|
||||
*/
|
||||
private void drawAxisLabelsBetweenStartIndexAndEndIndex(Canvas canvas,
|
||||
final Rect[] displayAreas, final int startIndex, final int endIndex,
|
||||
final float baselineY) {
|
||||
if (endIndex - startIndex <= 1) {
|
||||
return;
|
||||
}
|
||||
if ((endIndex - startIndex) % 2 == 0) {
|
||||
int middleIndex = (startIndex + endIndex) / 2;
|
||||
if (hasOverlap(displayAreas, startIndex, middleIndex)
|
||||
|| hasOverlap(displayAreas, middleIndex, endIndex)) {
|
||||
return;
|
||||
}
|
||||
drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY);
|
||||
drawAxisLabelsBetweenStartIndexAndEndIndex(
|
||||
canvas, displayAreas, startIndex, middleIndex, baselineY);
|
||||
drawAxisLabelsBetweenStartIndexAndEndIndex(
|
||||
canvas, displayAreas, middleIndex, endIndex, baselineY);
|
||||
} else {
|
||||
int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f);
|
||||
int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f);
|
||||
if (hasOverlap(displayAreas, startIndex, middleIndex1)
|
||||
|| hasOverlap(displayAreas, middleIndex1, middleIndex2)
|
||||
|| hasOverlap(displayAreas, middleIndex2, endIndex)) {
|
||||
return;
|
||||
}
|
||||
drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY);
|
||||
drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY);
|
||||
drawAxisLabelsBetweenStartIndexAndEndIndex(
|
||||
canvas, displayAreas, startIndex, middleIndex1, baselineY);
|
||||
drawAxisLabelsBetweenStartIndexAndEndIndex(
|
||||
canvas, displayAreas, middleIndex1, middleIndex2, baselineY);
|
||||
drawAxisLabelsBetweenStartIndexAndEndIndex(
|
||||
canvas, displayAreas, middleIndex2, endIndex, baselineY);
|
||||
}
|
||||
}
|
||||
|
||||
private int getTimestampY(int index) {
|
||||
return getHeight() - mTimestampsBounds[index].height()
|
||||
+ (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
|
||||
+ round(mTextPadding * 1.5f);
|
||||
private boolean hasOverlap(
|
||||
final Rect[] displayAreas, final int leftIndex, final int rightIndex) {
|
||||
return displayAreas[leftIndex].right + mTextPadding * 2f > displayAreas[rightIndex].left;
|
||||
}
|
||||
|
||||
private void drawAxisLabelText(
|
||||
Canvas canvas, final int index, final Rect displayArea, final float baselineY) {
|
||||
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(
|
||||
mViewModel.texts().get(index),
|
||||
displayArea.centerX(),
|
||||
baselineY,
|
||||
mTextPaint);
|
||||
}
|
||||
|
||||
private void drawTrapezoids(Canvas canvas) {
|
||||
// Ignores invalid trapezoid data.
|
||||
if (mLevels == null) {
|
||||
if (mViewModel == null) {
|
||||
return;
|
||||
}
|
||||
final float trapezoidBottom =
|
||||
getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
|
||||
- mTrapezoidVOffset;
|
||||
final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
|
||||
final float availableSpace =
|
||||
trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset;
|
||||
final float unitHeight = availableSpace / 100f;
|
||||
// Draws all trapezoid shapes into the canvas.
|
||||
final Path trapezoidPath = new Path();
|
||||
Path trapezoidCurvePath = null;
|
||||
for (int index = 0; index < mTrapezoidCount; index++) {
|
||||
for (int index = 0; index < mTrapezoidSlots.length; index++) {
|
||||
// Not draws the trapezoid for corner or not initialization cases.
|
||||
if (!isValidToDraw(index)) {
|
||||
if (!isValidToDraw(mViewModel, index)) {
|
||||
if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
|
||||
canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
|
||||
trapezoidCurvePath = null;
|
||||
@@ -530,17 +552,18 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
continue;
|
||||
}
|
||||
// Configures the trapezoid paint color.
|
||||
final int trapezoidColor =
|
||||
!mIsSlotsClickabled
|
||||
? mTrapezoidColor
|
||||
: mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
|
||||
? mTrapezoidSolidColor : mTrapezoidColor;
|
||||
final int trapezoidColor = mIsSlotsClickabled && (mViewModel.selectedIndex() == index
|
||||
|| mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
|
||||
? mTrapezoidSolidColor : mTrapezoidColor;
|
||||
final boolean isHoverState =
|
||||
mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
|
||||
mIsSlotsClickabled && mHoveredIndex == index
|
||||
&& isValidToDraw(mViewModel, mHoveredIndex);
|
||||
mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
|
||||
|
||||
final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
|
||||
final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
|
||||
final float leftTop = round(
|
||||
trapezoidBottom - requireNonNull(mViewModel.levels().get(index)) * unitHeight);
|
||||
final float rightTop = round(trapezoidBottom
|
||||
- requireNonNull(mViewModel.levels().get(index + 1)) * unitHeight);
|
||||
trapezoidPath.reset();
|
||||
trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
|
||||
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
|
||||
@@ -579,15 +602,37 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return SELECTED_INDEX_INVALID;
|
||||
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
}
|
||||
|
||||
private boolean isValidToDraw(int trapezoidIndex) {
|
||||
return mLevels != null
|
||||
private void initializeAxisLabelsBounds() {
|
||||
mAxisLabelsBounds.clear();
|
||||
for (int i = 0; i < mViewModel.size(); i++) {
|
||||
mAxisLabelsBounds.add(new Rect());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isTrapezoidValid(
|
||||
@NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
|
||||
return viewModel.levels().get(trapezoidIndex) != null
|
||||
&& viewModel.levels().get(trapezoidIndex + 1) != null;
|
||||
}
|
||||
|
||||
private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) {
|
||||
return viewModel != null
|
||||
&& trapezoidIndex >= 0
|
||||
&& trapezoidIndex < mLevels.length - 1
|
||||
&& mLevels[trapezoidIndex] != 0
|
||||
&& mLevels[trapezoidIndex + 1] != 0;
|
||||
&& trapezoidIndex < viewModel.size() - 1
|
||||
&& isTrapezoidValid(viewModel, trapezoidIndex);
|
||||
}
|
||||
|
||||
private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) {
|
||||
// Sets the chart is clickable if there is at least one valid item in it.
|
||||
for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) {
|
||||
if (isTrapezoidValid(viewModel, trapezoidIndex)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String[] getPercentages() {
|
||||
@@ -621,7 +666,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
}
|
||||
|
||||
// A container class for each trapezoid left and right location.
|
||||
private static final class TrapezoidSlot {
|
||||
@VisibleForTesting
|
||||
static final class TrapezoidSlot {
|
||||
public float mLeft;
|
||||
public float mRight;
|
||||
|
||||
|
Reference in New Issue
Block a user