• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 package com.android.settings.fuelgauge.batteryusage;
17 
18 import static com.android.settings.Utils.formatPercentage;
19 
20 import static java.lang.Math.round;
21 import static java.util.Objects.requireNonNull;
22 
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.CornerPathEffect;
28 import android.graphics.Paint;
29 import android.graphics.Path;
30 import android.graphics.Rect;
31 import android.os.Bundle;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.HapticFeedbackConstants;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.ViewParent;
38 import android.view.accessibility.AccessibilityEvent;
39 import android.view.accessibility.AccessibilityManager;
40 import android.view.accessibility.AccessibilityNodeInfo;
41 import android.view.accessibility.AccessibilityNodeProvider;
42 import android.widget.TextView;
43 
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 import androidx.annotation.VisibleForTesting;
47 import androidx.appcompat.widget.AppCompatImageView;
48 
49 import com.android.settings.R;
50 import com.android.settingslib.Utils;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Locale;
55 
56 /** A widget component to draw chart graph. */
57 public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
58     private static final String TAG = "BatteryChartView";
59 
60     private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
61     private static final long UPDATE_STATE_DELAYED_TIME = 500L;
62 
63     /** A callback listener for selected group index is updated. */
64     public interface OnSelectListener {
65         /** The callback function for selected group index is updated. */
onSelect(int trapezoidIndex)66         void onSelect(int trapezoidIndex);
67     }
68 
69     private final String[] mPercentages = getPercentages();
70     private final Rect mIndent = new Rect();
71     private final Rect[] mPercentageBounds = new Rect[]{new Rect(), new Rect(), new Rect()};
72     private final List<Rect> mAxisLabelsBounds = new ArrayList<>();
73 
74     private BatteryChartViewModel mViewModel;
75     private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
76     private int mDividerWidth;
77     private int mDividerHeight;
78     private float mTrapezoidVOffset;
79     private float mTrapezoidHOffset;
80     private int mTrapezoidColor;
81     private int mTrapezoidSolidColor;
82     private int mTrapezoidHoverColor;
83     private int mTextPadding;
84     private Paint mDividerPaint;
85     private Paint mTrapezoidPaint;
86     private Paint mTextPaint;
87     private AccessibilityNodeProvider mAccessibilityNodeProvider;
88     private BatteryChartView.OnSelectListener mOnSelectListener;
89 
90     @VisibleForTesting
91     TrapezoidSlot[] mTrapezoidSlots;
92     // Records the location to calculate selected index.
93     @VisibleForTesting
94     float mTouchUpEventX = Float.MIN_VALUE;
95 
BatteryChartView(Context context)96     public BatteryChartView(Context context) {
97         super(context, null);
98     }
99 
BatteryChartView(Context context, AttributeSet attrs)100     public BatteryChartView(Context context, AttributeSet attrs) {
101         super(context, attrs);
102         initializeColors(context);
103         // Registers the click event listener.
104         setOnClickListener(this);
105         setClickable(false);
106         requestLayout();
107     }
108 
109     /** Sets the data model of this view. */
setViewModel(BatteryChartViewModel viewModel)110     public void setViewModel(BatteryChartViewModel viewModel) {
111         if (viewModel == null) {
112             mViewModel = null;
113             invalidate();
114             return;
115         }
116 
117         Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
118                 viewModel.size(), viewModel.selectedIndex()));
119         mViewModel = viewModel;
120         initializeAxisLabelsBounds();
121         initializeTrapezoidSlots(viewModel.size() - 1);
122         setClickable(hasAnyValidTrapezoid(viewModel));
123         requestLayout();
124     }
125 
126     /** Sets the callback to monitor the selected group index. */
setOnSelectListener(BatteryChartView.OnSelectListener listener)127     public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
128         mOnSelectListener = listener;
129     }
130 
131     /** Sets the companion {@link TextView} for percentage information. */
setCompanionTextView(TextView textView)132     public void setCompanionTextView(TextView textView) {
133         if (textView != null) {
134             // Pre-draws the view first to load style atttributions into paint.
135             textView.draw(new Canvas());
136             mTextPaint = textView.getPaint();
137         } else {
138             mTextPaint = null;
139         }
140         requestLayout();
141     }
142 
143     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)144     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
145         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
146         // Measures text bounds and updates indent configuration.
147         if (mTextPaint != null) {
148             mTextPaint.setTextAlign(Paint.Align.LEFT);
149             for (int index = 0; index < mPercentages.length; index++) {
150                 mTextPaint.getTextBounds(
151                         mPercentages[index], 0, mPercentages[index].length(),
152                         mPercentageBounds[index]);
153             }
154             // Updates the indent configurations.
155             mIndent.top = mPercentageBounds[0].height();
156             mIndent.right = mPercentageBounds[0].width() + mTextPadding;
157 
158             if (mViewModel != null) {
159                 int maxTop = 0;
160                 for (int index = 0; index < mViewModel.size(); index++) {
161                     final String text = mViewModel.getText(index);
162                     mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index));
163                     maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top);
164                 }
165                 mIndent.bottom = maxTop + round(mTextPadding * 2f);
166             }
167             Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
168         } else {
169             mIndent.set(0, 0, 0, 0);
170         }
171     }
172 
173     @Override
draw(Canvas canvas)174     public void draw(Canvas canvas) {
175         super.draw(canvas);
176         // Before mLevels initialized, the count of trapezoids is unknown. Only draws the
177         // horizontal percentages and dividers.
178         drawHorizontalDividers(canvas);
179         if (mViewModel == null) {
180             return;
181         }
182         drawVerticalDividers(canvas);
183         drawTrapezoids(canvas);
184     }
185 
186     @Override
onTouchEvent(MotionEvent event)187     public boolean onTouchEvent(MotionEvent event) {
188         // Caches the location to calculate selected trapezoid index.
189         final int action = event.getAction();
190         switch (action) {
191             case MotionEvent.ACTION_UP:
192                 mTouchUpEventX = event.getX();
193                 break;
194             case MotionEvent.ACTION_CANCEL:
195                 mTouchUpEventX = Float.MIN_VALUE; // reset
196                 break;
197         }
198         return super.onTouchEvent(event);
199     }
200 
201     @Override
onHoverEvent(MotionEvent event)202     public boolean onHoverEvent(MotionEvent event) {
203         final int action = event.getAction();
204         switch (action) {
205             case MotionEvent.ACTION_HOVER_ENTER:
206             case MotionEvent.ACTION_HOVER_MOVE:
207                 final int trapezoidIndex = getTrapezoidIndex(event.getX());
208                 if (mHoveredIndex != trapezoidIndex) {
209                     mHoveredIndex = trapezoidIndex;
210                     invalidate();
211                     sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
212                 }
213                 // Ignore the super.onHoverEvent() because the hovered trapezoid has already been
214                 // sent here.
215                 return true;
216             case MotionEvent.ACTION_HOVER_EXIT:
217                 if (mHoveredIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID) {
218                     sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
219                     mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
220                     invalidate();
221                 }
222                 // Ignore the super.onHoverEvent() because the hovered trapezoid has already been
223                 // sent here.
224                 return true;
225             default:
226                 return super.onTouchEvent(event);
227         }
228     }
229 
230     @Override
onHoverChanged(boolean hovered)231     public void onHoverChanged(boolean hovered) {
232         super.onHoverChanged(hovered);
233         if (!hovered) {
234             mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
235             invalidate();
236         }
237     }
238 
239     @Override
onClick(View view)240     public void onClick(View view) {
241         if (mTouchUpEventX == Float.MIN_VALUE) {
242             Log.w(TAG, "invalid motion event for onClick() callback");
243             return;
244         }
245         onTrapezoidClicked(view, getTrapezoidIndex(mTouchUpEventX));
246     }
247 
248     @Override
getAccessibilityNodeProvider()249     public AccessibilityNodeProvider getAccessibilityNodeProvider() {
250         if (mViewModel == null) {
251             return super.getAccessibilityNodeProvider();
252         }
253         if (mAccessibilityNodeProvider == null) {
254             mAccessibilityNodeProvider = new BatteryChartAccessibilityNodeProvider();
255         }
256         return mAccessibilityNodeProvider;
257     }
258 
onTrapezoidClicked(View view, int index)259     private void onTrapezoidClicked(View view, int index) {
260         // Ignores the click event if the level is zero.
261         if (!isValidToDraw(mViewModel, index)) {
262             return;
263         }
264         if (mOnSelectListener != null) {
265             // Selects all if users click the same trapezoid item two times.
266             mOnSelectListener.onSelect(
267                     index == mViewModel.selectedIndex()
268                             ? BatteryChartViewModel.SELECTED_INDEX_ALL : index);
269         }
270         view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
271     }
272 
sendAccessibilityEvent(int virtualDescendantId, int eventType)273     private boolean sendAccessibilityEvent(int virtualDescendantId, int eventType) {
274         ViewParent parent = getParent();
275         if (parent == null || !AccessibilityManager.getInstance(mContext).isEnabled()) {
276             return false;
277         }
278         AccessibilityEvent accessibilityEvent = new AccessibilityEvent(eventType);
279         accessibilityEvent.setSource(this, virtualDescendantId);
280         accessibilityEvent.setEnabled(true);
281         accessibilityEvent.setClassName(getAccessibilityClassName());
282         accessibilityEvent.setPackageName(getContext().getPackageName());
283         return parent.requestSendAccessibilityEvent(this, accessibilityEvent);
284     }
285 
sendAccessibilityEventForHover(int eventType)286     private void sendAccessibilityEventForHover(int eventType) {
287         if (isTrapezoidIndexValid(mViewModel, mHoveredIndex)) {
288             sendAccessibilityEvent(mHoveredIndex, eventType);
289         }
290     }
291 
initializeTrapezoidSlots(int count)292     private void initializeTrapezoidSlots(int count) {
293         mTrapezoidSlots = new TrapezoidSlot[count];
294         for (int index = 0; index < mTrapezoidSlots.length; index++) {
295             mTrapezoidSlots[index] = new TrapezoidSlot();
296         }
297     }
298 
initializeColors(Context context)299     private void initializeColors(Context context) {
300         setBackgroundColor(Color.TRANSPARENT);
301         mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
302         mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
303         mTrapezoidHoverColor = Utils.getColorAttrDefaultColor(context,
304                 com.android.internal.R.attr.colorAccentSecondaryVariant);
305         // Initializes the divider line paint.
306         final Resources resources = getContext().getResources();
307         mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
308         mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
309         mDividerPaint = new Paint();
310         mDividerPaint.setAntiAlias(true);
311         mDividerPaint.setColor(DIVIDER_COLOR);
312         mDividerPaint.setStyle(Paint.Style.STROKE);
313         mDividerPaint.setStrokeWidth(mDividerWidth);
314         Log.i(TAG, "mDividerWidth:" + mDividerWidth);
315         Log.i(TAG, "mDividerHeight:" + mDividerHeight);
316         // Initializes the trapezoid paint.
317         mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
318         mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
319         mTrapezoidPaint = new Paint();
320         mTrapezoidPaint.setAntiAlias(true);
321         mTrapezoidPaint.setColor(mTrapezoidSolidColor);
322         mTrapezoidPaint.setStyle(Paint.Style.FILL);
323         mTrapezoidPaint.setPathEffect(
324                 new CornerPathEffect(
325                         resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
326         // Initializes for drawing text information.
327         mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
328     }
329 
drawHorizontalDividers(Canvas canvas)330     private void drawHorizontalDividers(Canvas canvas) {
331         final int width = getWidth() - mIndent.right;
332         final int height = getHeight() - mIndent.top - mIndent.bottom;
333         // Draws the top divider line for 100% curve.
334         float offsetY = mIndent.top + mDividerWidth * .5f;
335         canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
336         drawPercentage(canvas, /*index=*/ 0, offsetY);
337 
338         // Draws the center divider line for 50% curve.
339         final float availableSpace =
340                 height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
341         offsetY = mIndent.top + mDividerWidth + availableSpace * .5f;
342         canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
343         drawPercentage(canvas, /*index=*/ 1, offsetY);
344 
345         // Draws the bottom divider line for 0% curve.
346         offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
347         canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
348         drawPercentage(canvas, /*index=*/ 2, offsetY);
349     }
350 
drawPercentage(Canvas canvas, int index, float offsetY)351     private void drawPercentage(Canvas canvas, int index, float offsetY) {
352         if (mTextPaint != null) {
353             mTextPaint.setTextAlign(Paint.Align.RIGHT);
354             canvas.drawText(
355                     mPercentages[index],
356                     getWidth(),
357                     offsetY + mPercentageBounds[index].height() * .5f,
358                     mTextPaint);
359         }
360     }
361 
drawVerticalDividers(Canvas canvas)362     private void drawVerticalDividers(Canvas canvas) {
363         final int width = getWidth() - mIndent.right;
364         final int dividerCount = mTrapezoidSlots.length + 1;
365         final float dividerSpace = dividerCount * mDividerWidth;
366         final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
367         final float bottomY = getHeight() - mIndent.bottom;
368         final float startY = bottomY - mDividerHeight;
369         final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
370         // Draws each vertical dividers.
371         float startX = mDividerWidth * .5f;
372         for (int index = 0; index < dividerCount; index++) {
373             canvas.drawLine(startX, startY, startX, bottomY, mDividerPaint);
374             final float nextX = startX + mDividerWidth + unitWidth;
375             // Updates the trapezoid slots for drawing.
376             if (index < mTrapezoidSlots.length) {
377                 mTrapezoidSlots[index].mLeft = round(startX + trapezoidSlotOffset);
378                 mTrapezoidSlots[index].mRight = round(nextX - trapezoidSlotOffset);
379             }
380             startX = nextX;
381         }
382         // Draws the axis label slot information.
383         if (mViewModel != null) {
384             final float baselineY = getHeight() - mTextPadding;
385             Rect[] axisLabelDisplayAreas;
386             switch (mViewModel.axisLabelPosition()) {
387                 case CENTER_OF_TRAPEZOIDS:
388                     axisLabelDisplayAreas = getAxisLabelDisplayAreas(
389                             /* size= */ mViewModel.size() - 1,
390                             /* baselineX= */ mDividerWidth + unitWidth * .5f,
391                             /* offsetX= */ mDividerWidth + unitWidth,
392                             baselineY,
393                             /* shiftFirstAndLast= */ false);
394                     break;
395                 case BETWEEN_TRAPEZOIDS:
396                 default:
397                     axisLabelDisplayAreas = getAxisLabelDisplayAreas(
398                             /* size= */ mViewModel.size(),
399                             /* baselineX= */ mDividerWidth * .5f,
400                             /* offsetX= */ mDividerWidth + unitWidth,
401                             baselineY,
402                             /* shiftFirstAndLast= */ true);
403                     break;
404             }
405             drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
406         }
407     }
408 
409     /** Gets all the axis label texts displaying area positions if they are shown. */
getAxisLabelDisplayAreas(final int size, final float baselineX, final float offsetX, final float baselineY, final boolean shiftFirstAndLast)410     private Rect[] getAxisLabelDisplayAreas(final int size, final float baselineX,
411             final float offsetX, final float baselineY, final boolean shiftFirstAndLast) {
412         final Rect[] result = new Rect[size];
413         for (int index = 0; index < result.length; index++) {
414             final float width = mAxisLabelsBounds.get(index).width();
415             float middle = baselineX + index * offsetX;
416             if (shiftFirstAndLast) {
417                 if (index == 0) {
418                     middle += width * .5f;
419                 }
420                 if (index == size - 1) {
421                     middle -= width * .5f;
422                 }
423             }
424             final float left = middle - width * .5f;
425             final float right = left + width;
426             final float top = baselineY + mAxisLabelsBounds.get(index).top;
427             final float bottom = top + mAxisLabelsBounds.get(index).height();
428             result[index] = new Rect(round(left), round(top), round(right), round(bottom));
429         }
430         return result;
431     }
432 
drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY)433     private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) {
434         final int lastIndex = displayAreas.length - 1;
435         // Suppose first and last labels are always able to draw.
436         drawAxisLabelText(canvas, 0, displayAreas[0], baselineY);
437         drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY);
438         drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY);
439     }
440 
441     /**
442      * Recursively draws axis labels between the start index and the end index. If the inner number
443      * can be exactly divided into 2 parts, check and draw the middle index label and then
444      * recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two
445      * labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go
446      * back to the uplevel of the recursion.
447      */
drawAxisLabelsBetweenStartIndexAndEndIndex(Canvas canvas, final Rect[] displayAreas, final int startIndex, final int endIndex, final float baselineY)448     private void drawAxisLabelsBetweenStartIndexAndEndIndex(Canvas canvas,
449             final Rect[] displayAreas, final int startIndex, final int endIndex,
450             final float baselineY) {
451         if (endIndex - startIndex <= 1) {
452             return;
453         }
454         if ((endIndex - startIndex) % 2 == 0) {
455             int middleIndex = (startIndex + endIndex) / 2;
456             if (hasOverlap(displayAreas, startIndex, middleIndex)
457                     || hasOverlap(displayAreas, middleIndex, endIndex)) {
458                 return;
459             }
460             drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY);
461             drawAxisLabelsBetweenStartIndexAndEndIndex(
462                     canvas, displayAreas, startIndex, middleIndex, baselineY);
463             drawAxisLabelsBetweenStartIndexAndEndIndex(
464                     canvas, displayAreas, middleIndex, endIndex, baselineY);
465         } else {
466             int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f);
467             int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f);
468             if (hasOverlap(displayAreas, startIndex, middleIndex1)
469                     || hasOverlap(displayAreas, middleIndex1, middleIndex2)
470                     || hasOverlap(displayAreas, middleIndex2, endIndex)) {
471                 return;
472             }
473             drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY);
474             drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY);
475             drawAxisLabelsBetweenStartIndexAndEndIndex(
476                     canvas, displayAreas, startIndex, middleIndex1, baselineY);
477             drawAxisLabelsBetweenStartIndexAndEndIndex(
478                     canvas, displayAreas, middleIndex1, middleIndex2, baselineY);
479             drawAxisLabelsBetweenStartIndexAndEndIndex(
480                     canvas, displayAreas, middleIndex2, endIndex, baselineY);
481         }
482     }
483 
hasOverlap( final Rect[] displayAreas, final int leftIndex, final int rightIndex)484     private boolean hasOverlap(
485             final Rect[] displayAreas, final int leftIndex, final int rightIndex) {
486         return displayAreas[leftIndex].right + mTextPadding * 2.3f > displayAreas[rightIndex].left;
487     }
488 
drawAxisLabelText( Canvas canvas, final int index, final Rect displayArea, final float baselineY)489     private void drawAxisLabelText(
490             Canvas canvas, final int index, final Rect displayArea, final float baselineY) {
491         mTextPaint.setTextAlign(Paint.Align.CENTER);
492         canvas.drawText(
493                 mViewModel.getText(index),
494                 displayArea.centerX(),
495                 baselineY,
496                 mTextPaint);
497     }
498 
drawTrapezoids(Canvas canvas)499     private void drawTrapezoids(Canvas canvas) {
500         // Ignores invalid trapezoid data.
501         if (mViewModel == null) {
502             return;
503         }
504         final float trapezoidBottom =
505                 getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
506                         - mTrapezoidVOffset;
507         final float availableSpace =
508                 trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset;
509         final float unitHeight = availableSpace / 100f;
510         // Draws all trapezoid shapes into the canvas.
511         final Path trapezoidPath = new Path();
512         Path trapezoidCurvePath = null;
513         for (int index = 0; index < mTrapezoidSlots.length; index++) {
514             // Not draws the trapezoid for corner or not initialization cases.
515             if (!isValidToDraw(mViewModel, index)) {
516                 continue;
517             }
518             // Configures the trapezoid paint color.
519             final int trapezoidColor = (mViewModel.selectedIndex() == index
520                     || mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
521                     ? mTrapezoidSolidColor : mTrapezoidColor;
522             final boolean isHoverState = mHoveredIndex == index && isValidToDraw(mViewModel,
523                     mHoveredIndex);
524             mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
525 
526             final float leftTop = round(
527                     trapezoidBottom - requireNonNull(mViewModel.getLevel(index)) * unitHeight);
528             final float rightTop = round(trapezoidBottom
529                     - requireNonNull(mViewModel.getLevel(index + 1)) * unitHeight);
530             trapezoidPath.reset();
531             trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
532             trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
533             trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
534             trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
535             // A tricky way to make the trapezoid shape drawing the rounded corner.
536             trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
537             trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
538             // Draws the trapezoid shape into canvas.
539             canvas.drawPath(trapezoidPath, mTrapezoidPaint);
540         }
541     }
542 
543     // Searches the corresponding trapezoid index from x location.
getTrapezoidIndex(float x)544     private int getTrapezoidIndex(float x) {
545         if (mTrapezoidSlots == null) {
546             return BatteryChartViewModel.SELECTED_INDEX_INVALID;
547         }
548         for (int index = 0; index < mTrapezoidSlots.length; index++) {
549             final TrapezoidSlot slot = mTrapezoidSlots[index];
550             if (x >= slot.mLeft - mTrapezoidHOffset
551                     && x <= slot.mRight + mTrapezoidHOffset) {
552                 return index;
553             }
554         }
555         return BatteryChartViewModel.SELECTED_INDEX_INVALID;
556     }
557 
initializeAxisLabelsBounds()558     private void initializeAxisLabelsBounds() {
559         mAxisLabelsBounds.clear();
560         for (int i = 0; i < mViewModel.size(); i++) {
561             mAxisLabelsBounds.add(new Rect());
562         }
563     }
564 
isTrapezoidValid( @onNull BatteryChartViewModel viewModel, int trapezoidIndex)565     private static boolean isTrapezoidValid(
566             @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
567         return viewModel.getLevel(trapezoidIndex) != null
568                 && viewModel.getLevel(trapezoidIndex + 1) != null;
569     }
570 
isTrapezoidIndexValid( @onNull BatteryChartViewModel viewModel, int trapezoidIndex)571     private static boolean isTrapezoidIndexValid(
572             @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
573         return viewModel != null
574                 && trapezoidIndex >= 0
575                 && trapezoidIndex < viewModel.size() - 1;
576     }
577 
isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex)578     private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) {
579         return isTrapezoidIndexValid(viewModel, trapezoidIndex)
580                 && isTrapezoidValid(viewModel, trapezoidIndex);
581     }
582 
hasAnyValidTrapezoid(@onNull BatteryChartViewModel viewModel)583     private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) {
584         // Sets the chart is clickable if there is at least one valid item in it.
585         for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) {
586             if (isTrapezoidValid(viewModel, trapezoidIndex)) {
587                 return true;
588             }
589         }
590         return false;
591     }
592 
getPercentages()593     private static String[] getPercentages() {
594         return new String[]{
595                 formatPercentage(/*percentage=*/ 100, /*round=*/ true),
596                 formatPercentage(/*percentage=*/ 50, /*round=*/ true),
597                 formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
598     }
599 
600     private class BatteryChartAccessibilityNodeProvider extends AccessibilityNodeProvider {
601         @Override
createAccessibilityNodeInfo(int virtualViewId)602         public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
603             if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
604                 final AccessibilityNodeInfo hostInfo =
605                         new AccessibilityNodeInfo(BatteryChartView.this);
606                 for (int index = 0; index < mViewModel.size() - 1; index++) {
607                     hostInfo.addChild(BatteryChartView.this, index);
608                 }
609                 return hostInfo;
610             }
611             final int index = virtualViewId;
612             if (!isTrapezoidIndexValid(mViewModel, index)) {
613                 Log.w(TAG, "Invalid virtual view id:" + index);
614                 return null;
615             }
616             final AccessibilityNodeInfo childInfo =
617                     new AccessibilityNodeInfo(BatteryChartView.this, index);
618             onInitializeAccessibilityNodeInfo(childInfo);
619             childInfo.setClickable(isValidToDraw(mViewModel, index));
620             childInfo.setText(mViewModel.getFullText(index));
621             childInfo.setContentDescription(mViewModel.getFullText(index));
622 
623             final Rect bounds = new Rect();
624             getBoundsOnScreen(bounds, true);
625             final int hostLeft = bounds.left;
626             bounds.left = round(hostLeft + mTrapezoidSlots[index].mLeft);
627             bounds.right = round(hostLeft + mTrapezoidSlots[index].mRight);
628             childInfo.setBoundsInScreen(bounds);
629             return childInfo;
630         }
631 
632         @Override
performAction(int virtualViewId, int action, @Nullable Bundle arguments)633         public boolean performAction(int virtualViewId, int action,
634                 @Nullable Bundle arguments) {
635             if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
636                 return performAccessibilityAction(action, arguments);
637             }
638             switch (action) {
639                 case AccessibilityNodeInfo.ACTION_CLICK:
640                     onTrapezoidClicked(BatteryChartView.this, virtualViewId);
641                     return true;
642 
643                 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
644                     return sendAccessibilityEvent(virtualViewId,
645                             AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
646 
647                 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
648                     return sendAccessibilityEvent(virtualViewId,
649                             AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
650 
651                 default:
652                     return performAccessibilityAction(action, arguments);
653             }
654         }
655     }
656 
657     // A container class for each trapezoid left and right location.
658     @VisibleForTesting
659     static final class TrapezoidSlot {
660         public float mLeft;
661         public float mRight;
662 
663         @Override
toString()664         public String toString() {
665             return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
666         }
667     }
668 }
669