• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.internal.widget;
18 
19 import android.content.pm.ActivityInfo.Config;
20 import android.content.res.Resources;
21 import android.content.res.Resources.Theme;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.ColorFilter;
26 import android.graphics.Paint;
27 import android.graphics.PixelFormat;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.graphics.drawable.Drawable;
31 import android.util.AttributeSet;
32 import android.util.DisplayMetrics;
33 import android.util.Log;
34 
35 import androidx.annotation.ColorInt;
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import com.android.internal.R;
40 
41 import org.xmlpull.v1.XmlPullParser;
42 import org.xmlpull.v1.XmlPullParserException;
43 
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.List;
48 import java.util.Objects;
49 
50 /**
51  * This is used by NotificationProgressBar for displaying a custom background. It composes of
52  * segments, which have non-zero length varying drawing width, and points, which have zero length
53  * and fixed size for drawing.
54  *
55  * @see DrawableSegment
56  * @see DrawablePoint
57  */
58 public final class NotificationProgressDrawable extends Drawable {
59     private static final String TAG = "NotifProgressDrawable";
60 
61     @Nullable
62     private BoundsChangeListener mBoundsChangeListener = null;
63 
64     private State mState;
65     private boolean mMutated;
66 
67     private final ArrayList<DrawablePart> mParts = new ArrayList<>();
68 
69     private final RectF mSegRectF = new RectF();
70     private final RectF mPointRectF = new RectF();
71 
72     private final Paint mFillPaint = new Paint();
73 
74     {
75         mFillPaint.setStyle(Paint.Style.FILL);
76     }
77 
78     private @ColorInt int mEndDotColor = Color.TRANSPARENT;
79 
80     private int mAlpha;
81 
NotificationProgressDrawable()82     public NotificationProgressDrawable() {
83         this(new State(), null);
84     }
85 
86     /**
87      * Returns the radius for the points.
88      */
getPointRadius()89     public float getPointRadius() {
90         return mState.mPointRadius;
91     }
92 
93     /**
94      * Set the segments and points that constitute the drawable.
95      */
setParts(List<DrawablePart> parts)96     public void setParts(List<DrawablePart> parts) {
97         mParts.clear();
98         mParts.addAll(parts);
99 
100         invalidateSelf();
101     }
102 
103     /**
104      * Set the segments and points that constitute the drawable.
105      */
setParts(@onNull DrawablePart... parts)106     public void setParts(@NonNull DrawablePart... parts) {
107         setParts(Arrays.asList(parts));
108     }
109 
110     /**
111      * Update the color of the end dot. If TRANSPARENT, the dot is not drawn.
112      */
updateEndDotColor(@olorInt int endDotColor)113     public void updateEndDotColor(@ColorInt int endDotColor) {
114         if (mEndDotColor != endDotColor) {
115             mEndDotColor = endDotColor;
116             invalidateSelf();
117         }
118     }
119 
120     @Override
draw(@onNull Canvas canvas)121     public void draw(@NonNull Canvas canvas) {
122         final float pointRadius = mState.mPointRadius;
123         final float left = (float) getBounds().left;
124         final float centerY = (float) getBounds().centerY();
125 
126         final int numParts = mParts.size();
127         final float pointTop = Math.round(centerY - pointRadius);
128         final float pointBottom = Math.round(centerY + pointRadius);
129         for (int iPart = 0; iPart < numParts; iPart++) {
130             final DrawablePart part = mParts.get(iPart);
131             final float start = left + part.mStart;
132             final float end = left + part.mEnd;
133             if (part instanceof DrawableSegment segment) {
134                 // No space left to draw the segment
135                 if (start > end) continue;
136 
137                 final float radiusY = segment.mFaded ? mState.mFadedSegmentHeight / 2F
138                         : mState.mSegmentHeight / 2F;
139                 final float cornerRadius = mState.mSegmentCornerRadius;
140 
141                 mFillPaint.setColor(segment.mColor);
142 
143                 mSegRectF.set(Math.round(start), Math.round(centerY - radiusY), Math.round(end),
144                         Math.round(centerY + radiusY));
145                 canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint);
146             } else if (part instanceof DrawablePoint point) {
147                 // TODO: b/367804171 - actually use a vector asset for the default point
148                 //  rather than drawing it as a box?
149                 mPointRectF.set(Math.round(start), pointTop, Math.round(end), pointBottom);
150                 final float inset = mState.mPointRectInset;
151                 final float cornerRadius = mState.mPointRectCornerRadius;
152                 mPointRectF.inset(inset, inset);
153 
154                 mFillPaint.setColor(point.mColor);
155 
156                 canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
157             }
158         }
159 
160         if (mEndDotColor != Color.TRANSPARENT) {
161             final float right = (float) getBounds().right;
162             final float dotRadius = mState.mFadedSegmentHeight / 2F;
163             mFillPaint.setColor(mEndDotColor);
164             // Use drawRoundRect instead of drawCircle to ensure alignment with the segment below.
165             mSegRectF.set(
166                     Math.round(right - mState.mFadedSegmentHeight), Math.round(centerY - dotRadius),
167                             Math.round(right), Math.round(centerY + dotRadius));
168             canvas.drawRoundRect(mSegRectF, mState.mSegmentCornerRadius,
169                     mState.mSegmentCornerRadius, mFillPaint);
170         }
171     }
172 
173     @Override
getChangingConfigurations()174     public @Config int getChangingConfigurations() {
175         return super.getChangingConfigurations() | mState.getChangingConfigurations();
176     }
177 
178     @Override
setAlpha(int alpha)179     public void setAlpha(int alpha) {
180         if (mAlpha != alpha) {
181             mAlpha = alpha;
182             invalidateSelf();
183         }
184     }
185 
186     @Override
getAlpha()187     public int getAlpha() {
188         return mAlpha;
189     }
190 
191     @Override
setColorFilter(@ullable ColorFilter colorFilter)192     public void setColorFilter(@Nullable ColorFilter colorFilter) {
193         // NO-OP
194     }
195 
196     @Override
getOpacity()197     public int getOpacity() {
198         // This method is deprecated. Hence we return UNKNOWN.
199         return PixelFormat.UNKNOWN;
200     }
201 
setBoundsChangeListener(BoundsChangeListener listener)202     public void setBoundsChangeListener(BoundsChangeListener listener) {
203         mBoundsChangeListener = listener;
204     }
205 
206     @Override
onBoundsChange(Rect bounds)207     protected void onBoundsChange(Rect bounds) {
208         super.onBoundsChange(bounds);
209 
210         if (mBoundsChangeListener != null) {
211             mBoundsChangeListener.onDrawableBoundsChanged();
212         }
213     }
214 
215     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)216     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
217             @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)
218             throws XmlPullParserException, IOException {
219         super.inflate(r, parser, attrs, theme);
220 
221         mState.setDensity(resolveDensity(r, 0));
222 
223         inflateChildElements(r, parser, attrs, theme);
224 
225         updateLocalState();
226     }
227 
228     @Override
applyTheme(@onNull Theme t)229     public void applyTheme(@NonNull Theme t) {
230         super.applyTheme(t);
231 
232         final State state = mState;
233         if (state == null) {
234             return;
235         }
236 
237         state.setDensity(resolveDensity(t.getResources(), 0));
238 
239         applyThemeChildElements(t);
240 
241         updateLocalState();
242     }
243 
244     @Override
canApplyTheme()245     public boolean canApplyTheme() {
246         return (mState.canApplyTheme()) || super.canApplyTheme();
247     }
248 
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)249     private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
250             Theme theme) throws XmlPullParserException, IOException {
251         TypedArray a;
252         int type;
253 
254         final int innerDepth = parser.getDepth() + 1;
255         int depth;
256         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
257                 && ((depth = parser.getDepth()) >= innerDepth
258                 || type != XmlPullParser.END_TAG)) {
259             if (type != XmlPullParser.START_TAG) {
260                 continue;
261             }
262 
263             if (depth > innerDepth) {
264                 continue;
265             }
266 
267             String name = parser.getName();
268 
269             if (name.equals("segments")) {
270                 a = obtainAttributes(r, theme, attrs,
271                         R.styleable.NotificationProgressDrawableSegments);
272                 updateSegmentsFromTypedArray(a);
273                 a.recycle();
274             } else if (name.equals("points")) {
275                 a = obtainAttributes(r, theme, attrs,
276                         R.styleable.NotificationProgressDrawablePoints);
277                 updatePointsFromTypedArray(a);
278                 a.recycle();
279             } else {
280                 Log.w(TAG, "Bad element under NotificationProgressDrawable: " + name);
281             }
282         }
283     }
284 
applyThemeChildElements(Theme t)285     private void applyThemeChildElements(Theme t) {
286         final State state = mState;
287 
288         if (state.mThemeAttrsSegments != null) {
289             final TypedArray a = t.resolveAttributes(
290                     state.mThemeAttrsSegments, R.styleable.NotificationProgressDrawableSegments);
291             updateSegmentsFromTypedArray(a);
292             a.recycle();
293         }
294 
295         if (state.mThemeAttrsPoints != null) {
296             final TypedArray a = t.resolveAttributes(
297                     state.mThemeAttrsPoints, R.styleable.NotificationProgressDrawablePoints);
298             updatePointsFromTypedArray(a);
299             a.recycle();
300         }
301     }
302 
updateSegmentsFromTypedArray(TypedArray a)303     private void updateSegmentsFromTypedArray(TypedArray a) {
304         final State state = mState;
305 
306         // Account for any configuration changes.
307         state.mChangingConfigurations |= a.getChangingConfigurations();
308 
309         // Extract the theme attributes, if any.
310         state.mThemeAttrsSegments = a.extractThemeAttrs();
311 
312         state.mSegmentHeight = a.getDimension(
313                 R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight);
314         state.mFadedSegmentHeight = a.getDimension(
315                 R.styleable.NotificationProgressDrawableSegments_fadedHeight,
316                 state.mFadedSegmentHeight);
317         state.mSegmentCornerRadius = a.getDimension(
318                 R.styleable.NotificationProgressDrawableSegments_cornerRadius,
319                 state.mSegmentCornerRadius);
320     }
321 
updatePointsFromTypedArray(TypedArray a)322     private void updatePointsFromTypedArray(TypedArray a) {
323         final State state = mState;
324 
325         // Account for any configuration changes.
326         state.mChangingConfigurations |= a.getChangingConfigurations();
327 
328         // Extract the theme attributes, if any.
329         state.mThemeAttrsPoints = a.extractThemeAttrs();
330 
331         state.mPointRadius = a.getDimension(R.styleable.NotificationProgressDrawablePoints_radius,
332                 state.mPointRadius);
333         state.mPointRectInset = a.getDimension(R.styleable.NotificationProgressDrawablePoints_inset,
334                 state.mPointRectInset);
335         state.mPointRectCornerRadius = a.getDimension(
336                 R.styleable.NotificationProgressDrawablePoints_cornerRadius,
337                 state.mPointRectCornerRadius);
338     }
339 
resolveDensity(@ullable Resources r, int parentDensity)340     static int resolveDensity(@Nullable Resources r, int parentDensity) {
341         final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi;
342         return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
343     }
344 
345     /**
346      * Scales a floating-point pixel value from the source density to the
347      * target density.
348      */
scaleFromDensity(float pixels, int sourceDensity, int targetDensity)349     private static float scaleFromDensity(float pixels, int sourceDensity, int targetDensity) {
350         return pixels * targetDensity / sourceDensity;
351     }
352 
353     /**
354      * Scales a pixel value from the source density to the target density.
355      * <p>
356      * Optionally, when {@code isSize} is true, handles the resulting pixel value as a size,
357      * which is rounded to the closest positive integer.
358      * <p>
359      * Note: Iteratively applying density changes could result in drift of the pixel values due
360      * to rounding, especially for paddings which are truncated. Therefore it should be avoided.
361      * This isn't an issue for the notifications because the inflation pipeline reinflates
362      * notification views on density change.
363      */
scaleFromDensity( int pixels, int sourceDensity, int targetDensity, boolean isSize)364     private static int scaleFromDensity(
365             int pixels, int sourceDensity, int targetDensity, boolean isSize) {
366         if (pixels == 0 || sourceDensity == targetDensity) {
367             return pixels;
368         }
369 
370         final float result = pixels * targetDensity / (float) sourceDensity;
371         if (!isSize) {
372             return (int) result;
373         }
374 
375         final int rounded = Math.round(result);
376         if (rounded != 0) {
377             return rounded;
378         } else if (pixels > 0) {
379             return 1;
380         } else {
381             return -1;
382         }
383     }
384 
385     /**
386      * Listener to receive updates about drawable bounds changing
387      */
388     public interface BoundsChangeListener {
389         /** Called when bounds have changed */
onDrawableBoundsChanged()390         void onDrawableBoundsChanged();
391     }
392 
393     /**
394      * A part of the progress drawable, which is either a {@link DrawableSegment} with non-zero
395      * length and varying drawing width, or a {@link DrawablePoint} with zero length and fixed size
396      * for drawing.
397      */
398     public abstract static class DrawablePart {
399         // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the
400         //  bounds rect.
401         /** Start position for drawing (in pixels) */
402         protected float mStart;
403         /** End position for drawing (in pixels) */
404         protected float mEnd;
405         /** Drawing color. */
406         @ColorInt protected final int mColor;
407 
DrawablePart(float start, float end, @ColorInt int color)408         protected DrawablePart(float start, float end, @ColorInt int color) {
409             mStart = start;
410             mEnd = end;
411             mColor = color;
412         }
413 
getStart()414         public float getStart() {
415             return this.mStart;
416         }
417 
setStart(float start)418         public void setStart(float start) {
419             mStart = start;
420         }
421 
getEnd()422         public float getEnd() {
423             return this.mEnd;
424         }
425 
setEnd(float end)426         public void setEnd(float end) {
427             mEnd = end;
428         }
429 
430         /** Returns the calculated drawing width of the part */
getWidth()431         public float getWidth() {
432             return mEnd - mStart;
433         }
434 
getColor()435         public int getColor() {
436             return this.mColor;
437         }
438 
439         // Needed for unit tests
440         @Override
equals(@ullable Object other)441         public boolean equals(@Nullable Object other) {
442             if (this == other) return true;
443 
444             if (other == null || getClass() != other.getClass()) return false;
445 
446             DrawablePart that = (DrawablePart) other;
447             if (Float.compare(this.mStart, that.mStart) != 0) return false;
448             if (Float.compare(this.mEnd, that.mEnd) != 0) return false;
449             return this.mColor == that.mColor;
450         }
451 
452         @Override
hashCode()453         public int hashCode() {
454             return Objects.hash(mStart, mEnd, mColor);
455         }
456     }
457 
458     /**
459      * A segment is a part of the progress bar with non-zero length. For example, it can
460      * represent a portion in a navigation journey with certain traffic condition.
461      * <p>
462      * The start and end positions for drawing a segment are assumed to have been adjusted for
463      * the Points and gaps neighboring the segment.
464      * </p>
465      */
466     public static final class DrawableSegment extends DrawablePart {
467         /**
468          * Whether the segment is faded or not.
469          * <p>
470          * Faded segments and non-faded segments are drawn with different heights.
471          * </p>
472          */
473         private final boolean mFaded;
474 
DrawableSegment(float start, float end, int color)475         public DrawableSegment(float start, float end, int color) {
476             this(start, end, color, false);
477         }
478 
DrawableSegment(float start, float end, int color, boolean faded)479         public DrawableSegment(float start, float end, int color, boolean faded) {
480             super(start, end, color);
481             mFaded = faded;
482         }
483 
484         @Override
toString()485         public String toString() {
486             return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
487                     + ", faded=" + this.mFaded + ')';
488         }
489 
490         // Needed for unit tests.
491         @Override
equals(@ullable Object other)492         public boolean equals(@Nullable Object other) {
493             if (!super.equals(other)) return false;
494 
495             DrawableSegment that = (DrawableSegment) other;
496             return this.mFaded == that.mFaded;
497         }
498 
499         @Override
hashCode()500         public int hashCode() {
501             return Objects.hash(super.hashCode(), mFaded);
502         }
503     }
504 
505     /**
506      * A point is a part of the progress bar with zero length. Points are designated points within a
507      * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
508      * ride-share journey.
509      */
510     public static final class DrawablePoint extends DrawablePart {
DrawablePoint(float start, float end, int color)511         public DrawablePoint(float start, float end, int color) {
512             super(start, end, color);
513         }
514 
515         @Override
toString()516         public String toString() {
517             return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
518                     + ")";
519         }
520     }
521 
522     @Override
mutate()523     public Drawable mutate() {
524         if (!mMutated && super.mutate() == this) {
525             mState = new State(mState, null);
526             updateLocalState();
527             mMutated = true;
528         }
529         return this;
530     }
531 
532     @Override
clearMutated()533     public void clearMutated() {
534         super.clearMutated();
535         mMutated = false;
536     }
537 
538     static final class State extends ConstantState {
539         @Config
540         int mChangingConfigurations;
541         float mSegmentHeight;
542         float mFadedSegmentHeight;
543         float mSegmentCornerRadius;
544         // how big the point icon will be, halved
545         float mPointRadius;
546         float mPointRectInset;
547         float mPointRectCornerRadius;
548 
549         int[] mThemeAttrs;
550         int[] mThemeAttrsSegments;
551         int[] mThemeAttrsPoints;
552 
553         int mDensity = DisplayMetrics.DENSITY_DEFAULT;
554 
State()555         State() {
556         }
557 
State(@onNull State orig, @Nullable Resources res)558         State(@NonNull State orig, @Nullable Resources res) {
559             mChangingConfigurations = orig.mChangingConfigurations;
560             mSegmentHeight = orig.mSegmentHeight;
561             mFadedSegmentHeight = orig.mFadedSegmentHeight;
562             mSegmentCornerRadius = orig.mSegmentCornerRadius;
563             mPointRadius = orig.mPointRadius;
564             mPointRectInset = orig.mPointRectInset;
565             mPointRectCornerRadius = orig.mPointRectCornerRadius;
566 
567             mThemeAttrs = orig.mThemeAttrs;
568             mThemeAttrsSegments = orig.mThemeAttrsSegments;
569             mThemeAttrsPoints = orig.mThemeAttrsPoints;
570 
571             mDensity = resolveDensity(res, orig.mDensity);
572             if (orig.mDensity != mDensity) {
573                 applyDensityScaling(orig.mDensity, mDensity);
574             }
575         }
576 
applyDensityScaling(int sourceDensity, int targetDensity)577         private void applyDensityScaling(int sourceDensity, int targetDensity) {
578             if (mSegmentHeight > 0) {
579                 mSegmentHeight = scaleFromDensity(
580                         mSegmentHeight, sourceDensity, targetDensity);
581             }
582             if (mFadedSegmentHeight > 0) {
583                 mFadedSegmentHeight = scaleFromDensity(
584                         mFadedSegmentHeight, sourceDensity, targetDensity);
585             }
586             if (mSegmentCornerRadius > 0) {
587                 mSegmentCornerRadius = scaleFromDensity(
588                         mSegmentCornerRadius, sourceDensity, targetDensity);
589             }
590             if (mPointRadius > 0) {
591                 mPointRadius = scaleFromDensity(
592                         mPointRadius, sourceDensity, targetDensity);
593             }
594             if (mPointRectInset > 0) {
595                 mPointRectInset = scaleFromDensity(
596                         mPointRectInset, sourceDensity, targetDensity);
597             }
598             if (mPointRectCornerRadius > 0) {
599                 mPointRectCornerRadius = scaleFromDensity(
600                         mPointRectCornerRadius, sourceDensity, targetDensity);
601             }
602         }
603 
604         @NonNull
605         @Override
newDrawable()606         public Drawable newDrawable() {
607             return new NotificationProgressDrawable(this, null);
608         }
609 
610         @Override
newDrawable(@ullable Resources res)611         public Drawable newDrawable(@Nullable Resources res) {
612             // If this drawable is being created for a different density,
613             // just create a new constant state and call it a day.
614             final State state;
615             final int density = resolveDensity(res, mDensity);
616             if (density != mDensity) {
617                 state = new State(this, res);
618             } else {
619                 state = this;
620             }
621 
622             return new NotificationProgressDrawable(state, res);
623         }
624 
625         @Override
getChangingConfigurations()626         public int getChangingConfigurations() {
627             return mChangingConfigurations;
628         }
629 
630         @Override
canApplyTheme()631         public boolean canApplyTheme() {
632             return mThemeAttrs != null || mThemeAttrsSegments != null || mThemeAttrsPoints != null
633                     || super.canApplyTheme();
634         }
635 
setDensity(int targetDensity)636         public void setDensity(int targetDensity) {
637             if (mDensity != targetDensity) {
638                 final int sourceDensity = mDensity;
639                 mDensity = targetDensity;
640 
641                 applyDensityScaling(sourceDensity, targetDensity);
642             }
643         }
644     }
645 
646     @Override
getConstantState()647     public ConstantState getConstantState() {
648         mState.mChangingConfigurations = getChangingConfigurations();
649         return mState;
650     }
651 
652     /**
653      * Creates a new themed NotificationProgressDrawable based on the specified constant state.
654      * <p>
655      * The resulting drawable is guaranteed to have a new constant state.
656      *
657      * @param state Constant state from which the drawable inherits
658      */
NotificationProgressDrawable(@onNull State state, @Nullable Resources res)659     private NotificationProgressDrawable(@NonNull State state, @Nullable Resources res) {
660         mState = state;
661 
662         updateLocalState();
663     }
664 
updateLocalState()665     private void updateLocalState() {
666         // NO-OP
667     }
668 }
669