2929import android .graphics .Paint .Style ;
3030import android .graphics .PorterDuff .Mode ;
3131import android .graphics .PorterDuffXfermode ;
32+ import android .graphics .Rect ;
33+ import android .graphics .Region .Op ;
3234import android .graphics .drawable .Drawable ;
3335import android .graphics .drawable .RippleDrawable ;
3436import android .os .Build .VERSION ;
4951import android .view .MotionEvent ;
5052import android .view .View ;
5153import com .google .android .material .drawable .DrawableUtils ;
54+ import com .google .android .material .internal .DescendantOffsetUtils ;
5255import com .google .android .material .internal .ThemeEnforcement ;
56+ import com .google .android .material .internal .ViewUtils ;
5357import com .google .android .material .resources .MaterialResources ;
5458import com .google .android .material .shape .CornerFamily ;
5559import com .google .android .material .shape .MaterialShapeDrawable ;
@@ -279,6 +283,9 @@ public Slider(@NonNull Context context, @Nullable AttributeSet attrs, int defSty
279283 ((RippleDrawable ) background ).setColor (haloColor );
280284 DrawableUtils .setRippleDrawableRadius (background , haloRadius );
281285 }
286+ // Because the RippleDrawable can draw outside the bounds of the view, we can set the layer
287+ // type to hardware so we can use PorterDuffXfermode when drawing.
288+ setLayerType (LAYER_TYPE_HARDWARE , null );
282289 }
283290
284291 super .setOnFocusChangeListener (
@@ -300,8 +307,11 @@ public void setEnabled(boolean enabled) {
300307 super .setEnabled (enabled );
301308 // When we're disabled, set the layer type to hardware so we can clear the track out from behind
302309 // the thumb. When enabled set the layer type to none so that the halo can be drawn outside the
303- // bounds of the slider.
304- setLayerType (enabled ? LAYER_TYPE_NONE : LAYER_TYPE_HARDWARE , null );
310+ // bounds of the slider. After Lollipop we use Ripple for the halo, and an Overlay for the
311+ // marker so we don't need to worry about drawing outside the bounds.
312+ if (VERSION .SDK_INT < VERSION_CODES .LOLLIPOP ) {
313+ setLayerType (enabled ? LAYER_TYPE_NONE : LAYER_TYPE_HARDWARE , null );
314+ }
305315 }
306316
307317 @ Override
@@ -360,16 +370,25 @@ private void processAttributes(Context context, AttributeSet attrs, int defStyle
360370
361371 @ NonNull
362372 private TooltipDrawable parseLabelDrawable (@ NonNull Context context , @ NonNull TypedArray a ) {
363- TooltipDrawable label =
364- TooltipDrawable .createFromAttributes (
365- context ,
366- null ,
367- 0 ,
368- a .getResourceId (
369- R .styleable .Slider_labelStyle , R .style .Widget_MaterialComponents_Tooltip ));
370- label .setRelativeToView (this );
373+ return TooltipDrawable .createFromAttributes (
374+ context ,
375+ null ,
376+ 0 ,
377+ a .getResourceId (R .styleable .Slider_labelStyle , R .style .Widget_MaterialComponents_Tooltip ));
378+ }
371379
372- return label ;
380+ @ Override
381+ protected void onAttachedToWindow () {
382+ super .onAttachedToWindow ();
383+ // The label is attached on the Overlay relative to the content.
384+ label .setRelativeToView (ViewUtils .getContentView (this ));
385+ }
386+
387+ @ Override
388+ protected void onDetachedFromWindow () {
389+ super .onDetachedFromWindow ();
390+ ViewUtils .getContentViewOverlay (this ).remove (label );
391+ label .detachView (ViewUtils .getContentView (this ));
373392 }
374393
375394 private void validateValueFrom () {
@@ -679,7 +698,6 @@ protected void onDraw(@NonNull Canvas canvas) {
679698 }
680699
681700 maybeDrawHalo (canvas , trackWidth , top );
682- drawLabel (canvas , trackWidth , top );
683701 }
684702
685703 drawThumb (canvas , trackWidth , top );
@@ -701,13 +719,6 @@ private void drawTicks(@NonNull Canvas canvas) {
701719 canvas .drawPoints (ticksCoordinates , ticksPaint );
702720 }
703721
704- private void drawLabel (@ NonNull Canvas canvas , int width , int top ) {
705- int left = trackSidePadding + (int ) (thumbPosition * width ) - label .getIntrinsicWidth () / 2 ;
706- top -= labelPadding + thumbRadius ;
707- label .setBounds (left , top - label .getIntrinsicHeight (), left + label .getIntrinsicWidth (), top );
708- label .draw (canvas );
709- }
710-
711722 private void drawThumb (@ NonNull Canvas canvas , int width , int top ) {
712723 // Clear out the track behind the thumb if we're in a disable state since the thumb is
713724 // transparent.
@@ -725,7 +736,17 @@ private void drawThumb(@NonNull Canvas canvas, int width, int top) {
725736 private void maybeDrawHalo (@ NonNull Canvas canvas , int width , int top ) {
726737 // Only draw the halo for devices which don't support the ripple.
727738 if (forceDrawCompatShadow || VERSION .SDK_INT < VERSION_CODES .LOLLIPOP ) {
728- canvas .drawCircle (trackSidePadding + thumbPosition * width , top , haloRadius , haloPaint );
739+ int centerX = (int ) (trackSidePadding + thumbPosition * width );
740+ if (VERSION .SDK_INT < VERSION_CODES .LOLLIPOP ) {
741+ // In this case we can clip the rect to allow drawing outside the bounds.
742+ canvas .clipRect (
743+ centerX - haloRadius ,
744+ top - haloRadius ,
745+ centerX + haloRadius ,
746+ top + haloRadius ,
747+ Op .UNION );
748+ }
749+ canvas .drawCircle (centerX , top , haloRadius , haloPaint );
729750 }
730751 }
731752
@@ -747,6 +768,8 @@ public boolean onTouchEvent(@NonNull MotionEvent event) {
747768 thumbPosition = position ;
748769 snapThumbPosition ();
749770 updateHaloHotSpot ();
771+ ensureLabel ();
772+ updateLabelPosition ();
750773 invalidate ();
751774 if (hasOnChangeListener ()) {
752775 listener .onValueChange (this , getValue ());
@@ -756,6 +779,8 @@ public boolean onTouchEvent(@NonNull MotionEvent event) {
756779 thumbPosition = position ;
757780 snapThumbPosition ();
758781 updateHaloHotSpot ();
782+ ensureLabel ();
783+ updateLabelPosition ();
759784 invalidate ();
760785 if (hasOnChangeListener ()) {
761786 listener .onValueChange (this , getValue ());
@@ -766,21 +791,25 @@ public boolean onTouchEvent(@NonNull MotionEvent event) {
766791 thumbIsPressed = false ;
767792 thumbPosition = position ;
768793 snapThumbPosition ();
794+ ViewUtils .getContentViewOverlay (this ).remove (label );
769795 invalidate ();
770796 break ;
771797 default :
772798 // Nothing to do in this case.
773799 }
800+
801+ // Set if the thumb is pressed. This will cause the ripple to be drawn.
802+ setPressed (thumbIsPressed );
803+ return true ;
804+ }
805+
806+ private void ensureLabel () {
774807 float value = getValue ();
775808 if (hasLabelFormatter ()) {
776809 label .setText (formatter .getFormattedValue (value ));
777810 } else {
778811 label .setText (String .format ((int ) value == value ? "%.0f" : "%.2f" , value ));
779812 }
780-
781- // Set if the thumb is pressed. This will cause the ripple to be drawn.
782- setPressed (thumbIsPressed );
783- return true ;
784813 }
785814
786815 private void snapThumbPosition () {
@@ -790,6 +819,21 @@ private void snapThumbPosition() {
790819 }
791820 }
792821
822+ private void updateLabelPosition () {
823+ int left =
824+ trackSidePadding + (int ) (thumbPosition * trackWidth ) - label .getIntrinsicWidth () / 2 ;
825+ int top = calculateTop () - (labelPadding + thumbRadius );
826+ label .setBounds (left , top - label .getIntrinsicHeight (), left + label .getIntrinsicWidth (), top );
827+
828+ // Calculate the difference between the bounds of this view and the bounds of the root view to
829+ // correctly position this view in the overlay layer.
830+ Rect rect = new Rect (label .getBounds ());
831+ DescendantOffsetUtils .offsetDescendantRect (ViewUtils .getContentView (this ), this , rect );
832+ label .setBounds (rect );
833+
834+ ViewUtils .getContentViewOverlay (this ).add (label );
835+ }
836+
793837 @ Override
794838 protected void drawableStateChanged () {
795839 super .drawableStateChanged ();
0 commit comments