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.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Notification.ProgressStyle; 22 import android.content.Context; 23 import android.content.res.ColorStateList; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Matrix; 28 import android.graphics.Rect; 29 import android.graphics.drawable.Animatable2; 30 import android.graphics.drawable.AnimatedVectorDrawable; 31 import android.graphics.drawable.Drawable; 32 import android.graphics.drawable.Icon; 33 import android.graphics.drawable.LayerDrawable; 34 import android.os.Bundle; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.util.Pair; 38 import android.view.RemotableViewMethod; 39 import android.widget.ProgressBar; 40 import android.widget.RemoteViews; 41 42 import androidx.annotation.ColorInt; 43 44 import com.android.internal.R; 45 import com.android.internal.annotations.VisibleForTesting; 46 import com.android.internal.util.Preconditions; 47 import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; 48 import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; 49 import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; 50 51 import java.util.ArrayList; 52 import java.util.Collections; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Objects; 57 import java.util.SortedSet; 58 import java.util.TreeSet; 59 60 /** 61 * NotificationProgressBar extends the capabilities of ProgressBar by adding functionalities to 62 * represent Notification ProgressStyle progress, such as for ridesharing and navigation. 63 */ 64 @RemoteViews.RemoteView 65 public final class NotificationProgressBar extends ProgressBar implements 66 NotificationProgressDrawable.BoundsChangeListener { 67 private static final String TAG = "NotificationProgressBar"; 68 private static final boolean DEBUG = false; 69 private static final float FADED_OPACITY = 0.5f; 70 71 private Animatable2.AnimationCallback mIndeterminateAnimationCallback = null; 72 73 private NotificationProgressDrawable mNotificationProgressDrawable; 74 private final Rect mProgressDrawableBounds = new Rect(); 75 76 private NotificationProgressModel mProgressModel; 77 78 @Nullable 79 private List<Part> mParts = null; 80 81 // List of drawable parts before segment splitting by process. 82 @Nullable 83 private List<DrawablePart> mProgressDrawableParts = null; 84 85 /** @see R.styleable#NotificationProgressBar_segMinWidth */ 86 private final float mSegMinWidth; 87 /** @see R.styleable#NotificationProgressBar_segSegGap */ 88 private final float mSegSegGap; 89 /** @see R.styleable#NotificationProgressBar_segPointGap */ 90 private final float mSegPointGap; 91 92 @Nullable 93 private Drawable mTracker = null; 94 private boolean mHasTrackerIcon = false; 95 96 /** @see R.styleable#NotificationProgressBar_trackerHeight */ 97 private final int mTrackerHeight; 98 private int mTrackerDrawWidth = 0; 99 private int mTrackerPos; 100 private final Matrix mMatrix = new Matrix(); 101 private Matrix mTrackerDrawMatrix = null; 102 103 private float mProgressFraction = 0; 104 /** 105 * The location of progress on the stretched and rescaled progress bar, in fraction. Used for 106 * calculating the tracker position. If stretching and rescaling is not needed, == 107 * mProgressFraction. 108 */ 109 private float mAdjustedProgressFraction = 0; 110 /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */ 111 private boolean mTrackerPosIsDirty = false; 112 NotificationProgressBar(Context context)113 public NotificationProgressBar(Context context) { 114 this(context, null); 115 } 116 NotificationProgressBar(Context context, AttributeSet attrs)117 public NotificationProgressBar(Context context, AttributeSet attrs) { 118 this(context, attrs, R.attr.progressBarStyle); 119 } 120 NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr)121 public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { 122 this(context, attrs, defStyleAttr, 0); 123 } 124 NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)125 public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr, 126 int defStyleRes) { 127 super(context, attrs, defStyleAttr, defStyleRes); 128 129 final TypedArray a = context.obtainStyledAttributes(attrs, 130 R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); 131 saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a, 132 defStyleAttr, 133 defStyleRes); 134 135 try { 136 mNotificationProgressDrawable = getNotificationProgressDrawable(); 137 mNotificationProgressDrawable.setBoundsChangeListener(this); 138 } catch (IllegalStateException ex) { 139 Log.e(TAG, "Can't get NotificationProgressDrawable", ex); 140 } 141 142 mSegMinWidth = a.getDimension(R.styleable.NotificationProgressBar_segMinWidth, 0f); 143 mSegSegGap = a.getDimension(R.styleable.NotificationProgressBar_segSegGap, 0f); 144 mSegPointGap = a.getDimension(R.styleable.NotificationProgressBar_segPointGap, 0f); 145 146 // Supports setting the tracker in xml, but ProgressStyle notifications set/override it 147 // via {@code #setProgressTrackerIcon}. 148 final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker); 149 setTracker(tracker); 150 151 // If this is configured to be a non-zero size, will scale and crop the tracker drawable to 152 // ensure its aspect ratio is between 2:1 to 1:2. 153 mTrackerHeight = a.getDimensionPixelSize(R.styleable.NotificationProgressBar_trackerHeight, 154 0); 155 } 156 157 @Override setIndeterminateDrawable(Drawable d)158 public void setIndeterminateDrawable(Drawable d) { 159 final Drawable oldDrawable = getIndeterminateDrawable(); 160 if (oldDrawable != d) { 161 if (mIndeterminateAnimationCallback != null) { 162 ((AnimatedVectorDrawable) oldDrawable).unregisterAnimationCallback( 163 mIndeterminateAnimationCallback); 164 mIndeterminateAnimationCallback = null; 165 } 166 if (d instanceof AnimatedVectorDrawable) { 167 mIndeterminateAnimationCallback = new Animatable2.AnimationCallback() { 168 @Override 169 public void onAnimationEnd(Drawable drawable) { 170 super.onAnimationEnd(drawable); 171 172 if (shouldLoopIndeterminateAnimation()) { 173 ((AnimatedVectorDrawable) drawable).start(); 174 } 175 } 176 }; 177 ((AnimatedVectorDrawable) d).registerAnimationCallback( 178 mIndeterminateAnimationCallback); 179 } 180 } 181 182 super.setIndeterminateDrawable(d); 183 } 184 shouldLoopIndeterminateAnimation()185 private boolean shouldLoopIndeterminateAnimation() { 186 return isIndeterminate() && isAttachedToWindow() && isAggregatedVisible(); 187 } 188 189 /** 190 * Setter for the notification progress model. 191 * 192 * @see NotificationProgressModel#fromBundle 193 */ 194 @RemotableViewMethod setProgressModel(@ullable Bundle bundle)195 public void setProgressModel(@Nullable Bundle bundle) { 196 Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null"); 197 198 mProgressModel = NotificationProgressModel.fromBundle(bundle); 199 final boolean isIndeterminate = mProgressModel.isIndeterminate(); 200 setIndeterminate(isIndeterminate); 201 202 if (isIndeterminate) { 203 final int indeterminateColor = mProgressModel.getIndeterminateColor(); 204 setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor)); 205 } else { 206 // TODO: b/372908709 - maybe don't rerun the entire calculation every time the 207 // progress model is updated? For example, if the segments and parts aren't changed, 208 // there is no need to call `processModelAndConvertToViewParts` again. 209 210 final int progress = mProgressModel.getProgress(); 211 final int progressMax = mProgressModel.getProgressMax(); 212 213 mParts = processModelAndConvertToViewParts(mProgressModel.getSegments(), 214 mProgressModel.getPoints(), 215 progress, 216 progressMax); 217 218 setMax(progressMax); 219 setProgress(progress); 220 221 if (mNotificationProgressDrawable != null 222 && mNotificationProgressDrawable.getBounds().width() != 0) { 223 updateDrawableParts(); 224 } 225 } 226 } 227 228 @NonNull getNotificationProgressDrawable()229 private NotificationProgressDrawable getNotificationProgressDrawable() { 230 final Drawable d = getProgressDrawable(); 231 if (d == null) { 232 throw new IllegalStateException("getProgressDrawable() returns null"); 233 } 234 if (!(d instanceof LayerDrawable)) { 235 throw new IllegalStateException("getProgressDrawable() doesn't return a LayerDrawable"); 236 } 237 238 final Drawable layer = ((LayerDrawable) d).findDrawableByLayerId(R.id.background); 239 if (!(layer instanceof NotificationProgressDrawable)) { 240 throw new IllegalStateException( 241 "Couldn't get NotificationProgressDrawable, retrieved drawable is: " + ( 242 layer != null ? layer.toString() : null)); 243 } 244 245 return (NotificationProgressDrawable) layer; 246 } 247 248 /** 249 * Setter for the progress tracker icon. 250 * 251 * @see #setProgressTrackerIconAsync 252 */ 253 @RemotableViewMethod(asyncImpl = "setProgressTrackerIconAsync") setProgressTrackerIcon(@ullable Icon icon)254 public void setProgressTrackerIcon(@Nullable Icon icon) { 255 final Drawable progressTrackerDrawable; 256 if (icon != null) { 257 progressTrackerDrawable = icon.loadDrawable(getContext()); 258 } else { 259 progressTrackerDrawable = null; 260 } 261 setTracker(progressTrackerDrawable); 262 } 263 264 /** 265 * Async version of {@link #setProgressTrackerIcon} 266 */ setProgressTrackerIconAsync(@ullable Icon icon)267 public Runnable setProgressTrackerIconAsync(@Nullable Icon icon) { 268 final Drawable progressTrackerDrawable; 269 if (icon != null) { 270 progressTrackerDrawable = icon.loadDrawable(getContext()); 271 } else { 272 progressTrackerDrawable = null; 273 } 274 return () -> setTracker(progressTrackerDrawable); 275 } 276 setTracker(@ullable Drawable tracker)277 private void setTracker(@Nullable Drawable tracker) { 278 if (tracker == mTracker) return; 279 280 if (mTracker != null) { 281 mTracker.setCallback(null); 282 } 283 284 if (tracker != null) { 285 tracker.setCallback(this); 286 if (getMirrorForRtl()) { 287 tracker.setAutoMirrored(true); 288 } 289 290 if (canResolveLayoutDirection()) { 291 tracker.setLayoutDirection(getLayoutDirection()); 292 } 293 } 294 295 final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker); 296 297 mTracker = tracker; 298 final boolean hasTrackerIcon = (mTracker != null); 299 if (mHasTrackerIcon != hasTrackerIcon) { 300 mHasTrackerIcon = hasTrackerIcon; 301 if (mNotificationProgressDrawable != null 302 && mNotificationProgressDrawable.getBounds().width() != 0 303 && mProgressModel.isStyledByProgress()) { 304 updateDrawableParts(); 305 } 306 } 307 308 configureTrackerBounds(); 309 updateTrackerAndBarPos(getWidth(), getHeight()); 310 311 // Change in tracker size may lead to change in measured view size. 312 // @see #onMeasure. 313 if (trackerSizeChanged) requestLayout(); 314 315 invalidate(); 316 317 if (tracker != null && tracker.isStateful()) { 318 // Note that if the states are different this won't work. 319 // For now, let's consider that an app bug. 320 tracker.setState(getDrawableState()); 321 } 322 } 323 trackerSizeChanged(@ullable Drawable newTracker, @Nullable Drawable oldTracker)324 private static boolean trackerSizeChanged(@Nullable Drawable newTracker, 325 @Nullable Drawable oldTracker) { 326 if (newTracker == null && oldTracker == null) return false; 327 if (newTracker == null && oldTracker != null) return true; 328 if (newTracker != null && oldTracker == null) return true; 329 330 return newTracker.getIntrinsicWidth() != oldTracker.getIntrinsicWidth() 331 || newTracker.getIntrinsicHeight() != oldTracker.getIntrinsicHeight(); 332 } 333 configureTrackerBounds()334 private void configureTrackerBounds() { 335 // Reset the tracker draw matrix to null 336 mTrackerDrawMatrix = null; 337 mTrackerDrawWidth = 0; 338 339 if (mTracker == null) return; 340 if (mTrackerHeight <= 0) { 341 mTrackerDrawWidth = mTracker.getIntrinsicWidth(); 342 return; 343 } 344 345 final int dWidth = mTracker.getIntrinsicWidth(); 346 final int dHeight = mTracker.getIntrinsicHeight(); 347 if (dWidth <= 0 || dHeight <= 0) { 348 return; 349 } 350 final int maxDWidth = dHeight * 2; 351 final int maxDHeight = dWidth * 2; 352 353 mTrackerDrawMatrix = mMatrix; 354 float scale; 355 float dx = 0, dy = 0; 356 357 if (dWidth > maxDWidth) { 358 scale = (float) mTrackerHeight / (float) dHeight; 359 dx = (maxDWidth * scale - dWidth * scale) * 0.5f; 360 mTrackerDrawWidth = (int) (maxDWidth * scale); 361 } else if (dHeight > maxDHeight) { 362 scale = (float) mTrackerHeight * 0.5f / (float) dWidth; 363 dy = (maxDHeight * scale - dHeight * scale) * 0.5f; 364 mTrackerDrawWidth = mTrackerHeight / 2; 365 } else { 366 scale = (float) mTrackerHeight / (float) dHeight; 367 mTrackerDrawWidth = (int) (dWidth * scale); 368 } 369 370 mTrackerDrawMatrix.setScale(scale, scale); 371 mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); 372 } 373 374 // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't 375 // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. 376 @Override setProgress(int progress)377 public synchronized void setProgress(int progress) { 378 super.setProgress(progress); 379 380 onMaybeVisualProgressChanged(); 381 } 382 383 // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't 384 // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. 385 @Override setProgress(int progress, boolean animate)386 public void setProgress(int progress, boolean animate) { 387 // Animation isn't supported by NotificationProgressBar. 388 super.setProgress(progress, false); 389 390 onMaybeVisualProgressChanged(); 391 } 392 393 // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't 394 // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. 395 @Override setMin(int min)396 public synchronized void setMin(int min) { 397 super.setMin(min); 398 399 onMaybeVisualProgressChanged(); 400 } 401 402 // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't 403 // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. 404 @Override setMax(int max)405 public synchronized void setMax(int max) { 406 super.setMax(max); 407 408 onMaybeVisualProgressChanged(); 409 } 410 onMaybeVisualProgressChanged()411 private void onMaybeVisualProgressChanged() { 412 float progressFraction = getProgressFraction(); 413 if (mProgressFraction == progressFraction) return; 414 415 mProgressFraction = progressFraction; 416 mTrackerPosIsDirty = true; 417 invalidate(); 418 } 419 420 @Override verifyDrawable(@onNull Drawable who)421 protected boolean verifyDrawable(@NonNull Drawable who) { 422 return who == mTracker || super.verifyDrawable(who); 423 } 424 425 @Override jumpDrawablesToCurrentState()426 public void jumpDrawablesToCurrentState() { 427 super.jumpDrawablesToCurrentState(); 428 429 if (mTracker != null) { 430 mTracker.jumpToCurrentState(); 431 } 432 } 433 434 @Override drawableStateChanged()435 protected void drawableStateChanged() { 436 super.drawableStateChanged(); 437 438 final Drawable tracker = mTracker; 439 if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) { 440 invalidateDrawable(tracker); 441 } 442 } 443 444 @Override drawableHotspotChanged(float x, float y)445 public void drawableHotspotChanged(float x, float y) { 446 super.drawableHotspotChanged(x, y); 447 448 if (mTracker != null) { 449 mTracker.setHotspot(x, y); 450 } 451 } 452 453 @Override onSizeChanged(int w, int h, int oldw, int oldh)454 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 455 super.onSizeChanged(w, h, oldw, oldh); 456 457 updateTrackerAndBarPos(w, h); 458 } 459 460 @Override onDrawableBoundsChanged()461 public void onDrawableBoundsChanged() { 462 final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds(); 463 464 if (mProgressDrawableBounds.equals(progressDrawableBounds)) return; 465 466 if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) { 467 updateDrawableParts(); 468 } 469 470 mProgressDrawableBounds.set(progressDrawableBounds); 471 } 472 updateDrawableParts()473 private void updateDrawableParts() { 474 if (DEBUG) { 475 Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " 476 + mNotificationProgressDrawable + ", mParts = " + mParts); 477 } 478 479 if (mNotificationProgressDrawable == null) return; 480 if (mParts == null) return; 481 482 final float width = mNotificationProgressDrawable.getBounds().width(); 483 if (width == 0) { 484 if (mProgressDrawableParts != null) { 485 if (DEBUG) { 486 Log.d(TAG, "Clearing mProgressDrawableParts"); 487 } 488 mProgressDrawableParts.clear(); 489 mNotificationProgressDrawable.setParts(mProgressDrawableParts); 490 } 491 return; 492 } 493 494 final float pointRadius = mNotificationProgressDrawable.getPointRadius(); 495 mProgressDrawableParts = processPartsAndConvertToDrawableParts( 496 mParts, 497 width, 498 mSegSegGap, 499 mSegPointGap, 500 pointRadius, 501 mHasTrackerIcon, 502 mTrackerDrawWidth 503 ); 504 505 final float progressFraction = getProgressFraction(); 506 final boolean isStyledByProgress = mProgressModel.isStyledByProgress(); 507 final float progressGap = mHasTrackerIcon ? 0F : mSegSegGap; 508 Pair<List<DrawablePart>, Float> p = null; 509 try { 510 p = maybeStretchAndRescaleSegments( 511 mParts, 512 mProgressDrawableParts, 513 mSegMinWidth, 514 pointRadius, 515 progressFraction, 516 isStyledByProgress, 517 progressGap 518 ); 519 } catch (NotEnoughWidthToFitAllPartsException ex) { 520 Log.w(TAG, "Failed to stretch and rescale segments", ex); 521 } 522 523 List<ProgressStyle.Segment> fallbackSegments = null; 524 if (p == null && mProgressModel.getSegments().size() > 1) { 525 Log.w(TAG, "Falling back to single segment"); 526 try { 527 fallbackSegments = List.of(new ProgressStyle.Segment(getMax()).setColor( 528 mProgressModel.getSegmentsFallbackColor() 529 == NotificationProgressModel.INVALID_COLOR 530 ? mProgressModel.getSegments().getFirst().getColor() 531 : mProgressModel.getSegmentsFallbackColor())); 532 p = processModelAndConvertToFinalDrawableParts( 533 fallbackSegments, 534 mProgressModel.getPoints(), 535 mProgressModel.getProgress(), 536 getMax(), 537 width, 538 mSegSegGap, 539 mSegPointGap, 540 pointRadius, 541 mHasTrackerIcon, 542 mSegMinWidth, 543 isStyledByProgress, 544 mTrackerDrawWidth); 545 } catch (NotEnoughWidthToFitAllPartsException ex) { 546 Log.w(TAG, "Failed to stretch and rescale segments with single segment fallback", 547 ex); 548 } 549 } 550 551 if (p == null && !mProgressModel.getPoints().isEmpty()) { 552 Log.w(TAG, "Falling back to single segment and no points"); 553 if (fallbackSegments == null) { 554 fallbackSegments = List.of(new ProgressStyle.Segment(getMax()).setColor( 555 mProgressModel.getSegmentsFallbackColor() 556 == NotificationProgressModel.INVALID_COLOR 557 ? mProgressModel.getSegments().getFirst().getColor() 558 : mProgressModel.getSegmentsFallbackColor())); 559 } 560 try { 561 p = processModelAndConvertToFinalDrawableParts( 562 fallbackSegments, 563 Collections.emptyList(), 564 mProgressModel.getProgress(), 565 getMax(), 566 width, 567 mSegSegGap, 568 mSegPointGap, 569 pointRadius, 570 mHasTrackerIcon, 571 mSegMinWidth, 572 isStyledByProgress, 573 mTrackerDrawWidth); 574 } catch (NotEnoughWidthToFitAllPartsException ex) { 575 Log.w(TAG, 576 "Failed to stretch and rescale segments with single segments and no points", 577 ex); 578 } 579 } 580 581 if (p == null) { 582 Log.w(TAG, "Falling back to no stretching and rescaling"); 583 p = maybeSplitDrawableSegmentsByProgress( 584 mParts, 585 mProgressDrawableParts, 586 progressFraction, 587 isStyledByProgress, 588 progressGap); 589 } 590 591 // Extend the first and last segments to fill the entire width. 592 p.first.getFirst().setStart(0); 593 p.first.getLast().setEnd(width); 594 595 if (DEBUG) { 596 Log.d(TAG, "Updating NotificationProgressDrawable parts"); 597 } 598 mNotificationProgressDrawable.setParts(p.first); 599 mAdjustedProgressFraction = 600 (p.second - mTrackerDrawWidth / 2F) / (width - mTrackerDrawWidth); 601 602 mNotificationProgressDrawable.updateEndDotColor(getEndDotColor(fallbackSegments)); 603 } 604 getEndDotColor(List<ProgressStyle.Segment> fallbackSegments)605 private int getEndDotColor(List<ProgressStyle.Segment> fallbackSegments) { 606 if (!mProgressModel.isStyledByProgress()) return Color.TRANSPARENT; 607 if (mProgressModel.getProgress() == mProgressModel.getProgressMax()) { 608 return Color.TRANSPARENT; 609 } 610 611 return fallbackSegments == null ? mProgressModel.getSegments().getLast().getColor() 612 : fallbackSegments.getLast().getColor(); 613 } 614 updateTrackerAndBarPos(int w, int h)615 private void updateTrackerAndBarPos(int w, int h) { 616 final int paddedHeight = h - mPaddingTop - mPaddingBottom; 617 final Drawable bar = getCurrentDrawable(); 618 final Drawable tracker = mTracker; 619 620 // The max height does not incorporate padding, whereas the height 621 // parameter does. 622 final int barHeight = Math.min(getMaxHeight(), paddedHeight); 623 final int trackerHeight = tracker == null ? 0 624 : ((mTrackerHeight <= 0) ? tracker.getIntrinsicHeight() : mTrackerHeight); 625 626 // Apply offset to whichever item is taller. 627 final int barOffsetY; 628 final int trackerOffsetY; 629 if (trackerHeight > barHeight) { 630 final int offsetHeight = (paddedHeight - trackerHeight) / 2; 631 barOffsetY = offsetHeight + (trackerHeight - barHeight) / 2; 632 trackerOffsetY = offsetHeight; 633 } else { 634 final int offsetHeight = (paddedHeight - barHeight) / 2; 635 barOffsetY = offsetHeight; 636 trackerOffsetY = offsetHeight + (barHeight - trackerHeight) / 2; 637 } 638 639 if (bar != null) { 640 final int barWidth = w - mPaddingRight - mPaddingLeft; 641 bar.setBounds(0, barOffsetY, barWidth, barOffsetY + barHeight); 642 } 643 644 if (tracker != null) { 645 setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY); 646 } 647 } 648 getProgressFraction()649 private float getProgressFraction() { 650 int min = getMin(); 651 int max = getMax(); 652 int range = max - min; 653 return getProgressFraction(range, (getProgress() - min)); 654 } 655 getProgressFraction(int progressMax, int progress)656 private static float getProgressFraction(int progressMax, int progress) { 657 return progressMax > 0 ? progress / (float) progressMax : 0; 658 } 659 660 /** 661 * Updates the tracker drawable bounds. 662 * 663 * @param w Width of the view, including padding 664 * @param tracker Drawable used for the tracker 665 * @param progressFraction Current progress between 0 and 1 666 * @param offsetY Vertical offset for centering. If set to 667 * {@link Integer#MIN_VALUE}, the current offset will be used. 668 */ setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY)669 private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) { 670 int available = w - mPaddingLeft - mPaddingRight; 671 final int trackerWidth = tracker.getIntrinsicWidth(); 672 final int trackerHeight = tracker.getIntrinsicHeight(); 673 available -= mTrackerDrawWidth; 674 675 final int trackerPos = (int) (progressFraction * available + 0.5f); 676 677 final int top, bottom; 678 if (offsetY == Integer.MIN_VALUE) { 679 final Rect oldBounds = tracker.getBounds(); 680 top = oldBounds.top; 681 bottom = oldBounds.bottom; 682 } else { 683 top = offsetY; 684 bottom = offsetY + trackerHeight; 685 } 686 687 mTrackerPos = (isLayoutRtl() && getMirrorForRtl()) ? available - trackerPos : trackerPos; 688 final int left = 0; 689 final int right = left + trackerWidth; 690 691 final Drawable background = getBackground(); 692 if (background != null) { 693 final int bkgOffsetX = mPaddingLeft; 694 final int bkgOffsetY = mPaddingTop; 695 background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX, 696 bottom + bkgOffsetY); 697 } 698 699 // Canvas will be translated, so 0,0 is where we start drawing 700 tracker.setBounds(left, top, right, bottom); 701 702 mTrackerPosIsDirty = false; 703 } 704 705 @Override onResolveDrawables(int layoutDirection)706 public void onResolveDrawables(int layoutDirection) { 707 super.onResolveDrawables(layoutDirection); 708 709 if (mTracker != null) { 710 mTracker.setLayoutDirection(layoutDirection); 711 } 712 } 713 714 @Override onDraw(Canvas canvas)715 protected synchronized void onDraw(Canvas canvas) { 716 super.onDraw(canvas); 717 718 if (isIndeterminate()) return; 719 drawTracker(canvas); 720 } 721 722 /** 723 * Draw the tracker. 724 */ drawTracker(Canvas canvas)725 private void drawTracker(Canvas canvas) { 726 if (mTracker == null) return; 727 728 if (mTrackerPosIsDirty) { 729 setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE); 730 } 731 732 final int saveCount = canvas.save(); 733 // Translate the canvas origin to tracker position to make the draw matrix and the RtL 734 // transformations work. 735 canvas.translate(mPaddingLeft + mTrackerPos, mPaddingTop); 736 737 if (mTrackerHeight > 0) { 738 canvas.clipRect(0, 0, mTrackerDrawWidth, mTrackerHeight); 739 } 740 741 if (mTrackerDrawMatrix != null) { 742 canvas.concat(mTrackerDrawMatrix); 743 } 744 mTracker.draw(canvas); 745 canvas.restoreToCount(saveCount); 746 } 747 748 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)749 protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 750 Drawable d = getCurrentDrawable(); 751 752 int trackerHeight = mTracker == null ? 0 : mTracker.getIntrinsicHeight(); 753 int dw = 0; 754 int dh = 0; 755 if (d != null) { 756 dw = Math.max(getMinWidth(), Math.min(getMaxWidth(), d.getIntrinsicWidth())); 757 dh = Math.max(getMinHeight(), Math.min(getMaxHeight(), d.getIntrinsicHeight())); 758 dh = Math.max(trackerHeight, dh); 759 } 760 dw += mPaddingLeft + mPaddingRight; 761 dh += mPaddingTop + mPaddingBottom; 762 763 setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), 764 resolveSizeAndState(dh, heightMeasureSpec, 0)); 765 } 766 767 @Override getAccessibilityClassName()768 public CharSequence getAccessibilityClassName() { 769 return NotificationProgressBar.class.getName(); 770 } 771 772 @Override onRtlPropertiesChanged(int layoutDirection)773 public void onRtlPropertiesChanged(int layoutDirection) { 774 super.onRtlPropertiesChanged(layoutDirection); 775 776 final Drawable tracker = mTracker; 777 if (tracker != null) { 778 setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE); 779 780 // Since we draw translated, the drawable's bounds that it signals 781 // for invalidation won't be the actual bounds we want invalidated, 782 // so just invalidate this whole view. 783 invalidate(); 784 } 785 } 786 787 /** 788 * Processes the ProgressStyle data and convert to a list of {@code Part}. 789 */ 790 @VisibleForTesting processModelAndConvertToViewParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, int progressMax )791 public static List<Part> processModelAndConvertToViewParts( 792 List<ProgressStyle.Segment> segments, 793 List<ProgressStyle.Point> points, 794 int progress, 795 int progressMax 796 ) { 797 if (segments.isEmpty()) { 798 throw new IllegalArgumentException("List of segments shouldn't be empty"); 799 } 800 801 final int totalLength = segments.stream().mapToInt(ProgressStyle.Segment::getLength).sum(); 802 if (progressMax != totalLength) { 803 throw new IllegalArgumentException("Invalid progressMax : " + progressMax); 804 } 805 806 for (ProgressStyle.Segment segment : segments) { 807 final int length = segment.getLength(); 808 if (length <= 0) { 809 throw new IllegalArgumentException("Invalid segment length : " + length); 810 } 811 } 812 813 if (progress < 0 || progress > progressMax) { 814 throw new IllegalArgumentException("Invalid progress : " + progress); 815 } 816 817 818 for (ProgressStyle.Point point : points) { 819 final int pos = point.getPosition(); 820 if (pos < 0 || pos > progressMax) { 821 throw new IllegalArgumentException("Invalid Point position : " + pos); 822 } 823 } 824 825 // There should be no points at start or end. If there are, drop them with a warning. 826 points.removeIf(point -> { 827 final int pos = point.getPosition(); 828 if (pos == 0) { 829 Log.w(TAG, "Dropping point at start"); 830 return true; 831 } else if (pos == progressMax) { 832 Log.w(TAG, "Dropping point at end"); 833 return true; 834 } 835 return false; 836 }); 837 838 final Map<Integer, ProgressStyle.Segment> startToSegmentMap = generateStartToSegmentMap( 839 segments); 840 final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( 841 points); 842 final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap, 843 positionToPointMap); 844 845 final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints( 846 startToSegmentMap, sortedPos, progressMax); 847 848 return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos, 849 progressMax); 850 } 851 852 // Any segment with a point on it gets split by the point. splitSegmentsByPoints( Map<Integer, ProgressStyle.Segment> startToSegmentMap, SortedSet<Integer> sortedPos, int progressMax )853 private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints( 854 Map<Integer, ProgressStyle.Segment> startToSegmentMap, 855 SortedSet<Integer> sortedPos, 856 int progressMax 857 ) { 858 int prevSegStart = 0; 859 for (Integer pos : sortedPos) { 860 if (pos == 0 || pos == progressMax) continue; 861 if (startToSegmentMap.containsKey(pos)) { 862 prevSegStart = pos; 863 continue; 864 } 865 866 final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart); 867 final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment( 868 pos - prevSegStart).setColor(prevSeg.getColor()); 869 final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment( 870 prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor()); 871 872 startToSegmentMap.put(prevSegStart, leftSeg); 873 startToSegmentMap.put(pos, rightSeg); 874 875 prevSegStart = pos; 876 } 877 878 return startToSegmentMap; 879 } 880 convertToViewParts( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap, SortedSet<Integer> sortedPos, int progressMax )881 private static List<Part> convertToViewParts( 882 Map<Integer, ProgressStyle.Segment> startToSegmentMap, 883 Map<Integer, ProgressStyle.Point> positionToPointMap, 884 SortedSet<Integer> sortedPos, 885 int progressMax 886 ) { 887 List<Part> parts = new ArrayList<>(); 888 for (Integer pos : sortedPos) { 889 if (positionToPointMap.containsKey(pos)) { 890 final ProgressStyle.Point point = positionToPointMap.get(pos); 891 parts.add(new Point(point.getColor())); 892 } 893 if (startToSegmentMap.containsKey(pos)) { 894 final ProgressStyle.Segment seg = startToSegmentMap.get(pos); 895 parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor())); 896 } 897 } 898 899 return parts; 900 } 901 902 @ColorInt maybeGetFadedColor(@olorInt int color, boolean fade)903 private static int maybeGetFadedColor(@ColorInt int color, boolean fade) { 904 if (!fade) return color; 905 906 return getFadedColor(color); 907 } 908 909 /** 910 * Get a color that's the input color with opacity updated to FADED_OPACITY. 911 */ 912 @ColorInt getFadedColor(@olorInt int color)913 static int getFadedColor(@ColorInt int color) { 914 return Color.argb( 915 (int) (Color.alpha(color) * FADED_OPACITY + 0.5f), 916 Color.red(color), 917 Color.green(color), 918 Color.blue(color)); 919 } 920 generateStartToSegmentMap( List<ProgressStyle.Segment> segments )921 private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap( 922 List<ProgressStyle.Segment> segments 923 ) { 924 final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>(); 925 926 int currentStart = 0; // Initial start position is 0 927 928 for (ProgressStyle.Segment segment : segments) { 929 // Use the current start position as the key, and the segment as the value 930 startToSegmentMap.put(currentStart, segment); 931 932 // Update the start position for the next segment 933 currentStart += segment.getLength(); 934 } 935 936 return startToSegmentMap; 937 } 938 generatePositionToPointMap( List<ProgressStyle.Point> points )939 private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap( 940 List<ProgressStyle.Point> points 941 ) { 942 final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>(); 943 944 for (ProgressStyle.Point point : points) { 945 positionToPointMap.put(point.getPosition(), point); 946 } 947 948 return positionToPointMap; 949 } 950 generateSortedPositionSet( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap )951 private static SortedSet<Integer> generateSortedPositionSet( 952 Map<Integer, ProgressStyle.Segment> startToSegmentMap, 953 Map<Integer, ProgressStyle.Point> positionToPointMap 954 ) { 955 final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet()); 956 sortedPos.addAll(positionToPointMap.keySet()); 957 958 return sortedPos; 959 } 960 961 /** 962 * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}. 963 */ 964 @VisibleForTesting processPartsAndConvertToDrawableParts( List<Part> parts, float totalWidth, float segSegGap, float segPointGap, float pointRadius, boolean hasTrackerIcon, int trackerDrawWidth)965 public static List<DrawablePart> processPartsAndConvertToDrawableParts( 966 List<Part> parts, 967 float totalWidth, 968 float segSegGap, 969 float segPointGap, 970 float pointRadius, 971 boolean hasTrackerIcon, 972 int trackerDrawWidth) { 973 List<DrawablePart> drawableParts = new ArrayList<>(); 974 975 float available = totalWidth - trackerDrawWidth; 976 // Generally, we will start the first segment at (x+trackerDrawWidth/2, y) and end the last 977 // segment at (x+w-trackerDrawWidth/2, y) 978 float x = trackerDrawWidth / 2F; 979 980 final int nParts = parts.size(); 981 for (int iPart = 0; iPart < nParts; iPart++) { 982 final Part part = parts.get(iPart); 983 final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); 984 final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); 985 if (part instanceof Segment segment) { 986 final float segWidth = segment.mFraction * available; 987 // Advance the start position to account for a point immediately prior. 988 final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap); 989 final float start = x + startOffset; 990 // Retract the end position to account for the padding and a point immediately 991 // after. 992 final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, 993 segSegGap, hasTrackerIcon); 994 final float end = x + segWidth - endOffset; 995 996 drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded)); 997 998 segment.mStart = x; 999 segment.mEnd = x + segWidth; 1000 1001 // Advance the current position to account for the segment's fraction of the total 1002 // width (ignoring offset and padding) 1003 x += segWidth; 1004 } else if (part instanceof Point point) { 1005 final float pointWidth = 2 * pointRadius; 1006 float start = x - pointRadius; 1007 float end = x + pointRadius; 1008 1009 drawableParts.add(new DrawablePoint(start, end, point.mColor)); 1010 } 1011 } 1012 1013 return drawableParts; 1014 } 1015 getSegStartOffset(Part prevPart, float pointRadius, float segPointGap)1016 private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap) { 1017 if (!(prevPart instanceof Point)) return 0F; 1018 return pointRadius + segPointGap; 1019 } 1020 getSegEndOffset(Segment seg, Part nextPart, float pointRadius, float segPointGap, float segSegGap, boolean hasTrackerIcon)1021 private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, 1022 float segPointGap, float segSegGap, boolean hasTrackerIcon) { 1023 if (nextPart == null) return 0F; 1024 if (nextPart instanceof Segment nextSeg) { 1025 if (!seg.mFaded && nextSeg.mFaded) { 1026 // @see Segment#mFaded 1027 return hasTrackerIcon ? 0F : segSegGap; 1028 } 1029 return segSegGap; 1030 } 1031 1032 return segPointGap + pointRadius; 1033 } 1034 1035 /** 1036 * Processes the list of {@code DrawablePart} data and convert to a pair of: 1037 * - list of processed {@code DrawablePart}. 1038 * - location of progress on the stretched and rescaled progress bar. 1039 */ 1040 @VisibleForTesting maybeStretchAndRescaleSegments( List<Part> parts, List<DrawablePart> drawableParts, float segmentMinWidth, float pointRadius, float progressFraction, boolean isStyledByProgress, float progressGap )1041 public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments( 1042 List<Part> parts, 1043 List<DrawablePart> drawableParts, 1044 float segmentMinWidth, 1045 float pointRadius, 1046 float progressFraction, 1047 boolean isStyledByProgress, 1048 float progressGap 1049 ) throws NotEnoughWidthToFitAllPartsException { 1050 final List<DrawableSegment> drawableSegments = drawableParts 1051 .stream() 1052 .filter(DrawableSegment.class::isInstance) 1053 .map(DrawableSegment.class::cast) 1054 .toList(); 1055 float totalExcessWidth = 0; 1056 float totalPositiveExcessWidth = 0; 1057 for (DrawableSegment drawableSegment : drawableSegments) { 1058 final float excessWidth = drawableSegment.getWidth() - segmentMinWidth; 1059 totalExcessWidth += excessWidth; 1060 if (excessWidth > 0) totalPositiveExcessWidth += excessWidth; 1061 } 1062 1063 // All drawable segments are above minimum width. No need to stretch and rescale. 1064 if (totalExcessWidth == totalPositiveExcessWidth) { 1065 return maybeSplitDrawableSegmentsByProgress( 1066 parts, 1067 drawableParts, 1068 progressFraction, 1069 isStyledByProgress, 1070 progressGap); 1071 } 1072 1073 if (totalExcessWidth < 0) { 1074 throw new NotEnoughWidthToFitAllPartsException( 1075 "Not enough width to satisfy the minimum width for segments."); 1076 } 1077 1078 final int nParts = drawableParts.size(); 1079 float startOffset = 0; 1080 for (int iPart = 0; iPart < nParts; iPart++) { 1081 final DrawablePart drawablePart = drawableParts.get(iPart); 1082 if (drawablePart instanceof DrawableSegment drawableSegment) { 1083 final float origDrawableSegmentWidth = drawableSegment.getWidth(); 1084 1085 float drawableSegmentWidth = segmentMinWidth; 1086 // Allocate the totalExcessWidth to the segments above minimum, proportionally to 1087 // their initial excessWidth. 1088 if (origDrawableSegmentWidth > segmentMinWidth) { 1089 drawableSegmentWidth += 1090 totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth) 1091 / totalPositiveExcessWidth; 1092 } 1093 1094 final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth(); 1095 1096 // Adjust drawable segments to new widths 1097 drawableSegment.setStart(drawableSegment.getStart() + startOffset); 1098 drawableSegment.setEnd( 1099 drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff); 1100 1101 // Also adjust view segments to new width. (For view segments, only start is 1102 // needed?) 1103 // Check that segments and drawableSegments are of the same size? 1104 final Segment segment = (Segment) parts.get(iPart); 1105 final float origSegmentWidth = segment.getWidth(); 1106 segment.mStart = segment.mStart + startOffset; 1107 segment.mEnd = segment.mStart + origSegmentWidth + widthDiff; 1108 1109 // Increase startOffset for the subsequent segments. 1110 startOffset += widthDiff; 1111 } else if (drawablePart instanceof DrawablePoint drawablePoint) { 1112 drawablePoint.setStart(drawablePoint.getStart() + startOffset); 1113 drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius); 1114 } 1115 } 1116 1117 return maybeSplitDrawableSegmentsByProgress( 1118 parts, 1119 drawableParts, 1120 progressFraction, 1121 isStyledByProgress, 1122 progressGap); 1123 } 1124 1125 /** 1126 * Find the location of progress on the stretched and rescaled progress bar. 1127 * If isStyledByProgress is true, also split the drawable segment with the progress value in its 1128 * range. Style the drawable parts after process with reduced opacity and segment height. 1129 */ maybeSplitDrawableSegmentsByProgress( List<Part> parts, List<DrawablePart> drawableParts, float progressFraction, boolean isStyledByProgress, float progressGap )1130 private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress( 1131 // Needed to get the original segment start and end positions in pixels. 1132 List<Part> parts, 1133 List<DrawablePart> drawableParts, 1134 float progressFraction, 1135 boolean isStyledByProgress, 1136 float progressGap 1137 ) { 1138 if (progressFraction == 1) { 1139 return new Pair<>(drawableParts, drawableParts.getLast().getEnd()); 1140 } 1141 1142 int iPartFirstSegmentToStyle = -1; 1143 int iPartSegmentToSplit = -1; 1144 float rescaledProgressX = 0; 1145 float startFraction = 0; 1146 final int nParts = parts.size(); 1147 for (int iPart = 0; iPart < nParts; iPart++) { 1148 final Part part = parts.get(iPart); 1149 if (!(part instanceof Segment segment)) continue; 1150 if (startFraction == progressFraction) { 1151 iPartFirstSegmentToStyle = iPart; 1152 rescaledProgressX = segment.mStart; 1153 break; 1154 } else if (startFraction < progressFraction 1155 && progressFraction < startFraction + segment.mFraction) { 1156 iPartSegmentToSplit = iPart; 1157 rescaledProgressX = segment.mStart 1158 + (progressFraction - startFraction) / segment.mFraction 1159 * segment.getWidth(); 1160 break; 1161 } 1162 startFraction += segment.mFraction; 1163 } 1164 1165 if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX); 1166 1167 List<DrawablePart> splitDrawableParts = new ArrayList<>(); 1168 boolean styleRemainingParts = false; 1169 for (int iPart = 0; iPart < nParts; iPart++) { 1170 final DrawablePart drawablePart = drawableParts.get(iPart); 1171 if (drawablePart instanceof DrawablePoint drawablePoint) { 1172 final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts); 1173 splitDrawableParts.add( 1174 new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color)); 1175 } 1176 if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true; 1177 if (drawablePart instanceof DrawableSegment drawableSegment) { 1178 if (iPart == iPartSegmentToSplit) { 1179 if (rescaledProgressX <= drawableSegment.getStart()) { 1180 styleRemainingParts = true; 1181 final int color = maybeGetFadedColor(drawableSegment.getColor(), true); 1182 splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), 1183 drawableSegment.getEnd(), color, true)); 1184 } else if (drawableSegment.getStart() < rescaledProgressX 1185 && rescaledProgressX < drawableSegment.getEnd()) { 1186 splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), 1187 rescaledProgressX - progressGap, drawableSegment.getColor())); 1188 final int color = maybeGetFadedColor(drawableSegment.getColor(), true); 1189 splitDrawableParts.add( 1190 new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(), 1191 color, true)); 1192 styleRemainingParts = true; 1193 } else { 1194 splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), 1195 drawableSegment.getEnd(), drawableSegment.getColor())); 1196 styleRemainingParts = true; 1197 } 1198 } else { 1199 final int color = maybeGetFadedColor(drawableSegment.getColor(), 1200 styleRemainingParts); 1201 splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), 1202 drawableSegment.getEnd(), color, styleRemainingParts)); 1203 } 1204 } 1205 } 1206 1207 return new Pair<>(splitDrawableParts, rescaledProgressX); 1208 } 1209 1210 /** 1211 * Processes the ProgressStyle data and convert to a pair of: 1212 * - list of processed {@code DrawablePart}. 1213 * - location of progress on the stretched and rescaled progress bar. 1214 */ 1215 @VisibleForTesting processModelAndConvertToFinalDrawableParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, int progressMax, float totalWidth, float segSegGap, float segPointGap, float pointRadius, boolean hasTrackerIcon, float segmentMinWidth, boolean isStyledByProgress, int trackerDrawWidth )1216 public static Pair<List<DrawablePart>, Float> processModelAndConvertToFinalDrawableParts( 1217 List<ProgressStyle.Segment> segments, 1218 List<ProgressStyle.Point> points, 1219 int progress, 1220 int progressMax, 1221 float totalWidth, 1222 float segSegGap, 1223 float segPointGap, 1224 float pointRadius, 1225 boolean hasTrackerIcon, 1226 float segmentMinWidth, 1227 boolean isStyledByProgress, 1228 int trackerDrawWidth 1229 ) throws NotEnoughWidthToFitAllPartsException { 1230 List<Part> parts = processModelAndConvertToViewParts(segments, points, progress, 1231 progressMax); 1232 List<DrawablePart> drawableParts = processPartsAndConvertToDrawableParts(parts, totalWidth, 1233 segSegGap, segPointGap, pointRadius, hasTrackerIcon, trackerDrawWidth); 1234 return maybeStretchAndRescaleSegments(parts, drawableParts, segmentMinWidth, pointRadius, 1235 getProgressFraction(progressMax, progress), isStyledByProgress, 1236 hasTrackerIcon ? 0F : segSegGap); 1237 } 1238 1239 /** 1240 * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a 1241 * {@link Point} with zero length. 1242 */ 1243 public interface Part { 1244 } 1245 1246 /** 1247 * A segment is a part of the progress bar with non-zero length. For example, it can 1248 * represent a portion in a navigation journey with certain traffic condition. 1249 */ 1250 public static final class Segment implements Part { 1251 private final float mFraction; 1252 @ColorInt 1253 private final int mColor; 1254 /** 1255 * Whether the segment is faded or not. 1256 * <p> 1257 * <pre> 1258 * When mFaded is set to true, a combination of the following is done to the segment: 1259 * 1. The drawing color is mColor with opacity updated to FADED_OPACITY. 1260 * 2. The gap between faded and non-faded segments is: 1261 * - the segment-segment gap, when there is no tracker icon 1262 * - 0, when there is tracker icon 1263 * </pre> 1264 * </p> 1265 */ 1266 private final boolean mFaded; 1267 1268 /** Start position (in pixels) */ 1269 private float mStart; 1270 /** End position (in pixels */ 1271 private float mEnd; 1272 Segment(float fraction, @ColorInt int color)1273 public Segment(float fraction, @ColorInt int color) { 1274 this(fraction, color, false); 1275 } 1276 Segment(float fraction, @ColorInt int color, boolean faded)1277 public Segment(float fraction, @ColorInt int color, boolean faded) { 1278 mFraction = fraction; 1279 mColor = color; 1280 mFaded = faded; 1281 } 1282 1283 /** Returns the calculated drawing width of the part */ getWidth()1284 public float getWidth() { 1285 return mEnd - mStart; 1286 } 1287 1288 @Override toString()1289 public String toString() { 1290 return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" 1291 + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd; 1292 } 1293 1294 // Needed for unit tests 1295 @Override equals(@ndroidx.annotation.Nullable Object other)1296 public boolean equals(@androidx.annotation.Nullable Object other) { 1297 if (this == other) return true; 1298 1299 if (other == null || getClass() != other.getClass()) return false; 1300 1301 Segment that = (Segment) other; 1302 if (Float.compare(this.mFraction, that.mFraction) != 0) return false; 1303 if (this.mColor != that.mColor) return false; 1304 return this.mFaded == that.mFaded; 1305 } 1306 1307 @Override hashCode()1308 public int hashCode() { 1309 return Objects.hash(mFraction, mColor, mFaded); 1310 } 1311 } 1312 1313 /** 1314 * A point is a part of the progress bar with zero length. Points are designated points within a 1315 * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop 1316 * ride-share journey. 1317 */ 1318 public static final class Point implements Part { 1319 @ColorInt 1320 private final int mColor; 1321 Point(@olorInt int color)1322 public Point(@ColorInt int color) { 1323 mColor = color; 1324 } 1325 1326 @Override toString()1327 public String toString() { 1328 return "Point(color=" + this.mColor + ")"; 1329 } 1330 1331 // Needed for unit tests. 1332 @Override equals(@ndroidx.annotation.Nullable Object other)1333 public boolean equals(@androidx.annotation.Nullable Object other) { 1334 if (this == other) return true; 1335 1336 if (other == null || getClass() != other.getClass()) return false; 1337 1338 Point that = (Point) other; 1339 1340 return this.mColor == that.mColor; 1341 } 1342 1343 @Override hashCode()1344 public int hashCode() { 1345 return Objects.hash(mColor); 1346 } 1347 } 1348 1349 public static class NotEnoughWidthToFitAllPartsException extends Exception { NotEnoughWidthToFitAllPartsException(String message)1350 public NotEnoughWidthToFitAllPartsException(String message) { 1351 super(message); 1352 } 1353 } 1354 } 1355