@@ -343,6 +343,7 @@ abstract class BaseSlider<
343343 private int trackStopIndicatorSize ;
344344 private int trackCornerSize ;
345345 private int trackInsideCornerSize ;
346+ private boolean centered = false ;
346347 @ Nullable private Drawable trackIconActiveStart ;
347348 private boolean trackIconActiveStartMutated = false ;
348349 @ Nullable private Drawable trackIconActiveEnd ;
@@ -542,6 +543,7 @@ private void processAttributes(Context context, AttributeSet attrs, int defStyle
542543 valueFrom = a .getFloat (R .styleable .Slider_android_valueFrom , 0.0f );
543544 valueTo = a .getFloat (R .styleable .Slider_android_valueTo , 1.0f );
544545 setValues (valueFrom );
546+ setCentered (a .getBoolean (R .styleable .Slider_centered , false ));
545547 stepSize = a .getFloat (R .styleable .Slider_android_stepSize , 0.0f );
546548
547549 float defaultMinTouchTargetSize =
@@ -2396,6 +2398,30 @@ public void setOrientation(@Orientation int orientation) {
23962398 updateWidgetLayout (true );
23972399 }
23982400
2401+ /**
2402+ * Sets the slider to be in centered configuration, meaning the starting value is positioned in
2403+ * the middle of the slider.
2404+ *
2405+ * @param isCentered boolean to use for the slider's centered configuration.
2406+ * @attr ref com.google.android.material.R.styleable#Slider_centered
2407+ * @see #isCentered()
2408+ */
2409+ public void setCentered (boolean isCentered ) {
2410+ if (this .centered == isCentered ) {
2411+ return ;
2412+ }
2413+ this .centered = isCentered ;
2414+
2415+ // if centered, the default value is at the center
2416+ if (isCentered ) {
2417+ setValues ((valueFrom + valueTo ) / 2f );
2418+ } else {
2419+ setValues (valueFrom );
2420+ }
2421+
2422+ updateWidgetLayout (true );
2423+ }
2424+
23992425 @ Override
24002426 protected void onAttachedToWindow () {
24012427 super .onAttachedToWindow ();
@@ -2562,7 +2588,9 @@ protected void onDraw(@NonNull Canvas canvas) {
25622588 int yCenter = calculateTrackCenter ();
25632589
25642590 drawInactiveTracks (canvas , trackWidth , yCenter );
2565- drawActiveTracks (canvas , trackWidth , yCenter );
2591+ if (!isCentered ()) {
2592+ drawActiveTracks (canvas , trackWidth , yCenter );
2593+ }
25662594
25672595 if (isRtl () || isVertical ()) {
25682596 drawTrackIcons (canvas , activeTrackRect , inactiveTrackLeftRect );
@@ -2592,55 +2620,53 @@ private float[] getActiveRange() {
25922620 float left = normalizeValue (values .size () == 1 ? valueFrom : min );
25932621 float right = normalizeValue (max );
25942622
2623+ // When centered, there is no active range, left == right in order to draw the inactive track on
2624+ // both sides of the thumb leaving space for it, covering the entirety of the track.
2625+ if (isCentered ()) {
2626+ left = right ;
2627+ }
2628+
25952629 // In RTL we draw things in reverse, so swap the left and right range values
25962630 return isRtl () || isVertical () ? new float [] {right , left } : new float [] {left , right };
25972631 }
25982632
25992633 private void drawInactiveTracks (@ NonNull Canvas canvas , int width , int yCenter ) {
2600- populateInactiveTrackRightRect (width , yCenter );
2601- updateTrack (
2602- canvas ,
2603- inactiveTrackPaint ,
2604- inactiveTrackRightRect ,
2605- getTrackCornerSize (),
2606- FullCornerDirection .RIGHT );
2607-
2608- // Also draw inactive track to the left if there is any
2609- populateInactiveTrackLeftRect (width , yCenter );
2610- updateTrack (
2634+ float [] activeRange = getActiveRange ();
2635+ float top = yCenter - trackThickness / 2f ;
2636+ float bottom = yCenter + trackThickness / 2f ;
2637+
2638+ drawInactiveTrackSection (
2639+ trackSidePadding - getTrackCornerSize (),
2640+ trackSidePadding + activeRange [0 ] * width - thumbTrackGapSize ,
2641+ top ,
2642+ bottom ,
26112643 canvas ,
2612- inactiveTrackPaint ,
26132644 inactiveTrackLeftRect ,
2614- getTrackCornerSize (),
26152645 FullCornerDirection .LEFT );
2646+ drawInactiveTrackSection (
2647+ trackSidePadding + activeRange [1 ] * width + thumbTrackGapSize ,
2648+ trackSidePadding + width + getTrackCornerSize (),
2649+ top ,
2650+ bottom ,
2651+ canvas ,
2652+ inactiveTrackRightRect ,
2653+ FullCornerDirection .RIGHT );
26162654 }
26172655
2618- private void populateInactiveTrackRightRect (int width , int yCenter ) {
2619- float [] activeRange = getActiveRange ();
2620- float right = trackSidePadding + activeRange [1 ] * width ;
2621- if (right < trackSidePadding + width ) {
2622- inactiveTrackRightRect .set (
2623- right + thumbTrackGapSize ,
2624- yCenter - trackThickness / 2f ,
2625- trackSidePadding + width + getTrackCornerSize (),
2626- yCenter + trackThickness / 2f );
2627- } else {
2628- inactiveTrackRightRect .setEmpty ();
2629- }
2630- }
2631-
2632- private void populateInactiveTrackLeftRect (int width , int yCenter ) {
2633- float [] activeRange = getActiveRange ();
2634- float left = trackSidePadding + activeRange [0 ] * width ;
2635- if (left > trackSidePadding ) {
2636- inactiveTrackLeftRect .set (
2637- trackSidePadding - getTrackCornerSize (),
2638- yCenter - trackThickness / 2f ,
2639- left - thumbTrackGapSize ,
2640- yCenter + trackThickness / 2f );
2656+ private void drawInactiveTrackSection (
2657+ float from ,
2658+ float to ,
2659+ float top ,
2660+ float bottom ,
2661+ @ NonNull Canvas canvas ,
2662+ RectF rect ,
2663+ FullCornerDirection direction ) {
2664+ if (to - from > getTrackCornerSize () - thumbTrackGapSize ) {
2665+ rect .set (from , top , to , bottom );
26412666 } else {
2642- inactiveTrackLeftRect .setEmpty ();
2667+ rect .setEmpty ();
26432668 }
2669+ updateTrack (canvas , inactiveTrackPaint , rect , getTrackCornerSize (), direction );
26442670 }
26452671
26462672 /**
@@ -2923,26 +2949,41 @@ private void maybeDrawTicks(@NonNull Canvas canvas) {
29232949
29242950 // Draw ticks on the left inactive track (if any).
29252951 if (leftActiveTickIndex > 0 ) {
2926- canvas . drawPoints ( ticksCoordinates , 0 , leftActiveTickIndex * 2 , inactiveTicksPaint );
2952+ drawTicks ( 0 , leftActiveTickIndex * 2 , canvas , inactiveTicksPaint );
29272953 }
29282954
29292955 // Draw ticks on the active track (if any).
29302956 if (leftActiveTickIndex <= rightActiveTickIndex ) {
2931- canvas .drawPoints (
2932- ticksCoordinates ,
2957+ drawTicks (
29332958 leftActiveTickIndex * 2 ,
2934- (rightActiveTickIndex - leftActiveTickIndex + 1 ) * 2 ,
2935- activeTicksPaint );
2959+ (rightActiveTickIndex + 1 ) * 2 ,
2960+ canvas ,
2961+ isCentered () ? inactiveTicksPaint : activeTicksPaint ); // centered uses inactive color.
29362962 }
29372963
29382964 // Draw ticks on the right inactive track (if any).
29392965 if ((rightActiveTickIndex + 1 ) * 2 < ticksCoordinates .length ) {
2940- canvas .drawPoints (
2941- ticksCoordinates ,
2942- (rightActiveTickIndex + 1 ) * 2 ,
2943- ticksCoordinates .length - (rightActiveTickIndex + 1 ) * 2 ,
2944- inactiveTicksPaint );
2966+ drawTicks (
2967+ (rightActiveTickIndex + 1 ) * 2 , ticksCoordinates .length , canvas , inactiveTicksPaint );
2968+ }
2969+ }
2970+
2971+ private void drawTicks (int from , int to , Canvas canvas , Paint paint ) {
2972+ for (int i = from ; i < to ; i += 2 ) {
2973+ if (isOverlappingThumb (ticksCoordinates [i ])) {
2974+ continue ;
2975+ }
2976+ canvas .drawPoint (ticksCoordinates [i ], ticksCoordinates [i + 1 ], paint );
2977+ }
2978+ }
2979+
2980+ private boolean isOverlappingThumb (float tickCoordinate ) {
2981+ float threshold = thumbTrackGapSize + thumbWidth / 2f ;
2982+ for (float value : values ) {
2983+ float valueToX = valueToX (value );
2984+ return tickCoordinate >= valueToX - threshold && tickCoordinate <= valueToX + threshold ;
29452985 }
2986+ return false ;
29462987 }
29472988
29482989 private void maybeDrawStopIndicator (@ NonNull Canvas canvas , int yCenter ) {
@@ -2954,13 +2995,25 @@ private void maybeDrawStopIndicator(@NonNull Canvas canvas, int yCenter) {
29542995 if (values .get (values .size () - 1 ) < valueTo ) {
29552996 drawStopIndicator (canvas , valueToX (valueTo ), yCenter );
29562997 }
2957- // Multiple thumbs, inactive track may be visible at the start.
2958- if (values .size () > 1 && values .get (0 ) > valueFrom ) {
2998+ // Centered, multiple thumbs, inactive track may be visible at the start.
2999+ if (isCentered () || ( values .size () > 1 && values .get (0 ) > valueFrom ) ) {
29593000 drawStopIndicator (canvas , valueToX (valueFrom ), yCenter );
29603001 }
3002+ // Centered, draw indicator in the middle of the track.
3003+ if (isCentered ()) {
3004+ drawStopIndicator (canvas , (valueToX (valueTo ) + valueToX (valueFrom )) / 2f , yCenter );
3005+ }
29613006 }
29623007
29633008 private void drawStopIndicator (@ NonNull Canvas canvas , float x , float y ) {
3009+ // Prevent drawing indicator on the thumbs.
3010+ for (float value : values ) {
3011+ float valueToX = valueToX (value );
3012+ float threshold = thumbTrackGapSize + thumbWidth / 2f ;
3013+ if (x >= valueToX - threshold && x <= valueToX + threshold ) {
3014+ return ;
3015+ }
3016+ }
29643017 if (isVertical ()) {
29653018 canvas .drawPoint (y , x , stopIndicatorPaint );
29663019 } else {
@@ -3751,10 +3804,14 @@ final boolean isRtl() {
37513804 return getLayoutDirection () == View .LAYOUT_DIRECTION_RTL ;
37523805 }
37533806
3754- final boolean isVertical () {
3807+ public boolean isVertical () {
37553808 return widgetOrientation == VERTICAL ;
37563809 }
37573810
3811+ public boolean isCentered () {
3812+ return centered ;
3813+ }
3814+
37583815 /**
37593816 * Attempts to move focus to next or previous thumb <i>independent of layout direction</i> and
37603817 * returns whether the focused thumb changed. If focused thumb didn't change, we're at the view
0 commit comments