/******************************************************************************* * Copyright 2011 See AUTHORS file. * * 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 com.badlogic.gdx.scenes.scene2d.ui; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.NinePatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Pools; /** * A slider is a horizontal indicator that allows a user to set a value. The slider his a range (min, max) and a * stepping between each value the slider represents. * <p> * {@link ChangeEvent} is fired when the slider knob is moved. Cancelling the event will move the knob to where it was * previously. * <p> * The preferred height of a slider is determined by the larger of the knob and background. The preferred width of a * slider is 140, a relatively arbitrary size. * * @author mzechner * @author Nathan Sweet */ public class Slider extends Widget { private SliderStyle style; private float min, max, stepSize; private float value, animateFromValue; private float sliderPos; private final boolean vertical; int draggingPointer = -1; private float animateDuration, animateTime; private Interpolation animateInterpolation = Interpolation.linear; public Slider(float min, float max, float stepSize, boolean vertical, Skin skin) { this(min, max, stepSize, vertical, skin.get("default-" + (vertical ? "vertical" : "horizontal"), SliderStyle.class)); } public Slider(float min, float max, float stepSize, boolean vertical, Skin skin, String styleName) { this(min, max, stepSize, vertical, skin.get(styleName, SliderStyle.class)); } /** * Creates a new slider. It's width is determined by the given prefWidth parameter, its height is determined by the * maximum of the height of either the slider {@link NinePatch} or slider handle {@link TextureRegion}. The min and * max values determine the range the values of this slider can take on, the stepSize parameter specifies the * distance between individual values. E.g. min could be 4, max could be 10 and stepSize could be 0.2, giving you a * total of 30 values, 4.0 4.2, 4.4 and so on. * * @param min * the minimum value * @param max * the maximum value * @param stepSize * the step size between values * @param style * the {@link SliderStyle} */ public Slider(float min, float max, float stepSize, boolean vertical, SliderStyle style) { if (min > max) throw new IllegalArgumentException("min must be > max: " + min + " > " + max); if (stepSize <= 0) throw new IllegalArgumentException("stepSize must be > 0: " + stepSize); setStyle(style); this.min = min; this.max = max; this.stepSize = stepSize; this.vertical = vertical; this.value = min; setWidth(getPrefWidth()); setHeight(getPrefHeight()); addListener(new InputListener() { public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) { if (draggingPointer != -1) return false; draggingPointer = pointer; calculatePositionAndValue(x, y); return true; } public void touchUp(InputEvent event, float x, float y, int pointer, int button) { if (pointer != draggingPointer) return; draggingPointer = -1; calculatePositionAndValue(x, y); } public void touchDragged(InputEvent event, float x, float y, int pointer) { calculatePositionAndValue(x, y); } }); } public void setStyle(SliderStyle style) { if (style == null) throw new IllegalArgumentException("style cannot be null."); this.style = style; invalidateHierarchy(); } /** * Returns the slider's style. Modifying the returned style may not have an effect until * {@link #setStyle(SliderStyle)} is called. */ public SliderStyle getStyle() { return style; } public void act(float delta) { super.act(delta); animateTime -= delta; } @Override public void draw(SpriteBatch batch, float parentAlpha) { final Drawable knob = style.knob; final Drawable bg = style.background; final Drawable knobBefore = style.knobBefore; final Drawable knobAfter = style.knobAfter; Color color = getColor(); float x = getX(); float y = getY(); float width = getWidth(); float height = getHeight(); float knobHeight = knob == null ? 0 : knob.getMinHeight(); float knobWidth = knob == null ? 0 : knob.getMinWidth(); float value = getVisualValue(); batch.setColor(color.r, color.g, color.b, color.a * parentAlpha); if (vertical) { bg.draw(batch, x + (int) ((width - bg.getMinWidth()) * 0.5f), y, bg.getMinWidth(), height); float sliderPosHeight = height - (bg.getTopHeight() + bg.getBottomHeight()); sliderPos = (value - min) / (max - min) * (sliderPosHeight - knobHeight); sliderPos = Math.max(0, sliderPos); sliderPos = Math.min(sliderPosHeight - knobHeight, sliderPos) + bg.getBottomHeight(); float knobHeightHalf = knobHeight * 0.5f; if (knobBefore != null) { knobBefore.draw(batch, x + (int) ((width - knobBefore.getMinWidth()) * 0.5f), y, knobBefore.getMinWidth(), (int) (sliderPos + knobHeightHalf)); } if (knobAfter != null) { knobAfter.draw(batch, x + (int) ((width - knobAfter.getMinWidth()) * 0.5f), y + (int) (sliderPos + knobHeightHalf), knobAfter.getMinWidth(), height - (int) (sliderPos + knobHeightHalf)); } if (knob != null) knob.draw(batch, x + (int) ((width - knobWidth) * 0.5f), (int) (y + sliderPos), knobWidth, knobHeight); } else { bg.draw(batch, x, y + (int) ((height - bg.getMinHeight()) * 0.5f), width, bg.getMinHeight()); float sliderPosWidth = width - (bg.getLeftWidth() + bg.getRightWidth()); sliderPos = (value - min) / (max - min) * (sliderPosWidth - knobWidth); sliderPos = Math.max(0, sliderPos); sliderPos = Math.min(sliderPosWidth - knobWidth, sliderPos) + bg.getLeftWidth(); float knobHeightHalf = knobHeight * 0.5f; if (knobBefore != null) { knobBefore.draw(batch, x, y + (int) ((height - knobBefore.getMinHeight()) * 0.5f), (int) (sliderPos + knobHeightHalf), knobBefore.getMinHeight()); } if (knobAfter != null) { knobAfter.draw(batch, x + (int) (sliderPos + knobHeightHalf), y + (int) ((height - knobAfter.getMinHeight()) * 0.5f), width - (int) (sliderPos + knobHeightHalf), knobAfter.getMinHeight()); } if (knob != null) knob.draw(batch, (int) (x + sliderPos), (int) (y + (height - knobHeight) * 0.5f), knobWidth, knobHeight); } } void calculatePositionAndValue(float x, float y) { final Drawable knob = style.knob; final Drawable bg = style.background; float value; float oldPosition = sliderPos; if (vertical) { float height = getHeight() - bg.getTopHeight() - bg.getBottomHeight(); float knobHeight = knob == null ? 0 : knob.getMinHeight(); sliderPos = y - bg.getBottomHeight() - knobHeight * 0.5f; sliderPos = Math.max(0, sliderPos); sliderPos = Math.min(height - knobHeight, sliderPos); value = min + (max - min) * (sliderPos / (height - knobHeight)); } else { float width = getWidth() - bg.getLeftWidth() - bg.getRightWidth(); float knobWidth = knob == null ? 0 : knob.getMinWidth(); sliderPos = x - bg.getLeftWidth() - knobWidth * 0.5f; sliderPos = Math.max(0, sliderPos); sliderPos = Math.min(width - knobWidth, sliderPos); value = min + (max - min) * (sliderPos / (width - knobWidth)); } float oldValue = value; setValue(value); if (value == oldValue) sliderPos = oldPosition; } /** Returns true if the slider is being dragged. */ public boolean isDragging() { return draggingPointer != -1; } public float getValue() { return value; } /** If {@link #setAnimateDuration(float) animating} the slider value, this returns the value current displayed. */ public float getVisualValue() { if (animateTime > 0) return animateInterpolation.apply(animateFromValue, value, 1 - animateTime / animateDuration); return value; } /** Sets the slider position, rounded to the nearest step size and clamped to the minumum and maximim values. */ public void setValue(float value) { if (value < min || value > max) throw new IllegalArgumentException("value must be >= min and <= max: " + value); value = MathUtils.clamp(Math.round(value / stepSize) * stepSize, min, max); float oldValue = this.value; if (value == oldValue) return; float oldVisualValue = getVisualValue(); this.value = value; ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); if (fire(changeEvent)) this.value = oldValue; else if (animateDuration > 0) { animateFromValue = oldVisualValue; animateTime = animateDuration; } Pools.free(changeEvent); } /** Sets the range of this slider. The slider's current value is reset to min. */ public void setRange(float min, float max) { if (min >= max) throw new IllegalArgumentException("min must be < max"); this.min = min; this.max = max; setValue(min); } /** Sets the step size of the slider */ public void setStepSize(float stepSize) { if (stepSize <= 0) throw new IllegalArgumentException("steps must be > 0: " + stepSize); this.stepSize = stepSize; } public float getPrefWidth() { if (vertical) return Math.max(style.knob == null ? 0 : style.knob.getMinWidth(), style.background.getMinWidth()); else return 140; } public float getPrefHeight() { if (vertical) return 140; else return Math.max(style.knob == null ? 0 : style.knob.getMinHeight(), style.background.getMinHeight()); } public float getMinValue() { return this.min; } public float getMaxValue() { return this.max; } public float getStepSize() { return this.stepSize; } /** If > 0, changes to the slider value via {@link #setValue(float)} will happen over this duration in seconds. */ public void setAnimateDuration(float duration) { this.animateDuration = duration; } /** Sets the interpolation to use for {@link #setAnimateDuration(float)}. */ public void setAnimateInterpolation(Interpolation animateInterpolation) { if (animateInterpolation == null) throw new IllegalArgumentException("animateInterpolation cannot be null."); this.animateInterpolation = animateInterpolation; } /** * The style for a slider, see {@link Slider}. * * @author mzechner * @author Nathan Sweet */ static public class SliderStyle { /** The slider background, stretched only in one direction. */ public Drawable background; /** Optional, centered on the background. */ public Drawable knob; /** Optional. */ public Drawable knobBefore, knobAfter; public SliderStyle() { } public SliderStyle(Drawable background, Drawable knob) { this.background = background; this.knob = knob; } public SliderStyle(SliderStyle style) { this.background = style.background; this.knob = style.knob; } } }