Skip to content

Commit df94cd1

Browse files
cketchamwcshi
authored andcommitted
Remove the requirement to setClipChildren false on the parent of Slider
Draw the Slider label with ViewOverlay, and update the way the halo is drawn pre lollipop. This stops any clipping that happens by the bounds view. Resolves #697 PiperOrigin-RevId: 284650572
1 parent 6694175 commit df94cd1

6 files changed

Lines changed: 88 additions & 57 deletions

File tree

catalog/java/io/material/catalog/slider/res/layout/cat_slider_demo_continuous.xml

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
android:layout_height="wrap_content"
2525
android:paddingLeft="16dp"
2626
android:paddingRight="16dp"
27-
android:clipChildren="false"
28-
android:clipToPadding="false"
2927
android:orientation="vertical">
3028

3129
<TextView
@@ -37,8 +35,6 @@
3735
<LinearLayout
3836
android:layout_width="match_parent"
3937
android:layout_height="wrap_content"
40-
android:clipChildren="false"
41-
android:clipToPadding="false"
4238
android:gravity="center_vertical">
4339

4440
<com.google.android.material.switchmaterial.SwitchMaterial
@@ -73,9 +69,7 @@
7369
<LinearLayout
7470
android:layout_width="match_parent"
7571
android:layout_height="wrap_content"
76-
android:gravity="center_vertical"
77-
android:clipChildren="false"
78-
android:clipToPadding="false">
72+
android:gravity="center_vertical">
7973

8074
<com.google.android.material.switchmaterial.SwitchMaterial
8175
android:id="@+id/switch_button_2"
@@ -109,9 +103,7 @@
109103
<LinearLayout
110104
android:layout_width="match_parent"
111105
android:layout_height="wrap_content"
112-
android:gravity="center_vertical"
113-
android:clipChildren="false"
114-
android:clipToPadding="false">
106+
android:gravity="center_vertical">
115107

116108
<com.google.android.material.switchmaterial.SwitchMaterial
117109
android:id="@+id/switch_button_3"

catalog/java/io/material/catalog/slider/res/layout/cat_slider_demo_discrete.xml

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
android:layout_height="wrap_content"
2525
android:paddingLeft="16dp"
2626
android:paddingRight="16dp"
27-
android:clipChildren="false"
28-
android:clipToPadding="false"
2927
android:orientation="vertical">
3028

3129
<TextView
@@ -37,9 +35,7 @@
3735
<LinearLayout
3836
android:layout_width="match_parent"
3937
android:layout_height="wrap_content"
40-
android:gravity="center_vertical"
41-
android:clipChildren="false"
42-
android:clipToPadding="false">
38+
android:gravity="center_vertical">
4339

4440
<androidx.appcompat.widget.SwitchCompat
4541
android:id="@+id/switch_button_1"
@@ -69,9 +65,7 @@
6965
<LinearLayout
7066
android:layout_width="match_parent"
7167
android:layout_height="wrap_content"
72-
android:gravity="center_vertical"
73-
android:clipChildren="false"
74-
android:clipToPadding="false">
68+
android:gravity="center_vertical">
7569

7670
<androidx.appcompat.widget.SwitchCompat
7771
android:id="@+id/switch_button_2"
@@ -101,9 +95,7 @@
10195
<LinearLayout
10296
android:layout_width="match_parent"
10397
android:layout_height="wrap_content"
104-
android:gravity="center_vertical"
105-
android:clipChildren="false"
106-
android:clipToPadding="false">
98+
android:gravity="center_vertical">
10799

108100
<androidx.appcompat.widget.SwitchCompat
109101
android:id="@+id/switch_button_3"
@@ -133,9 +125,7 @@
133125
<LinearLayout
134126
android:layout_width="match_parent"
135127
android:layout_height="wrap_content"
136-
android:gravity="center_vertical"
137-
android:clipChildren="false"
138-
android:clipToPadding="false">
128+
android:gravity="center_vertical">
139129

140130
<androidx.appcompat.widget.SwitchCompat
141131
android:id="@+id/switch_button_4"
@@ -165,9 +155,7 @@
165155
<LinearLayout
166156
android:layout_width="match_parent"
167157
android:layout_height="wrap_content"
168-
android:gravity="center_vertical"
169-
android:clipChildren="false"
170-
android:clipToPadding="false">
158+
android:gravity="center_vertical">
171159

172160
<androidx.appcompat.widget.SwitchCompat
173161
android:id="@+id/switch_button_5"

catalog/java/io/material/catalog/slider/res/layout/cat_slider_fragment.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
android:paddingTop="16dp"
2121
android:paddingLeft="16dp"
2222
android:paddingRight="16dp"
23-
android:clipChildren="false"
24-
android:clipToPadding="false"
2523
android:orientation="vertical">
2624

2725
<com.google.android.material.slider.Slider

lib/java/com/google/android/material/slider/Slider.java

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import android.graphics.Paint.Style;
3030
import android.graphics.PorterDuff.Mode;
3131
import android.graphics.PorterDuffXfermode;
32+
import android.graphics.Rect;
33+
import android.graphics.Region.Op;
3234
import android.graphics.drawable.Drawable;
3335
import android.graphics.drawable.RippleDrawable;
3436
import android.os.Build.VERSION;
@@ -49,7 +51,9 @@
4951
import android.view.MotionEvent;
5052
import android.view.View;
5153
import com.google.android.material.drawable.DrawableUtils;
54+
import com.google.android.material.internal.DescendantOffsetUtils;
5255
import com.google.android.material.internal.ThemeEnforcement;
56+
import com.google.android.material.internal.ViewUtils;
5357
import com.google.android.material.resources.MaterialResources;
5458
import com.google.android.material.shape.CornerFamily;
5559
import 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();

lib/java/com/google/android/material/slider/res/values/dimens.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
<dimen name="mtrl_slider_halo_radius">24dp</dimen>
3232

3333
<dimen name="mtrl_slider_label_square_side">26dp</dimen>
34-
<dimen name="mtrl_slider_label_width">26dp</dimen>
35-
<dimen name="mtrl_slider_label_height">26dp</dimen>
3634
<dimen name="mtrl_slider_label_radius">13dp</dimen>
3735
<dimen name="mtrl_slider_label_padding">4dp</dimen>
3836
</resources>

lib/java/com/google/android/material/tooltip/TooltipDrawable.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public class TooltipDrawable extends MaterialShapeDrawable implements TextDrawab
7474
new TextDrawableHelper(/* delegate= */ this);
7575

7676
@NonNull
77-
private final OnLayoutChangeListener attachedViewLayoutChaneListener =
77+
private final OnLayoutChangeListener attachedViewLayoutChangeListener =
7878
new OnLayoutChangeListener() {
7979
@Override
8080
public void onLayoutChange(
@@ -331,11 +331,22 @@ public void setLayoutMargin(@Px int layoutMargin) {
331331
/**
332332
* Should be called to allow this drawable to calculate its position within the current display
333333
* frame. This allows it to apply to specified window padding.
334+
*
335+
* @see #detachView(View)
334336
*/
335337
public void setRelativeToView(@NonNull View view) {
336338
updateLocationOnScreen(view);
337339
// Listen for changes that indicate the view has moved so the location can be updated
338-
view.addOnLayoutChangeListener(attachedViewLayoutChaneListener);
340+
view.addOnLayoutChangeListener(attachedViewLayoutChangeListener);
341+
}
342+
343+
/**
344+
* Should be called when the view is detached from the screen.
345+
*
346+
* @see #setRelativeToView(View)
347+
*/
348+
public void detachView(@NonNull View view) {
349+
view.removeOnLayoutChangeListener(attachedViewLayoutChangeListener);
339350
}
340351

341352
@Override

0 commit comments

Comments
 (0)