diff options
| author | 2025-01-08 13:05:04 -0800 | |
|---|---|---|
| committer | 2025-01-08 13:05:04 -0800 | |
| commit | 3171e569c403c8ada3fdecc3112c9948c08ae189 (patch) | |
| tree | 9cf3a3d58dd3d7d18cef71e47a84f2ba405e419b | |
| parent | a85e73c38524035e36cb80159ecf343e10498f81 (diff) | |
| parent | ef7cce266ca0592f7f7cef61c63e2eea56552393 (diff) | |
Merge changes Ieb551182,I4778dfee,Ifc09326c,Ic513aef1 into main
* changes:
Stretch and rescale segments to satisfy minimum width requirement.
Move segment splitting by process after converting to drawable segments.
Refactor to precalculate start/end positions for drawing Segment/Point.
Fix documentation comments for NotificationProgressDrawable attributes.
| -rw-r--r-- | core/java/com/android/internal/widget/NotificationProgressBar.java | 605 | ||||
| -rw-r--r-- | core/java/com/android/internal/widget/NotificationProgressDrawable.java | 372 | ||||
| -rw-r--r-- | core/res/res/drawable/notification_progress.xml | 1 | ||||
| -rw-r--r-- | core/res/res/values/attrs.xml | 22 | ||||
| -rw-r--r-- | core/res/res/values/dimens.xml | 2 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 1 | ||||
| -rw-r--r-- | core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java | 650 |
7 files changed, 1250 insertions, 403 deletions
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 8cd7843fe1d9..f0b54937546b 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -31,6 +32,7 @@ import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.RemotableViewMethod; import android.widget.ProgressBar; import android.widget.RemoteViews; @@ -40,14 +42,12 @@ import androidx.annotation.ColorInt; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -56,18 +56,25 @@ import java.util.TreeSet; * represent Notification ProgressStyle progress, such as for ridesharing and navigation. */ @RemoteViews.RemoteView -public final class NotificationProgressBar extends ProgressBar { +public final class NotificationProgressBar extends ProgressBar implements + NotificationProgressDrawable.BoundsChangeListener { private static final String TAG = "NotificationProgressBar"; private NotificationProgressDrawable mNotificationProgressDrawable; + private final Rect mProgressDrawableBounds = new Rect(); private NotificationProgressModel mProgressModel; @Nullable - private List<Part> mProgressDrawableParts = null; + private List<Part> mParts = null; + + // List of drawable parts before segment splitting by process. + @Nullable + private List<NotificationProgressDrawable.Part> mProgressDrawableParts = null; @Nullable private Drawable mTracker = null; + private boolean mHasTrackerIcon = false; /** @see R.styleable#NotificationProgressBar_trackerHeight */ private final int mTrackerHeight; @@ -76,7 +83,13 @@ public final class NotificationProgressBar extends ProgressBar { private final Matrix mMatrix = new Matrix(); private Matrix mTrackerDrawMatrix = null; - private float mScale = 0; + private float mProgressFraction = 0; + /** + * The location of progress on the stretched and rescaled progress bar, in fraction. Used for + * calculating the tracker position. If stretching and rescaling is not needed, == + * mProgressFraction. + */ + private float mAdjustedProgressFraction = 0; /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */ private boolean mTrackerPosIsDirty = false; @@ -104,12 +117,13 @@ public final class NotificationProgressBar extends ProgressBar { try { mNotificationProgressDrawable = getNotificationProgressDrawable(); + mNotificationProgressDrawable.setBoundsChangeListener(this); } catch (IllegalStateException ex) { Log.e(TAG, "Can't get NotificationProgressDrawable", ex); } // Supports setting the tracker in xml, but ProgressStyle notifications set/override it - // via {@code setProgressTrackerIcon}. + // via {@code #setProgressTrackerIcon}. final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker); setTracker(tracker); @@ -137,20 +151,25 @@ public final class NotificationProgressBar extends ProgressBar { final int indeterminateColor = mProgressModel.getIndeterminateColor(); setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor)); } else { + // TODO: b/372908709 - maybe don't rerun the entire calculation every time the + // progress model is updated? For example, if the segments and parts aren't changed, + // there is no need to call `processAndConvertToViewParts` again. + final int progress = mProgressModel.getProgress(); final int progressMax = mProgressModel.getProgressMax(); - mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(), + + mParts = processAndConvertToViewParts(mProgressModel.getSegments(), mProgressModel.getPoints(), progress, - progressMax, - mProgressModel.isStyledByProgress()); - - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setParts(mProgressDrawableParts); - } + progressMax); setMax(progressMax); setProgress(progress); + + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0) { + updateDrawableParts(); + } } } @@ -200,9 +219,7 @@ public final class NotificationProgressBar extends ProgressBar { } else { progressTrackerDrawable = null; } - return () -> { - setTracker(progressTrackerDrawable); - }; + return () -> setTracker(progressTrackerDrawable); } private void setTracker(@Nullable Drawable tracker) { @@ -226,8 +243,14 @@ public final class NotificationProgressBar extends ProgressBar { final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker); mTracker = tracker; - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null); + final boolean hasTrackerIcon = (mTracker != null); + if (mHasTrackerIcon != hasTrackerIcon) { + mHasTrackerIcon = hasTrackerIcon; + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0 + && mProgressModel.isStyledByProgress()) { + updateDrawableParts(); + } } configureTrackerBounds(); @@ -293,6 +316,8 @@ public final class NotificationProgressBar extends ProgressBar { mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setProgress(int progress) { super.setProgress(progress); @@ -300,6 +325,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public void setProgress(int progress, boolean animate) { // Animation isn't supported by NotificationProgressBar. @@ -308,6 +335,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMin(int min) { super.setMin(min); @@ -315,6 +344,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMax(int max) { super.setMax(max); @@ -323,10 +354,10 @@ public final class NotificationProgressBar extends ProgressBar { } private void onMaybeVisualProgressChanged() { - float scale = getScale(); - if (mScale == scale) return; + float progressFraction = getProgressFraction(); + if (mProgressFraction == progressFraction) return; - mScale = scale; + mProgressFraction = progressFraction; mTrackerPosIsDirty = true; invalidate(); } @@ -372,6 +403,59 @@ public final class NotificationProgressBar extends ProgressBar { updateTrackerAndBarPos(w, h); } + @Override + public void onDrawableBoundsChanged() { + final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds(); + + if (mProgressDrawableBounds.equals(progressDrawableBounds)) return; + + if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) { + updateDrawableParts(); + } + + mProgressDrawableBounds.set(progressDrawableBounds); + } + + private void updateDrawableParts() { + Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " + + mNotificationProgressDrawable + ", mParts = " + mParts); + + if (mNotificationProgressDrawable == null) return; + if (mParts == null) return; + + final float width = mNotificationProgressDrawable.getBounds().width(); + if (width == 0) { + if (mProgressDrawableParts != null) { + Log.d(TAG, "Clearing mProgressDrawableParts"); + mProgressDrawableParts.clear(); + mNotificationProgressDrawable.setParts(mProgressDrawableParts); + } + return; + } + + mProgressDrawableParts = processAndConvertToDrawableParts( + mParts, + width, + mNotificationProgressDrawable.getSegSegGap(), + mNotificationProgressDrawable.getSegPointGap(), + mNotificationProgressDrawable.getPointRadius(), + mHasTrackerIcon + ); + Pair<List<NotificationProgressDrawable.Part>, Float> p = maybeStretchAndRescaleSegments( + mParts, + mProgressDrawableParts, + mNotificationProgressDrawable.getSegmentMinWidth(), + mNotificationProgressDrawable.getPointRadius(), + getProgressFraction(), + width, + mProgressModel.isStyledByProgress(), + mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap()); + + Log.d(TAG, "Updating NotificationProgressDrawable parts"); + mNotificationProgressDrawable.setParts(p.first); + mAdjustedProgressFraction = p.second / width; + } + private void updateTrackerAndBarPos(int w, int h) { final int paddedHeight = h - mPaddingTop - mPaddingBottom; final Drawable bar = getCurrentDrawable(); @@ -402,11 +486,11 @@ public final class NotificationProgressBar extends ProgressBar { } if (tracker != null) { - setTrackerPos(w, tracker, mScale, trackerOffsetY); + setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY); } } - private float getScale() { + private float getProgressFraction() { int min = getMin(); int max = getMax(); int range = max - min; @@ -418,17 +502,17 @@ public final class NotificationProgressBar extends ProgressBar { * * @param w Width of the view, including padding * @param tracker Drawable used for the tracker - * @param scale Current progress between 0 and 1 + * @param progressFraction Current progress between 0 and 1 * @param offsetY Vertical offset for centering. If set to * {@link Integer#MIN_VALUE}, the current offset will be used. */ - private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) { + private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) { int available = w - mPaddingLeft - mPaddingRight; final int trackerWidth = tracker.getIntrinsicWidth(); final int trackerHeight = tracker.getIntrinsicHeight(); available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth); - final int trackerPos = (int) (scale * available + 0.5f); + final int trackerPos = (int) (progressFraction * available + 0.5f); final int top, bottom; if (offsetY == Integer.MIN_VALUE) { @@ -482,7 +566,7 @@ public final class NotificationProgressBar extends ProgressBar { if (mTracker == null) return; if (mTrackerPosIsDirty) { - setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE); } final int saveCount = canvas.save(); @@ -531,7 +615,7 @@ public final class NotificationProgressBar extends ProgressBar { final Drawable tracker = mTracker; if (tracker != null) { - setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE); // Since we draw translated, the drawable's bounds that it signals // for invalidation won't be the actual bounds we want invalidated, @@ -541,16 +625,14 @@ public final class NotificationProgressBar extends ProgressBar { } /** - * Processes the ProgressStyle data and convert to list of {@code - * NotificationProgressDrawable.Part}. + * Processes the ProgressStyle data and convert to a list of {@code Part}. */ @VisibleForTesting - public static List<Part> processAndConvertToDrawableParts( + public static List<Part> processAndConvertToViewParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { if (segments.isEmpty()) { throw new IllegalArgumentException("List of segments shouldn't be empty"); @@ -571,6 +653,7 @@ public final class NotificationProgressBar extends ProgressBar { if (progress < 0 || progress > progressMax) { throw new IllegalArgumentException("Invalid progress : " + progress); } + for (ProgressStyle.Point point : points) { final int pos = point.getPosition(); if (pos < 0 || pos > progressMax) { @@ -583,23 +666,21 @@ public final class NotificationProgressBar extends ProgressBar { final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( points); final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap, - positionToPointMap, progress, isStyledByProgress); + positionToPointMap); final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = - splitSegmentsByPointsAndProgress( - startToSegmentMap, sortedPos, progressMax); + splitSegmentsByPoints(startToSegmentMap, sortedPos, progressMax); - return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos, - progress, progressMax, - isStyledByProgress); + return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos, + progressMax); } // Any segment with a point on it gets split by the point. - // If isStyledByProgress is true, also split the segment with the progress value in its range. - private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress( + private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints( Map<Integer, ProgressStyle.Segment> startToSegmentMap, SortedSet<Integer> sortedPos, - int progressMax) { + int progressMax + ) { int prevSegStart = 0; for (Integer pos : sortedPos) { if (pos == 0 || pos == progressMax) continue; @@ -624,32 +705,22 @@ public final class NotificationProgressBar extends ProgressBar { return startToSegmentMap; } - private static List<Part> convertToDrawableParts( + private static List<Part> convertToViewParts( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap, SortedSet<Integer> sortedPos, - int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { List<Part> parts = new ArrayList<>(); - boolean styleRemainingParts = false; for (Integer pos : sortedPos) { if (positionToPointMap.containsKey(pos)) { final ProgressStyle.Point point = positionToPointMap.get(pos); - final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts); - parts.add(new Point(null, color, styleRemainingParts)); - } - // We want the Point at the current progress to be filled (not faded), but a Segment - // starting at this progress to be faded. - if (isStyledByProgress && !styleRemainingParts && pos == progress) { - styleRemainingParts = true; + parts.add(new Point(point.getColor())); } if (startToSegmentMap.containsKey(pos)) { final ProgressStyle.Segment seg = startToSegmentMap.get(pos); - final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts); parts.add(new Segment( - (float) seg.getLength() / progressMax, color, styleRemainingParts)); + (float) seg.getLength() / progressMax, seg.getColor())); } } @@ -660,11 +731,24 @@ public final class NotificationProgressBar extends ProgressBar { private static int maybeGetFadedColor(@ColorInt int color, boolean fade) { if (!fade) return color; - return NotificationProgressDrawable.getFadedColor(color); + return getFadedColor(color); + } + + /** + * Get a color with an opacity that's 40% of the input color. + */ + @ColorInt + static int getFadedColor(@ColorInt int color) { + return Color.argb( + (int) (Color.alpha(color) * 0.4f + 0.5f), + Color.red(color), + Color.green(color), + Color.blue(color)); } private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap( - List<ProgressStyle.Segment> segments) { + List<ProgressStyle.Segment> segments + ) { final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>(); int currentStart = 0; // Initial start position is 0 @@ -681,7 +765,8 @@ public final class NotificationProgressBar extends ProgressBar { } private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap( - List<ProgressStyle.Point> points) { + List<ProgressStyle.Point> points + ) { final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>(); for (ProgressStyle.Point point : points) { @@ -693,14 +778,404 @@ public final class NotificationProgressBar extends ProgressBar { private static SortedSet<Integer> generateSortedPositionSet( Map<Integer, ProgressStyle.Segment> startToSegmentMap, - Map<Integer, ProgressStyle.Point> positionToPointMap, int progress, - boolean isStyledByProgress) { + Map<Integer, ProgressStyle.Point> positionToPointMap + ) { final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet()); sortedPos.addAll(positionToPointMap.keySet()); - if (isStyledByProgress) { - sortedPos.add(progress); - } return sortedPos; } + + /** + * Processes the list of {@code Part} and convert to a list of + * {@code NotificationProgressDrawable.Part}. + */ + @VisibleForTesting + public static List<NotificationProgressDrawable.Part> processAndConvertToDrawableParts( + List<Part> parts, + float totalWidth, + float segSegGap, + float segPointGap, + float pointRadius, + boolean hasTrackerIcon + ) { + List<NotificationProgressDrawable.Part> drawableParts = new ArrayList<>(); + + // generally, we will start drawing at (x, y) and end at (x+w, y) + float x = (float) 0; + + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); + final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); + if (part instanceof Segment segment) { + final float segWidth = segment.mFraction * totalWidth; + // Advance the start position to account for a point immediately prior. + final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); + final float start = x + startOffset; + // Retract the end position to account for the padding and a point immediately + // after. + final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, + segSegGap, x + segWidth, totalWidth, hasTrackerIcon); + final float end = x + segWidth - endOffset; + + drawableParts.add( + new NotificationProgressDrawable.Segment(start, end, segment.mColor, + segment.mFaded)); + + segment.mStart = x; + segment.mEnd = x + segWidth; + + // Advance the current position to account for the segment's fraction of the total + // width (ignoring offset and padding) + x += segWidth; + } else if (part instanceof Point point) { + final float pointWidth = 2 * pointRadius; + float start = x - pointRadius; + if (start < 0) start = 0; + float end = start + pointWidth; + if (end > totalWidth) { + end = totalWidth; + if (totalWidth > pointWidth) start = totalWidth - pointWidth; + } + + drawableParts.add( + new NotificationProgressDrawable.Point(start, end, point.mColor)); + } + } + + return drawableParts; + } + + private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, + float startX) { + if (!(prevPart instanceof Point)) return 0F; + final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; + return pointOffset + pointRadius + segPointGap; + } + + private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, + float segPointGap, + float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { + if (nextPart == null) return 0F; + if (nextPart instanceof Segment nextSeg) { + if (!seg.mFaded && nextSeg.mFaded) { + // @see Segment#mFaded + return hasTrackerIcon ? 0F : segSegGap; + } + return segSegGap; + } + + final float pointWidth = 2 * pointRadius; + final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) + ? (endX + pointRadius - totalWidth) : 0; + return segPointGap + pointRadius + pointOffset; + } + + /** + * Processes the list of {@code NotificationProgressBar.Part} data and convert to a pair of: + * - list of {@code NotificationProgressDrawable.Part}. + * - location of progress on the stretched and rescaled progress bar. + */ + @VisibleForTesting + public static Pair<List<NotificationProgressDrawable.Part>, Float> + maybeStretchAndRescaleSegments( + List<Part> parts, + List<NotificationProgressDrawable.Part> drawableParts, + float segmentMinWidth, + float pointRadius, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + final List<NotificationProgressDrawable.Segment> drawableSegments = drawableParts + .stream() + .filter(NotificationProgressDrawable.Segment.class::isInstance) + .map(NotificationProgressDrawable.Segment.class::cast) + .toList(); + float totalExcessWidth = 0; + float totalPositiveExcessWidth = 0; + for (NotificationProgressDrawable.Segment drawableSegment : drawableSegments) { + final float excessWidth = drawableSegment.getWidth() - segmentMinWidth; + totalExcessWidth += excessWidth; + if (excessWidth > 0) totalPositiveExcessWidth += excessWidth; + } + + // All drawable segments are above minimum width. No need to stretch and rescale. + if (totalExcessWidth == totalPositiveExcessWidth) { + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + if (totalExcessWidth < 0) { + // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback + // option. (instead of return.) + Log.w(TAG, "Not enough width to satisfy the minimum width for segments."); + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + final int nParts = drawableParts.size(); + float startOffset = 0; + for (int iPart = 0; iPart < nParts; iPart++) { + final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) { + final float origDrawableSegmentWidth = drawableSegment.getWidth(); + + float drawableSegmentWidth = segmentMinWidth; + // Allocate the totalExcessWidth to the segments above minimum, proportionally to + // their initial excessWidth. + if (origDrawableSegmentWidth > segmentMinWidth) { + drawableSegmentWidth += + totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth) + / totalPositiveExcessWidth; + } + + final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth(); + + // Adjust drawable segments to new widths + drawableSegment.setStart(drawableSegment.getStart() + startOffset); + drawableSegment.setEnd( + drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff); + + // Also adjust view segments to new width. (For view segments, only start is + // needed?) + // Check that segments and drawableSegments are of the same size? + final Segment segment = (Segment) parts.get(iPart); + final float origSegmentWidth = segment.getWidth(); + segment.mStart = segment.mStart + startOffset; + segment.mEnd = segment.mStart + origSegmentWidth + widthDiff; + + // Increase startOffset for the subsequent segments. + startOffset += widthDiff; + } else if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) { + drawablePoint.setStart(drawablePoint.getStart() + startOffset); + drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius); + } + } + + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + // Find the location of progress on the stretched and rescaled progress bar. + // If isStyledByProgress is true, also split the segment with the progress value in its range. + private static Pair<List<NotificationProgressDrawable.Part>, Float> + maybeSplitDrawableSegmentsByProgress( + // Needed to get the original segment start and end positions in pixels. + List<Part> parts, + List<NotificationProgressDrawable.Part> drawableParts, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth); + + int iPartFirstSegmentToStyle = -1; + int iPartSegmentToSplit = -1; + float rescaledProgressX = 0; + float startFraction = 0; + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + if (!(part instanceof Segment)) continue; + final Segment segment = (Segment) part; + if (startFraction == progressFraction) { + iPartFirstSegmentToStyle = iPart; + rescaledProgressX = segment.mStart; + break; + } else if (startFraction < progressFraction + && progressFraction < startFraction + segment.mFraction) { + iPartSegmentToSplit = iPart; + rescaledProgressX = + segment.mStart + (progressFraction - startFraction) / segment.mFraction + * segment.getWidth(); + break; + } + startFraction += segment.mFraction; + } + + if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX); + + List<NotificationProgressDrawable.Part> splitDrawableParts = new ArrayList<>(); + boolean styleRemainingParts = false; + for (int iPart = 0; iPart < nParts; iPart++) { + final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) { + final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts); + splitDrawableParts.add( + new NotificationProgressDrawable.Point(drawablePoint.getStart(), + drawablePoint.getEnd(), color)); + } + if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true; + if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) { + if (iPart == iPartSegmentToSplit) { + if (rescaledProgressX <= drawableSegment.getStart()) { + styleRemainingParts = true; + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + drawableSegment.getEnd(), + color, true)); + } else if (drawableSegment.getStart() < rescaledProgressX + && rescaledProgressX < drawableSegment.getEnd()) { + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + rescaledProgressX - progressGap, + drawableSegment.getColor())); + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(rescaledProgressX, + drawableSegment.getEnd(), color, true)); + styleRemainingParts = true; + } else { + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + drawableSegment.getEnd(), + drawableSegment.getColor())); + styleRemainingParts = true; + } + } else { + final int color = maybeGetFadedColor(drawableSegment.getColor(), + styleRemainingParts); + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + drawableSegment.getEnd(), + color, styleRemainingParts)); + } + } + } + + return new Pair<>(splitDrawableParts, rescaledProgressX); + } + + /** + * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a + * {@link Point} with zero length. + */ + // TODO: b/372908709 - maybe this should be made private? Only test the final + // NotificationDrawable.Parts. + // TODO: b/372908709 - rename to BarPart, BarSegment, BarPoint. This avoids naming ambiguity + // with the types in NotificationProgressDrawable. + public interface Part { + } + + /** + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + * + */ + public static final class Segment implements Part { + private final float mFraction; + @ColorInt private final int mColor; + /** Whether the segment is faded or not. + * <p> + * <pre> + * When mFaded is set to true, a combination of the following is done to the segment: + * 1. The drawing color is mColor with opacity updated to 40%. + * 2. The gap between faded and non-faded segments is: + * - the segment-segment gap, when there is no tracker icon + * - 0, when there is tracker icon + * </pre> + * </p> + */ + private final boolean mFaded; + + /** Start position (in pixels) */ + private float mStart; + /** End position (in pixels */ + private float mEnd; + + public Segment(float fraction, @ColorInt int color) { + this(fraction, color, false); + } + + public Segment(float fraction, @ColorInt int color, boolean faded) { + mFraction = fraction; + mColor = color; + mFaded = faded; + } + + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; + } + + @Override + public String toString() { + return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" + + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd; + } + + // Needed for unit tests + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Segment that = (Segment) other; + if (Float.compare(this.mFraction, that.mFraction) != 0) return false; + if (this.mColor != that.mColor) return false; + return this.mFaded == that.mFaded; + } + + @Override + public int hashCode() { + return Objects.hash(mFraction, mColor, mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class Point implements Part { + @ColorInt private final int mColor; + + public Point(@ColorInt int color) { + mColor = color; + } + + @Override + public String toString() { + return "Point(color=" + this.mColor + ")"; + } + + // Needed for unit tests. + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Point that = (Point) other; + + return this.mColor == that.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mColor); + } + } } diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index 8629a1c95202..ef0a5d5cdec2 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -21,7 +21,6 @@ import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -49,7 +48,8 @@ import java.util.Objects; /** * This is used by NotificationProgressBar for displaying a custom background. It composes of - * segments, which have non-zero length, and points, which have zero length. + * segments, which have non-zero length varying drawing width, and points, which have zero length + * and fixed size for drawing. * * @see Segment * @see Point @@ -57,14 +57,15 @@ import java.util.Objects; public final class NotificationProgressDrawable extends Drawable { private static final String TAG = "NotifProgressDrawable"; + @Nullable + private BoundsChangeListener mBoundsChangeListener = null; + private State mState; private boolean mMutated; private final ArrayList<Part> mParts = new ArrayList<>(); - private boolean mHasTrackerIcon; private final RectF mSegRectF = new RectF(); - private final Rect mPointRect = new Rect(); private final RectF mPointRectF = new RectF(); private final Paint mFillPaint = new Paint(); @@ -80,27 +81,31 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * <p>Set the segment default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the stroke - * @see #mutate() + * Returns the gap between two segments. */ - public void setSegmentDefaultColor(@ColorInt int color) { - mState.setSegmentColor(color); + public float getSegSegGap() { + return mState.mSegSegGap; } /** - * <p>Set the point rect default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the point rect - * @see #mutate() + * Returns the gap between a segment and a point. + */ + public float getSegPointGap() { + return mState.mSegPointGap; + } + + /** + * Returns the gap between a segment and a point. */ - public void setPointRectDefaultColor(@ColorInt int color) { - mState.setPointRectColor(color); + public float getSegmentMinWidth() { + return mState.mSegmentMinWidth; + } + + /** + * Returns the radius for the points. + */ + public float getPointRadius() { + return mState.mPointRadius; } /** @@ -120,47 +125,18 @@ public final class NotificationProgressDrawable extends Drawable { setParts(Arrays.asList(parts)); } - /** - * Set whether a tracker is drawn on top of this NotificationProgressDrawable. - */ - public void setHasTrackerIcon(boolean hasTrackerIcon) { - if (mHasTrackerIcon != hasTrackerIcon) { - mHasTrackerIcon = hasTrackerIcon; - invalidateSelf(); - } - } - @Override public void draw(@NonNull Canvas canvas) { - final float pointRadius = - mState.mPointRadius; // how big the point icon will be, halved - - // generally, we will start drawing at (x, y) and end at (x+w, y) - float x = (float) getBounds().left; + final float pointRadius = mState.mPointRadius; + final float left = (float) getBounds().left; final float centerY = (float) getBounds().centerY(); - final float totalWidth = (float) getBounds().width(); - float segPointGap = mState.mSegPointGap; final int numParts = mParts.size(); for (int iPart = 0; iPart < numParts; iPart++) { final Part part = mParts.get(iPart); - final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1); - final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1); + final float start = left + part.mStart; + final float end = left + part.mEnd; if (part instanceof Segment segment) { - final float segWidth = segment.mFraction * totalWidth; - // Advance the start position to account for a point immediately prior. - final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); - final float start = x + startOffset; - // Retract the end position to account for the padding and a point immediately - // after. - final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, - mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon); - final float end = x + segWidth - endOffset; - - // Advance the current position to account for the segment's fraction of the total - // width (ignoring offset and padding) - x += segWidth; - // No space left to draw the segment if (start > end) continue; @@ -168,67 +144,23 @@ public final class NotificationProgressDrawable extends Drawable { : mState.mSegmentHeight / 2F; final float cornerRadius = mState.mSegmentCornerRadius; - mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor - : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor)); + mFillPaint.setColor(segment.mColor); mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY); canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint); } else if (part instanceof Point point) { - final float pointWidth = 2 * pointRadius; - float start = x - pointRadius; - if (start < 0) start = 0; - float end = start + pointWidth; - if (end > totalWidth) { - end = totalWidth; - if (totalWidth > pointWidth) start = totalWidth - pointWidth; - } - mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end, - (int) (centerY + pointRadius)); - - if (point.mIcon != null) { - point.mIcon.setBounds(mPointRect); - point.mIcon.draw(canvas); - } else { - // TODO: b/367804171 - actually use a vector asset for the default point - // rather than drawing it as a box? - mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); - final float inset = mState.mPointRectInset; - final float cornerRadius = mState.mPointRectCornerRadius; - mPointRectF.inset(inset, inset); - - mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor - : (point.mFaded ? mState.mFadedPointRectColor - : mState.mPointRectColor)); - - canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); - } - } - } - } + // TODO: b/367804171 - actually use a vector asset for the default point + // rather than drawing it as a box? + mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); + final float inset = mState.mPointRectInset; + final float cornerRadius = mState.mPointRectCornerRadius; + mPointRectF.inset(inset, inset); - private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, - float startX) { - if (!(prevPart instanceof Point)) return 0F; - final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; - return pointOffset + pointRadius + segPointGap; - } + mFillPaint.setColor(point.mColor); - private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, - float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { - if (nextPart == null) return 0F; - if (nextPart instanceof Segment nextSeg) { - if (!seg.mFaded && nextSeg.mFaded) { - // @see Segment#mFaded - return hasTrackerIcon ? 0F : segSegGap; + canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); } - return segSegGap; } - - final float pointWidth = 2 * pointRadius; - final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) - ? (endX + pointRadius - totalWidth) : 0; - return segPointGap + pointRadius + pointOffset; } @Override @@ -260,6 +192,19 @@ public final class NotificationProgressDrawable extends Drawable { return PixelFormat.UNKNOWN; } + public void setBoundsChangeListener(BoundsChangeListener listener) { + mBoundsChangeListener = listener; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (mBoundsChangeListener != null) { + mBoundsChangeListener.onDrawableBoundsChanged(); + } + } + @Override public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) @@ -384,6 +329,8 @@ public final class NotificationProgressDrawable extends Drawable { // Extract the theme attributes, if any. state.mThemeAttrsSegments = a.extractThemeAttrs(); + state.mSegmentMinWidth = a.getDimension( + R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth); state.mSegmentHeight = a.getDimension( R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight); state.mFadedSegmentHeight = a.getDimension( @@ -392,9 +339,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mSegmentCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawableSegments_cornerRadius, state.mSegmentCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color, - state.mSegmentColor); - setSegmentDefaultColor(color); } private void updatePointsFromTypedArray(TypedArray a) { @@ -413,9 +357,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mPointRectCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawablePoints_cornerRadius, state.mPointRectCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color, - state.mPointRectColor); - setPointRectDefaultColor(color); } static int resolveDensity(@Nullable Resources r, int parentDensity) { @@ -464,63 +405,56 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a - * {@link Point} with zero length. + * Listener to receive updates about drawable bounds changing */ - public interface Part { + public interface BoundsChangeListener { + /** Called when bounds have changed */ + void onDrawableBoundsChanged(); } /** - * A segment is a part of the progress bar with non-zero length. For example, it can - * represent a portion in a navigation journey with certain traffic condition. - * + * A part of the progress bar, which is either a {@link Segment} with non-zero length and + * varying drawing width, or a {@link Point} with zero length and fixed size for drawing. */ - public static final class Segment implements Part { - private final float mFraction; - @ColorInt private final int mColor; - /** Whether the segment is faded or not. - * <p> - * <pre> - * When mFaded is set to true, a combination of the following is done to the segment: - * 1. The drawing color is mColor with opacity updated to 40%. - * 2. The gap between faded and non-faded segments is: - * - the segment-segment gap, when there is no tracker icon - * - 0, when there is tracker icon - * </pre> - * </p> - */ - private final boolean mFaded; - - public Segment(float fraction) { - this(fraction, Color.TRANSPARENT); + public abstract static class Part { + // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the + // bounds rect. + /** Start position for drawing (in pixels) */ + protected float mStart; + /** End position for drawing (in pixels) */ + protected float mEnd; + /** Drawing color. */ + @ColorInt protected final int mColor; + + protected Part(float start, float end, @ColorInt int color) { + mStart = start; + mEnd = end; + mColor = color; } - public Segment(float fraction, @ColorInt int color) { - this(fraction, color, false); + public float getStart() { + return this.mStart; } - public Segment(float fraction, @ColorInt int color, boolean faded) { - mFraction = fraction; - mColor = color; - mFaded = faded; + public void setStart(float start) { + mStart = start; } - public float getFraction() { - return this.mFraction; + public float getEnd() { + return this.mEnd; } - public int getColor() { - return this.mColor; + public void setEnd(float end) { + mEnd = end; } - public boolean getFaded() { - return this.mFaded; + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; } - @Override - public String toString() { - return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" - + this.mFaded + ')'; + public int getColor() { + return this.mColor; } // Needed for unit tests @@ -530,80 +464,79 @@ public final class NotificationProgressDrawable extends Drawable { if (other == null || getClass() != other.getClass()) return false; - Segment that = (Segment) other; - if (Float.compare(this.mFraction, that.mFraction) != 0) return false; - if (this.mColor != that.mColor) return false; - return this.mFaded == that.mFaded; + Part that = (Part) other; + if (Float.compare(this.mStart, that.mStart) != 0) return false; + if (Float.compare(this.mEnd, that.mEnd) != 0) return false; + return this.mColor == that.mColor; } @Override public int hashCode() { - return Objects.hash(mFraction, mColor, mFaded); + return Objects.hash(mStart, mEnd, mColor); } } /** - * A point is a part of the progress bar with zero length. Points are designated points within a - * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop - * ride-share journey. + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + * <p> + * The start and end positions for drawing a segment are assumed to have been adjusted for + * the Points and gaps neighboring the segment. + * </p> */ - public static final class Point implements Part { - @Nullable - private final Drawable mIcon; - @ColorInt private final int mColor; + public static final class Segment extends Part { + /** + * Whether the segment is faded or not. + * <p> + * Faded segments and non-faded segments are drawn with different heights. + * </p> + */ private final boolean mFaded; - public Point(@Nullable Drawable icon) { - this(icon, Color.TRANSPARENT, false); - } - - public Point(@Nullable Drawable icon, @ColorInt int color) { - this(icon, color, false); - + public Segment(float start, float end, int color) { + this(start, end, color, false); } - public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) { - mIcon = icon; - mColor = color; + public Segment(float start, float end, int color, boolean faded) { + super(start, end, color); mFaded = faded; } - @Nullable - public Drawable getIcon() { - return this.mIcon; - } - - public int getColor() { - return this.mColor; - } - - public boolean getFaded() { - return this.mFaded; - } - @Override public String toString() { - return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded - + ")"; + return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ", faded=" + this.mFaded + ')'; } // Needed for unit tests. @Override public boolean equals(@Nullable Object other) { - if (this == other) return true; - - if (other == null || getClass() != other.getClass()) return false; - - Point that = (Point) other; + if (!super.equals(other)) return false; - if (!Objects.equals(this.mIcon, that.mIcon)) return false; - if (this.mColor != that.mColor) return false; + Segment that = (Segment) other; return this.mFaded == that.mFaded; } @Override public int hashCode() { - return Objects.hash(mIcon, mColor, mFaded); + return Objects.hash(super.hashCode(), mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class Point extends Part { + public Point(float start, float end, int color) { + super(start, end, color); + } + + @Override + public String toString() { + return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ")"; } } @@ -628,16 +561,14 @@ public final class NotificationProgressDrawable extends Drawable { int mChangingConfigurations; float mSegSegGap = 0.0f; float mSegPointGap = 0.0f; + float mSegmentMinWidth = 0.0f; float mSegmentHeight; float mFadedSegmentHeight; float mSegmentCornerRadius; - int mSegmentColor; - int mFadedSegmentColor; + // how big the point icon will be, halved float mPointRadius; float mPointRectInset; float mPointRectCornerRadius; - int mPointRectColor; - int mFadedPointRectColor; int[] mThemeAttrs; int[] mThemeAttrsSegments; @@ -652,16 +583,13 @@ public final class NotificationProgressDrawable extends Drawable { mChangingConfigurations = orig.mChangingConfigurations; mSegSegGap = orig.mSegSegGap; mSegPointGap = orig.mSegPointGap; + mSegmentMinWidth = orig.mSegmentMinWidth; mSegmentHeight = orig.mSegmentHeight; mFadedSegmentHeight = orig.mFadedSegmentHeight; mSegmentCornerRadius = orig.mSegmentCornerRadius; - mSegmentColor = orig.mSegmentColor; - mFadedSegmentColor = orig.mFadedSegmentColor; mPointRadius = orig.mPointRadius; mPointRectInset = orig.mPointRectInset; mPointRectCornerRadius = orig.mPointRectCornerRadius; - mPointRectColor = orig.mPointRectColor; - mFadedPointRectColor = orig.mFadedPointRectColor; mThemeAttrs = orig.mThemeAttrs; mThemeAttrsSegments = orig.mThemeAttrsSegments; @@ -674,6 +602,18 @@ public final class NotificationProgressDrawable extends Drawable { } private void applyDensityScaling(int sourceDensity, int targetDensity) { + if (mSegSegGap > 0) { + mSegSegGap = scaleFromDensity( + mSegSegGap, sourceDensity, targetDensity); + } + if (mSegPointGap > 0) { + mSegPointGap = scaleFromDensity( + mSegPointGap, sourceDensity, targetDensity); + } + if (mSegmentMinWidth > 0) { + mSegmentMinWidth = scaleFromDensity( + mSegmentMinWidth, sourceDensity, targetDensity); + } if (mSegmentHeight > 0) { mSegmentHeight = scaleFromDensity( mSegmentHeight, sourceDensity, targetDensity); @@ -740,28 +680,6 @@ public final class NotificationProgressDrawable extends Drawable { applyDensityScaling(sourceDensity, targetDensity); } } - - public void setSegmentColor(int color) { - mSegmentColor = color; - mFadedSegmentColor = getFadedColor(color); - } - - public void setPointRectColor(int color) { - mPointRectColor = color; - mFadedPointRectColor = getFadedColor(color); - } - } - - /** - * Get a color with an opacity that's 25% of the input color. - */ - @ColorInt - static int getFadedColor(@ColorInt int color) { - return Color.argb( - (int) (Color.alpha(color) * 0.4f + 0.5f), - Color.red(color), - Color.green(color), - Color.blue(color)); } @Override diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml index 5d272fb00e34..ff5450ee106f 100644 --- a/core/res/res/drawable/notification_progress.xml +++ b/core/res/res/drawable/notification_progress.xml @@ -24,6 +24,7 @@ android:segPointGap="@dimen/notification_progress_segPoint_gap"> <segments android:color="?attr/colorProgressBackgroundNormal" + android:minWidth="@dimen/notification_progress_segments_min_width" android:height="@dimen/notification_progress_segments_height" android:fadedHeight="@dimen/notification_progress_segments_faded_height" android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 728c856f5855..8372aecf0d27 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7572,25 +7572,31 @@ <!-- NotificationProgressDrawable class --> <!-- ================================== --> - <!-- Drawable used to render a segmented bar, with segments and points. --> + <!-- Drawable used to render a notification progress bar, with segments and points. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawable"> - <!-- Default color for the parts. --> + <!-- The gap between two segments. --> <attr name="segSegGap" format="dimension" /> + <!-- The gap between a segment and a point. --> <attr name="segPointGap" format="dimension" /> </declare-styleable> <!-- Used to config the segments of a NotificationProgressDrawable. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawableSegments"> - <!-- Height of the solid segments --> + <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only + place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap + above. --> + <!-- Minimum required drawing width. The drawing width refers to the width after + the original segments have been adjusted for the neighboring Points and gaps. This is + enforced by stretching the segments that are too short. --> + <attr name="minWidth" format="dimension" /> + <!-- Height of the solid segments. --> <attr name="height" /> - <!-- Height of the faded segments --> - <attr name="fadedHeight" format="dimension"/> + <!-- Height of the faded segments. --> + <attr name="fadedHeight" format="dimension" /> <!-- Corner radius of the segment rect. --> <attr name="cornerRadius" format="dimension" /> - <!-- Default color of the segment. --> - <attr name="color" /> </declare-styleable> <!-- Used to config the points of a NotificationProgressDrawable. --> @@ -7602,8 +7608,6 @@ <attr name="inset" /> <!-- Corner radius of the point rect. --> <attr name="cornerRadius"/> - <!-- Default color of the point rect. --> - <attr name="color" /> </declare-styleable> <!-- ========================== --> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 4f7351c7cc4d..d6b8704a978b 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -899,6 +899,8 @@ <dimen name="notification_progress_segSeg_gap">4dp</dimen> <!-- The gap between a segment and a point in the notification progress bar --> <dimen name="notification_progress_segPoint_gap">4dp</dimen> + <!-- The minimum required drawing width of the notification progress bar segments --> + <dimen name="notification_progress_segments_min_width">16dp</dimen> <!-- The height of the notification progress bar segments --> <dimen name="notification_progress_segments_height">6dp</dimen> <!-- The height of the notification progress bar faded segments --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f89ca44cce30..6c014e93d4cc 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3950,6 +3950,7 @@ <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_progress_segSeg_gap" /> <java-symbol type="dimen" name="notification_progress_segPoint_gap" /> + <java-symbol type="dimen" name="notification_progress_segments_min_width" /> <java-symbol type="dimen" name="notification_progress_segments_height" /> <java-symbol type="dimen" name="notification_progress_segments_faded_height" /> <java-symbol type="dimen" name="notification_progress_segments_corner_radius" /> diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index d26bb35e5481..f105ec305eab 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -20,12 +20,13 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Notification.ProgressStyle; import android.graphics.Color; +import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressBar.Part; +import com.android.internal.widget.NotificationProgressBar.Point; +import com.android.internal.widget.NotificationProgressBar.Segment; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,183 +38,303 @@ import java.util.List; public class NotificationProgressBarTest { @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsIsEmpty() { + public void processAndConvertToParts_segmentsIsEmpty() { List<ProgressStyle.Segment> segments = new ArrayList<>(); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() { + public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsNegative() { + public void processAndConvertToParts_segmentLengthIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(-50)); segments.add(new ProgressStyle.Segment(150)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsZero() { + public void processAndConvertToParts_segmentLengthIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(0)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressIsNegative() { + public void processAndConvertToParts_progressIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = -50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_progressIsZero() { + public void processAndConvertToParts_progressIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 0; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedRed = 0x66FF0000; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 300, fadedRed, true))); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true))); - - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(0); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_progressAtMax() { + public void processAndConvertToParts_progressAtMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 100; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); - assertThat(parts).isEqualTo(expected); + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + assertThat(p.second).isEqualTo(300); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressAboveMax() { + public void processAndConvertToParts_progressAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 150; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionIsNegative() { + public void processAndConvertToParts_pointPositionIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(-50).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionAboveMax() { + public void processAndConvertToParts_pointPositionAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(150).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() { + public void processAndConvertToParts_multipleSegmentsWithoutPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Segment(0.50f, Color.RED), + new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 180, Color.GREEN), + new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); + segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); + List<ProgressStyle.Point> points = new ArrayList<>(); + int progress = 60; + int progressMax = 100; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.50f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Segment(0.40f, fadedGreen, true))); + new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = false; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 176, Color.GREEN), + new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true))); - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_singleSegmentWithPoints() { + public void processAndConvertToParts_singleSegmentWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); @@ -223,31 +344,77 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - // Colors with 40% opacity - int fadedBlue = 0x660000FF; - int fadedYellow = 0x66FFFF00; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.15f, Color.BLUE), - new Point(null, Color.RED), + new Point(Color.RED), new Segment(0.10f, Color.BLUE), - new Point(null, Color.BLUE), + new Point(Color.BLUE), new Segment(0.35f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedBlue, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedBlue, true))); + new Point(Color.BLUE), + new Segment(0.15f, Color.BLUE), + new Point(Color.YELLOW), + new Segment(0.25f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 35, Color.BLUE), + new NotificationProgressDrawable.Point(39, 51, Color.RED), + new NotificationProgressDrawable.Segment(55, 65, Color.BLUE), + new NotificationProgressDrawable.Point(69, 81, Color.BLUE), + new NotificationProgressDrawable.Segment(85, 170, Color.BLUE), + new NotificationProgressDrawable.Point(174, 186, Color.BLUE), + new NotificationProgressDrawable.Segment(190, 215, Color.BLUE), + new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), + new NotificationProgressDrawable.Segment(235, 300, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + // Colors with 40% opacity + int fadedBlue = 0x660000FF; + int fadedYellow = 0x66FFFF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 34.219177F, Color.BLUE), + new NotificationProgressDrawable.Point(38.219177F, 50.219177F, Color.RED), + new NotificationProgressDrawable.Segment(54.219177F, 70.21918F, Color.BLUE), + new NotificationProgressDrawable.Point(74.21918F, 86.21918F, Color.BLUE), + new NotificationProgressDrawable.Segment(90.21918F, 172.38356F, Color.BLUE), + new NotificationProgressDrawable.Point(176.38356F, 188.38356F, Color.BLUE), + new NotificationProgressDrawable.Segment(192.38356F, 217.0137F, fadedBlue, + true), + new NotificationProgressDrawable.Point(221.0137F, 233.0137F, fadedYellow), + new NotificationProgressDrawable.Segment(237.0137F, 300F, fadedBlue, + true))); + + assertThat(p.second).isEqualTo(182.38356F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() { + public void processAndConvertToParts_multipleSegmentsWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -258,32 +425,81 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); - // Colors with 40% opacity - int fadedGreen = 0x6600FF00; - int fadedYellow = 0x66FFFF00; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.15f, Color.RED), - new Point(null, Color.RED), + new Point(Color.RED), new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), + new Point(Color.BLUE), new Segment(0.25f, Color.RED), new Segment(0.10f, Color.GREEN), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedGreen, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedGreen, true))); + new Point(Color.BLUE), + new Segment(0.15f, Color.GREEN), + new Point(Color.YELLOW), + new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED), + new NotificationProgressDrawable.Point(39, 51, Color.RED), + new NotificationProgressDrawable.Segment(55, 65, Color.RED), + new NotificationProgressDrawable.Point(69, 81, Color.BLUE), + new NotificationProgressDrawable.Segment(85, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 170, Color.GREEN), + new NotificationProgressDrawable.Point(174, 186, Color.BLUE), + new NotificationProgressDrawable.Segment(190, 215, Color.GREEN), + new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), + new NotificationProgressDrawable.Segment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + int fadedYellow = 0x66FFFF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 34.095238F, Color.RED), + new NotificationProgressDrawable.Point(38.095238F, 50.095238F, Color.RED), + new NotificationProgressDrawable.Segment(54.095238F, 70.09524F, Color.RED), + new NotificationProgressDrawable.Point(74.09524F, 86.09524F, Color.BLUE), + new NotificationProgressDrawable.Segment(90.09524F, 148.9524F, Color.RED), + new NotificationProgressDrawable.Segment(152.95238F, 172.7619F, + Color.GREEN), + new NotificationProgressDrawable.Point(176.7619F, 188.7619F, Color.BLUE), + new NotificationProgressDrawable.Segment(192.7619F, 217.33333F, + fadedGreen, true), + new NotificationProgressDrawable.Point(221.33333F, 233.33333F, + fadedYellow), + new NotificationProgressDrawable.Segment(237.33333F, 299.99997F, + fadedGreen, true))); + + assertThat(p.second).isEqualTo(182.7619F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() { + public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -293,21 +509,251 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = false; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.15f, Color.RED), - new Point(null, Color.RED), + new Point(Color.RED), new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), + new Point(Color.BLUE), new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), - new Point(null, Color.YELLOW), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); - assertThat(parts).isEqualTo(expected); + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED), + new NotificationProgressDrawable.Point(39, 51, Color.RED), + new NotificationProgressDrawable.Segment(55, 65, Color.RED), + new NotificationProgressDrawable.Point(69, 81, Color.BLUE), + new NotificationProgressDrawable.Segment(85, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 215, Color.GREEN), + new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), + new NotificationProgressDrawable.Segment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = false; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 34.296295F, Color.RED), + new NotificationProgressDrawable.Point(38.296295F, 50.296295F, Color.RED), + new NotificationProgressDrawable.Segment(54.296295F, 70.296295F, Color.RED), + new NotificationProgressDrawable.Point(74.296295F, 86.296295F, Color.BLUE), + new NotificationProgressDrawable.Segment(90.296295F, 149.62962F, Color.RED), + new NotificationProgressDrawable.Segment(153.62962F, 216.8148F, + Color.GREEN), + new NotificationProgressDrawable.Point(220.81482F, 232.81482F, + Color.YELLOW), + new NotificationProgressDrawable.Segment(236.81482F, 300, Color.GREEN))); + + assertThat(p.second).isEqualTo(182.9037F); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `zeroWidthDrawableSegment` test below is the longer + // segmentMinWidth (= 16dp). + @Test + public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 16, Color.BLUE), + new NotificationProgressDrawable.Segment(20, 56, Color.BLUE), + new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), + new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 200, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 32, Color.BLUE), + new NotificationProgressDrawable.Segment(36, 69.41936F, Color.BLUE), + new NotificationProgressDrawable.Segment(73.41936F, 124.25807F, Color.BLUE), + new NotificationProgressDrawable.Segment(128.25807F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `negativeWidthDrawableSegment` test above is the shorter + // segmentMinWidth (= 10dp). + @Test + public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 16, Color.BLUE), + new NotificationProgressDrawable.Segment(20, 56, Color.BLUE), + new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), + new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 200, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 26, Color.BLUE), + new NotificationProgressDrawable.Segment(30, 64.169014F, Color.BLUE), + new NotificationProgressDrawable.Segment(68.169014F, 120.92958F, + Color.BLUE), + new NotificationProgressDrawable.Segment(124.92958F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void maybeStretchAndRescaleSegments_noStretchingNecessary() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Point(Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 36, Color.BLUE), + new NotificationProgressDrawable.Segment(40, 56, Color.BLUE), + new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), + new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 200, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); } } |