diff --git a/catalog/java/io/material/catalog/slider/SliderFragment.java b/catalog/java/io/material/catalog/slider/SliderFragment.java index 1e170deec5e..e9b6c2dd609 100644 --- a/catalog/java/io/material/catalog/slider/SliderFragment.java +++ b/catalog/java/io/material/catalog/slider/SliderFragment.java @@ -77,6 +77,13 @@ public Fragment createFragment() { return new SliderScrollContainerDemoFragment(); } }); + additionalDemos.add( + new Demo(R.string.cat_slider_demo_label_behavior_title) { + @Override + public Fragment createFragment() { + return new SliderLabelBehaviorDemoFragment(); + } + }); return additionalDemos; } diff --git a/catalog/java/io/material/catalog/slider/SliderLabelBehaviorDemoFragment.java b/catalog/java/io/material/catalog/slider/SliderLabelBehaviorDemoFragment.java new file mode 100644 index 00000000000..c5f48e48539 --- /dev/null +++ b/catalog/java/io/material/catalog/slider/SliderLabelBehaviorDemoFragment.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.material.catalog.slider; + +import io.material.catalog.R; + +import android.os.Bundle; +import androidx.appcompat.widget.SwitchCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.slider.RangeSlider; +import com.google.android.material.slider.Slider; +import io.material.catalog.feature.DemoFragment; + +/** + * Fragment to display a few basic uses of the {@link Slider} widget with different label behaviors + * for the Catalog app. + */ +public class SliderLabelBehaviorDemoFragment extends DemoFragment { + + @Nullable + @Override + public View onCreateDemoView( + @NonNull LayoutInflater inflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { + View view = + inflater.inflate( + R.layout.cat_slider_demo_label_behavior, viewGroup, false /* attachToRoot */); + + setUpSlider(view, R.id.switch_button_1, R.id.slider_1); + setUpSlider(view, R.id.switch_button_2, R.id.slider_2); + setUpSlider(view, R.id.switch_button_3, R.id.slider_3); + setUpSlider(view, R.id.switch_button_4, R.id.slider_4); + + return view; + } + + private void setUpSlider( + View view, @IdRes int switchId, @IdRes int sliderId) { + final RangeSlider slider = view.findViewById(sliderId); + SwitchCompat switchButton = view.findViewById(switchId); + switchButton.setOnCheckedChangeListener( + (buttonView, isChecked) -> slider.setEnabled(isChecked)); + switchButton.setChecked(true); + } +} diff --git a/catalog/java/io/material/catalog/slider/res/layout/cat_slider_demo_label_behavior.xml b/catalog/java/io/material/catalog/slider/res/layout/cat_slider_demo_label_behavior.xml new file mode 100644 index 00000000000..de7ed37d4f4 --- /dev/null +++ b/catalog/java/io/material/catalog/slider/res/layout/cat_slider_demo_label_behavior.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/catalog/java/io/material/catalog/slider/res/values/strings.xml b/catalog/java/io/material/catalog/slider/res/values/strings.xml index ee690ed23a0..9cf7d3a05f8 100644 --- a/catalog/java/io/material/catalog/slider/res/values/strings.xml +++ b/catalog/java/io/material/catalog/slider/res/values/strings.xml @@ -15,10 +15,11 @@ limitations under the License. --> - Slider - Continuous Slider demo - Discrete Slider demo - Slider in scrolling container demo + Slider + Continuous Slider demo + Discrete Slider demo + Slider in scrolling container demo + Slider label behavior demo Sliders let users select from a range of values by moving the slider thumb. @@ -28,14 +29,18 @@ Discrete sliders allow users to select a specific value from a range. - Set - This one goes to eleven - From 100 to 1000 - Negative numbers! - With a label formatter - I can have decimal numbers? - Without tick marks - Slider started being touched - Slider stopped being touched + Set + This one goes to eleven + From 100 to 1000 + Negative numbers! + With a label formatter + I can have decimal numbers? + Without tick marks + Slider started being touched + Slider stopped being touched + This one has floating labels + This one has labels within bounds + This one shows no labels + This one always shows labels diff --git a/docs/components/Slider.md b/docs/components/Slider.md index 017c18734b6..d0275f8537d 100644 --- a/docs/components/Slider.md +++ b/docs/components/Slider.md @@ -145,6 +145,7 @@ The modes of `app:labelBehavior` are: view * `withinBounds` - draws the label floating within the bounds of this view * `gone` - prevents the label from being drawn +* `visible` - always draws the label ### Setting a `LabelFormatter` diff --git a/lib/java/com/google/android/material/slider/BaseSlider.java b/lib/java/com/google/android/material/slider/BaseSlider.java index fd4d6771a09..ac793f6aa21 100644 --- a/lib/java/com/google/android/material/slider/BaseSlider.java +++ b/lib/java/com/google/android/material/slider/BaseSlider.java @@ -22,11 +22,15 @@ import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_FLOAT; import static com.google.android.material.slider.LabelFormatter.LABEL_FLOATING; import static com.google.android.material.slider.LabelFormatter.LABEL_GONE; +import static com.google.android.material.slider.LabelFormatter.LABEL_VISIBLE; import static com.google.android.material.slider.LabelFormatter.LABEL_WITHIN_BOUNDS; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import static java.lang.Float.compare; import static java.lang.Math.abs; import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.util.Collections.max; +import static java.util.Collections.min; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -312,9 +316,10 @@ private interface TooltipDrawableFactory { * will always be drawn within the bounds of this view. This means extra space will be * visible above the slider when the label is not visible. *
  • {@code LABEL_GONE}: The label will never be drawn. + *
  • {@code LABEL_VISIBLE}: The label will never be hidden. * */ - @IntDef({LABEL_FLOATING, LABEL_WITHIN_BOUNDS, LABEL_GONE}) + @IntDef({LABEL_FLOATING, LABEL_WITHIN_BOUNDS, LABEL_GONE, LABEL_VISIBLE}) @Retention(RetentionPolicy.SOURCE) @interface LabelBehavior {} @@ -482,7 +487,7 @@ private void processAttributes(Context context, AttributeSet attrs, int defStyle setTrackHeight(a.getDimensionPixelSize(R.styleable.Slider_trackHeight, 0)); - labelBehavior = a.getInt(R.styleable.Slider_labelBehavior, LABEL_FLOATING); + setLabelBehavior(a.getInt(R.styleable.Slider_labelBehavior, LABEL_FLOATING)); if (!a.getBoolean(R.styleable.Slider_android_enabled, true)) { setEnabled(false); @@ -837,9 +842,9 @@ void setCustomThumbDrawable(@NonNull Drawable drawable) { /** * Sets custom thumb drawables. The drawables provided will be used in its corresponding value - * position - i.e., the first drawable will be used to indicate the first value, and so on. If - * the number of drawables is less than the number of values, the default drawable will be used - * for the remaining values. + * position - i.e., the first drawable will be used to indicate the first value, and so on. If the + * number of drawables is less than the number of values, the default drawable will be used for + * the remaining values. * *

    Note that the custom drawables provided will be resized to match the thumb radius set by * {@link #setThumbRadius(int)} or {@link #setThumbRadiusResource(int)}. Be aware that the image @@ -857,9 +862,9 @@ void setCustomThumbDrawablesForValues(@NonNull @DrawableRes int... customThumbDr /** * Sets custom thumb drawables. The drawables provided will be used in its corresponding value - * position - i.e., the first drawable will be used to indicate the first value, and so on. If - * the number of drawables is less than the number of values, the default drawable will be used - * for the remaining values. + * position - i.e., the first drawable will be used to indicate the first value, and so on. If the + * number of drawables is less than the number of values, the default drawable will be used for + * the remaining values. * *

    Note that the custom drawables provided will be resized to match the thumb radius set by * {@link #setThumbRadius(int)} or {@link #setThumbRadiusResource(int)}. Be aware that the image @@ -889,7 +894,7 @@ private void adjustCustomThumbDrawableBounds(Drawable drawable) { if (originalWidth == -1 && originalHeight == -1) { drawable.setBounds(0, 0, thumbDiameter, thumbDiameter); } else { - float scaleRatio = (float) thumbDiameter / Math.max(originalWidth, originalHeight); + float scaleRatio = (float) thumbDiameter / max(originalWidth, originalHeight); drawable.setBounds( 0, 0, (int) (originalWidth * scaleRatio), (int) (originalHeight * scaleRatio)); } @@ -920,8 +925,8 @@ public int getActiveThumbIndex() { } /** - * Registers a callback to be invoked when the slider changes. - * On the RangeSlider implementation, the listener is invoked once for each value. + * Registers a callback to be invoked when the slider changes. On the RangeSlider implementation, + * the listener is invoked once for each value. * * @param listener The callback to run when the slider changes */ @@ -1017,9 +1022,9 @@ public void setThumbElevationResource(@DimenRes int elevation) { /** * Returns the radius of the thumb. Note that setting this will also affect custom drawables set - * through {@link #setCustomThumbDrawable(int)}, {@link #setCustomThumbDrawable(Drawable)}, - * {@link #setCustomThumbDrawablesForValues(int...)}, and - * {@link #setCustomThumbDrawablesForValues(Drawable...)}. + * through {@link #setCustomThumbDrawable(int)}, {@link #setCustomThumbDrawable(Drawable)}, {@link + * #setCustomThumbDrawablesForValues(int...)}, and {@link + * #setCustomThumbDrawablesForValues(Drawable...)}. * * @see #setThumbRadius(int) * @see #setThumbRadiusResource(int) @@ -1032,9 +1037,9 @@ public int getThumbRadius() { /** * Sets the radius of the thumb in pixels. Note that setting this will also affect custom - * drawables set through {@link #setCustomThumbDrawable(int)}, - * {@link #setCustomThumbDrawable(Drawable)}, {@link #setCustomThumbDrawablesForValues(int...)}, - * and {@link #setCustomThumbDrawablesForValues(Drawable...)}. + * drawables set through {@link #setCustomThumbDrawable(int)}, {@link + * #setCustomThumbDrawable(Drawable)}, {@link #setCustomThumbDrawablesForValues(int...)}, and + * {@link #setCustomThumbDrawablesForValues(Drawable...)}. * * @see #getThumbRadius() * @attr ref com.google.android.material.R.styleable#Slider_thumbRadius @@ -1063,9 +1068,9 @@ public void setThumbRadius(@IntRange(from = 0) @Dimension int radius) { /** * Sets the radius of the thumb from a dimension resource. Note that setting this will also affect - * custom drawables set through {@link #setCustomThumbDrawable(int)}, - * {@link #setCustomThumbDrawable(Drawable)}, {@link #setCustomThumbDrawablesForValues(int...)}, - * and {@link #setCustomThumbDrawablesForValues(Drawable...)}. + * custom drawables set through {@link #setCustomThumbDrawable(int)}, {@link + * #setCustomThumbDrawable(Drawable)}, {@link #setCustomThumbDrawablesForValues(int...)}, and + * {@link #setCustomThumbDrawablesForValues(Drawable...)}. * * @see #getThumbRadius() * @attr ref com.google.android.material.R.styleable#Slider_thumbRadius @@ -1225,6 +1230,16 @@ public void setLabelBehavior(@LabelBehavior int labelBehavior) { } } + /** + * Returns whether the labels should be always shown based on the {@link LabelBehavior}. + * + * @see LabelBehavior + * @attr ref com.google.android.material.R.styleable#Slider_labelBehavior + */ + private boolean shouldAlwaysShowLabel() { + return this.labelBehavior == LABEL_VISIBLE; + } + /** Returns the side padding of the track. */ @Dimension() public int getTrackSidePadding() { @@ -1580,7 +1595,9 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec, MeasureSpec.makeMeasureSpec( widgetHeight - + (labelBehavior == LABEL_WITHIN_BOUNDS ? labels.get(0).getIntrinsicHeight() : 0), + + (labelBehavior == LABEL_WITHIN_BOUNDS || shouldAlwaysShowLabel() + ? labels.get(0).getIntrinsicHeight() + : 0), MeasureSpec.EXACTLY)); } @@ -1599,7 +1616,7 @@ private void maybeCalculateTicksCoordinates() { int tickCount = (int) ((valueTo - valueFrom) / stepSize + 1); // Limit the tickCount if they will be too dense. - tickCount = Math.min(tickCount, trackWidth / (trackHeight * 2) + 1); + tickCount = min(tickCount, trackWidth / (trackHeight * 2) + 1); if (ticksCoordinates == null || ticksCoordinates.length != tickCount * 2) { ticksCoordinates = new float[tickCount * 2]; } @@ -1613,7 +1630,7 @@ private void maybeCalculateTicksCoordinates() { private void updateTrackWidth(int width) { // Update the visible track width. - trackWidth = Math.max(width - trackSidePadding * 2, 0); + trackWidth = max(width - trackSidePadding * 2, 0); // Update the visible tick coordinates. maybeCalculateTicksCoordinates(); @@ -1634,7 +1651,9 @@ private void updateHaloHotspot() { private int calculateTop() { return trackTop - + (labelBehavior == LABEL_WITHIN_BOUNDS ? labels.get(0).getIntrinsicHeight() : 0); + + (labelBehavior == LABEL_WITHIN_BOUNDS || shouldAlwaysShowLabel() + ? labels.get(0).getIntrinsicHeight() + : 0); } @Override @@ -1651,19 +1670,22 @@ protected void onDraw(@NonNull Canvas canvas) { int top = calculateTop(); drawInactiveTrack(canvas, trackWidth, top); - if (Collections.max(getValues()) > valueFrom) { + if (max(getValues()) > valueFrom) { drawActiveTrack(canvas, trackWidth, top); } maybeDrawTicks(canvas); - if ((thumbIsPressed || isFocused()) && isEnabled()) { + if ((thumbIsPressed || isFocused() || shouldAlwaysShowLabel()) && isEnabled()) { maybeDrawHalo(canvas, trackWidth, top); - - // Draw labels if there is an active thumb. - if (activeThumbIdx != -1) { + // Draw labels if there is an active thumb or the labels are always visible. + if (activeThumbIdx != -1 || shouldAlwaysShowLabel()) { ensureLabelsAdded(); + } else { + ensureLabelsRemoved(); } + } else { + ensureLabelsRemoved(); } drawThumbs(canvas, trackWidth, top); @@ -1674,8 +1696,8 @@ protected void onDraw(@NonNull Canvas canvas) { * float[1]} is the normalized right position of the range. */ private float[] getActiveRange() { - float max = Collections.max(getValues()); - float min = Collections.min(getValues()); + float max = max(getValues()); + float min = min(getValues()); float left = normalizeValue(values.size() == 1 ? valueFrom : min); float right = normalizeValue(max); @@ -1769,8 +1791,7 @@ private void drawThumbDrawable( trackSidePadding + (int) (normalizeValue(value) * width) - (thumbDrawable.getBounds().width() / 2f), - top - - (thumbDrawable.getBounds().height() / 2f)); + top - (thumbDrawable.getBounds().height() / 2f)); thumbDrawable.draw(canvas); canvas.restore(); } @@ -1805,8 +1826,8 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { } float x = event.getX(); touchPosition = (x - trackSidePadding) / trackWidth; - touchPosition = Math.max(0, touchPosition); - touchPosition = Math.min(1, touchPosition); + touchPosition = max(0, touchPosition); + touchPosition = min(1, touchPosition); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: @@ -1869,7 +1890,6 @@ && abs(lastEvent.getY() - event.getY()) <= scaledTouchSlop) { activeThumbIdx = -1; onStopTrackingTouch(); } - ensureLabelsRemoved(); invalidate(); break; default: @@ -2299,7 +2319,6 @@ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: activeThumbIdx = -1; - ensureLabelsRemoved(); postInvalidate(); return true; default: @@ -2441,7 +2460,6 @@ protected void onFocusChanged( super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (!gainFocus) { activeThumbIdx = -1; - ensureLabelsRemoved(); accessibilityHelper.clearKeyboardFocusForVirtualView(focusedThumbIdx); } else { focusThumbOnFocusGained(direction); @@ -2610,7 +2628,7 @@ void updateBoundsForVirturalViewId(int virtualViewId, Rect virtualViewBounds) { private static class AccessibilityHelper extends ExploreByTouchHelper { private final BaseSlider slider; - Rect virtualViewBounds = new Rect(); + final Rect virtualViewBounds = new Rect(); AccessibilityHelper(BaseSlider slider) { super(slider); diff --git a/lib/java/com/google/android/material/slider/LabelFormatter.java b/lib/java/com/google/android/material/slider/LabelFormatter.java index 83a00bd3276..d108cdbe9d0 100644 --- a/lib/java/com/google/android/material/slider/LabelFormatter.java +++ b/lib/java/com/google/android/material/slider/LabelFormatter.java @@ -27,6 +27,7 @@ public interface LabelFormatter { int LABEL_FLOATING = 0; int LABEL_WITHIN_BOUNDS = 1; int LABEL_GONE = 2; + int LABEL_VISIBLE = 3; @NonNull String getFormattedValue(float value); diff --git a/lib/java/com/google/android/material/slider/res/values/attrs.xml b/lib/java/com/google/android/material/slider/res/values/attrs.xml index 770cb5d6ecb..fc0e9005c33 100644 --- a/lib/java/com/google/android/material/slider/res/values/attrs.xml +++ b/lib/java/com/google/android/material/slider/res/values/attrs.xml @@ -32,8 +32,10 @@ - + + +