com.badlogic.gdx.scenes.scene2d.ui.ScrollPane.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.scenes.scene2d.ui.ScrollPane.java

Source

/*******************************************************************************
 * 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.Batch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Event;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener;
import com.badlogic.gdx.scenes.scene2d.utils.Cullable;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.scenes.scene2d.utils.Layout;
import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack;

/** A group that scrolls a child widget using scrollbars and/or mouse or touch dragging.
 * <p>
 * The widget is sized to its preferred size. If the widget's preferred width or height is less than the size of this scroll pane,
 * it is set to the size of this scroll pane. Scrollbars appear when the widget is larger than the scroll pane.
 * <p>
 * The scroll pane's preferred size is that of the child widget. At this size, the child widget will not need to scroll, so the
 * scroll pane is typically sized by ignoring the preferred size in one or both directions.
 * @author mzechner
 * @author Nathan Sweet */
public class ScrollPane extends WidgetGroup {
    private ScrollPaneStyle style;
    private Actor widget;

    final Rectangle hScrollBounds = new Rectangle();
    final Rectangle vScrollBounds = new Rectangle();
    final Rectangle hKnobBounds = new Rectangle();
    final Rectangle vKnobBounds = new Rectangle();
    private final Rectangle widgetAreaBounds = new Rectangle();
    private final Rectangle widgetCullingArea = new Rectangle();
    private final Rectangle scissorBounds = new Rectangle();
    private ActorGestureListener flickScrollListener;

    boolean scrollX, scrollY;
    boolean vScrollOnRight = true;
    boolean hScrollOnBottom = true;
    float amountX, amountY;
    float visualAmountX, visualAmountY;
    float maxX, maxY;
    boolean touchScrollH, touchScrollV;
    final Vector2 lastPoint = new Vector2();
    float areaWidth, areaHeight;
    private boolean fadeScrollBars = true, smoothScrolling = true;
    float fadeAlpha, fadeAlphaSeconds = 1, fadeDelay, fadeDelaySeconds = 1;
    boolean cancelTouchFocus = true;

    boolean flickScroll = true;
    float velocityX, velocityY;
    float flingTimer;
    private boolean overscrollX = true, overscrollY = true;
    float flingTime = 1f;
    private float overscrollDistance = 50, overscrollSpeedMin = 30, overscrollSpeedMax = 200;
    private boolean forceScrollX, forceScrollY;
    private boolean disableX, disableY;
    private boolean clamp = true;
    private boolean scrollbarsOnTop;
    private boolean variableSizeKnobs = true;
    int draggingPointer = -1;

    /** @param widget May be null. */
    public ScrollPane(Actor widget) {
        this(widget, new ScrollPaneStyle());
    }

    /** @param widget May be null. */
    public ScrollPane(Actor widget, Skin skin) {
        this(widget, skin.get(ScrollPaneStyle.class));
    }

    /** @param widget May be null. */
    public ScrollPane(Actor widget, Skin skin, String styleName) {
        this(widget, skin.get(styleName, ScrollPaneStyle.class));
    }

    /** @param widget May be null. */
    public ScrollPane(Actor widget, ScrollPaneStyle style) {
        if (style == null)
            throw new IllegalArgumentException("style cannot be null.");
        this.style = style;
        setWidget(widget);
        setSize(150, 150);

        addCaptureListener(new InputListener() {
            private float handlePosition;

            public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
                if (draggingPointer != -1)
                    return false;
                if (pointer == 0 && button != 0)
                    return false;
                getStage().setScrollFocus(ScrollPane.this);

                if (!flickScroll)
                    resetFade();

                if (fadeAlpha == 0)
                    return false;

                if (scrollX && hScrollBounds.contains(x, y)) {
                    event.stop();
                    resetFade();
                    if (hKnobBounds.contains(x, y)) {
                        lastPoint.set(x, y);
                        handlePosition = hKnobBounds.x;
                        touchScrollH = true;
                        draggingPointer = pointer;
                        return true;
                    }
                    setScrollX(amountX + areaWidth * (x < hKnobBounds.x ? -1 : 1));
                    return true;
                }
                if (scrollY && vScrollBounds.contains(x, y)) {
                    event.stop();
                    resetFade();
                    if (vKnobBounds.contains(x, y)) {
                        lastPoint.set(x, y);
                        handlePosition = vKnobBounds.y;
                        touchScrollV = true;
                        draggingPointer = pointer;
                        return true;
                    }
                    setScrollY(amountY + areaHeight * (y < vKnobBounds.y ? 1 : -1));
                    return true;
                }
                return false;
            }

            public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
                if (pointer != draggingPointer)
                    return;
                cancel();
            }

            public void touchDragged(InputEvent event, float x, float y, int pointer) {
                if (pointer != draggingPointer)
                    return;
                if (touchScrollH) {
                    float delta = x - lastPoint.x;
                    float scrollH = handlePosition + delta;
                    handlePosition = scrollH;
                    scrollH = Math.max(hScrollBounds.x, scrollH);
                    scrollH = Math.min(hScrollBounds.x + hScrollBounds.width - hKnobBounds.width, scrollH);
                    float total = hScrollBounds.width - hKnobBounds.width;
                    if (total != 0)
                        setScrollPercentX((scrollH - hScrollBounds.x) / total);
                    lastPoint.set(x, y);
                } else if (touchScrollV) {
                    float delta = y - lastPoint.y;
                    float scrollV = handlePosition + delta;
                    handlePosition = scrollV;
                    scrollV = Math.max(vScrollBounds.y, scrollV);
                    scrollV = Math.min(vScrollBounds.y + vScrollBounds.height - vKnobBounds.height, scrollV);
                    float total = vScrollBounds.height - vKnobBounds.height;
                    if (total != 0)
                        setScrollPercentY(1 - ((scrollV - vScrollBounds.y) / total));
                    lastPoint.set(x, y);
                }
            }

            public boolean mouseMoved(InputEvent event, float x, float y) {
                if (!flickScroll)
                    resetFade();
                return false;
            }
        });

        flickScrollListener = new ActorGestureListener() {
            public void pan(InputEvent event, float x, float y, float deltaX, float deltaY) {
                resetFade();
                amountX -= deltaX;
                amountY += deltaY;
                clamp();
                cancelTouchFocusedChild(event);
            }

            public void fling(InputEvent event, float x, float y, int button) {
                if (Math.abs(x) > 150) {
                    flingTimer = flingTime;
                    velocityX = x;
                    cancelTouchFocusedChild(event);
                }
                if (Math.abs(y) > 150) {
                    flingTimer = flingTime;
                    velocityY = -y;
                    cancelTouchFocusedChild(event);
                }
            }

            public boolean handle(Event event) {
                if (super.handle(event)) {
                    if (((InputEvent) event).getType() == InputEvent.Type.touchDown)
                        flingTimer = 0;
                    return true;
                }
                return false;
            }
        };
        addListener(flickScrollListener);

        addListener(new InputListener() {
            public boolean scrolled(InputEvent event, float x, float y, int amount) {
                resetFade();
                if (scrollY)
                    setScrollY(amountY + getMouseWheelY() * amount);
                else if (scrollX) //
                    setScrollX(amountX + getMouseWheelX() * amount);
                else
                    return false;
                return true;
            }
        });
    }

    void resetFade() {
        fadeAlpha = fadeAlphaSeconds;
        fadeDelay = fadeDelaySeconds;
    }

    void cancelTouchFocusedChild(InputEvent event) {
        if (!cancelTouchFocus)
            return;
        Stage stage = getStage();
        if (stage != null)
            stage.cancelTouchFocusExcept(flickScrollListener, this);
    }

    /** If currently scrolling by tracking a touch down, stop scrolling. */
    public void cancel() {
        draggingPointer = -1;
        touchScrollH = false;
        touchScrollV = false;
        flickScrollListener.getGestureDetector().cancel();
    }

    void clamp() {
        if (!clamp)
            return;
        scrollX(overscrollX ? MathUtils.clamp(amountX, -overscrollDistance, maxX + overscrollDistance)
                : MathUtils.clamp(amountX, 0, maxX));
        scrollY(overscrollY ? MathUtils.clamp(amountY, -overscrollDistance, maxY + overscrollDistance)
                : MathUtils.clamp(amountY, 0, maxY));
    }

    public void setStyle(ScrollPaneStyle style) {
        if (style == null)
            throw new IllegalArgumentException("style cannot be null.");
        this.style = style;
        invalidateHierarchy();
    }

    /** Returns the scroll pane's style. Modifying the returned style may not have an effect until
     * {@link #setStyle(ScrollPaneStyle)} is called. */
    public ScrollPaneStyle getStyle() {
        return style;
    }

    public void act(float delta) {
        super.act(delta);

        boolean panning = flickScrollListener.getGestureDetector().isPanning();

        if (fadeAlpha > 0 && fadeScrollBars && !panning && !touchScrollH && !touchScrollV) {
            fadeDelay -= delta;
            if (fadeDelay <= 0)
                fadeAlpha = Math.max(0, fadeAlpha - delta);
        }

        if (flingTimer > 0) {
            resetFade();

            float alpha = flingTimer / flingTime;
            amountX -= velocityX * alpha * delta;
            amountY -= velocityY * alpha * delta;
            clamp();

            // Stop fling if hit overscroll distance.
            if (amountX == -overscrollDistance)
                velocityX = 0;
            if (amountX >= maxX + overscrollDistance)
                velocityX = 0;
            if (amountY == -overscrollDistance)
                velocityY = 0;
            if (amountY >= maxY + overscrollDistance)
                velocityY = 0;

            flingTimer -= delta;
            if (flingTimer <= 0) {
                velocityX = 0;
                velocityY = 0;
            }
        }

        if (smoothScrolling && flingTimer <= 0 && !touchScrollH && !touchScrollV && !panning) {
            if (visualAmountX != amountX) {
                if (visualAmountX < amountX)
                    visualScrollX(Math.min(amountX,
                            visualAmountX + Math.max(200 * delta, (amountX - visualAmountX) * 7 * delta)));
                else
                    visualScrollX(Math.max(amountX,
                            visualAmountX - Math.max(200 * delta, (visualAmountX - amountX) * 7 * delta)));
            }
            if (visualAmountY != amountY) {
                if (visualAmountY < amountY)
                    visualScrollY(Math.min(amountY,
                            visualAmountY + Math.max(200 * delta, (amountY - visualAmountY) * 7 * delta)));
                else
                    visualScrollY(Math.max(amountY,
                            visualAmountY - Math.max(200 * delta, (visualAmountY - amountY) * 7 * delta)));
            }
        } else {
            if (visualAmountX != amountX)
                visualScrollX(amountX);
            if (visualAmountY != amountY)
                visualScrollY(amountY);
        }

        if (!panning) {
            if (overscrollX && scrollX) {
                if (amountX < 0) {
                    resetFade();
                    amountX += (overscrollSpeedMin
                            + (overscrollSpeedMax - overscrollSpeedMin) * -amountX / overscrollDistance) * delta;
                    if (amountX > 0)
                        scrollX(0);
                } else if (amountX > maxX) {
                    resetFade();
                    amountX -= (overscrollSpeedMin
                            + (overscrollSpeedMax - overscrollSpeedMin) * -(maxX - amountX) / overscrollDistance)
                            * delta;
                    if (amountX < maxX)
                        scrollX(maxX);
                }
            }
            if (overscrollY && scrollY) {
                if (amountY < 0) {
                    resetFade();
                    amountY += (overscrollSpeedMin
                            + (overscrollSpeedMax - overscrollSpeedMin) * -amountY / overscrollDistance) * delta;
                    if (amountY > 0)
                        scrollY(0);
                } else if (amountY > maxY) {
                    resetFade();
                    amountY -= (overscrollSpeedMin
                            + (overscrollSpeedMax - overscrollSpeedMin) * -(maxY - amountY) / overscrollDistance)
                            * delta;
                    if (amountY < maxY)
                        scrollY(maxY);
                }
            }
        }
    }

    public void layout() {
        final Drawable bg = style.background;
        final Drawable hScrollKnob = style.hScrollKnob;
        final Drawable vScrollKnob = style.vScrollKnob;

        float bgLeftWidth = 0, bgRightWidth = 0, bgTopHeight = 0, bgBottomHeight = 0;
        if (bg != null) {
            bgLeftWidth = bg.getLeftWidth();
            bgRightWidth = bg.getRightWidth();
            bgTopHeight = bg.getTopHeight();
            bgBottomHeight = bg.getBottomHeight();
        }

        float width = getWidth();
        float height = getHeight();

        float scrollbarHeight = 0;
        if (hScrollKnob != null)
            scrollbarHeight = hScrollKnob.getMinHeight();
        if (style.hScroll != null)
            scrollbarHeight = Math.max(scrollbarHeight, style.hScroll.getMinHeight());
        float scrollbarWidth = 0;
        if (vScrollKnob != null)
            scrollbarWidth = vScrollKnob.getMinWidth();
        if (style.vScroll != null)
            scrollbarWidth = Math.max(scrollbarWidth, style.vScroll.getMinWidth());

        // Get available space size by subtracting background's padded area.
        areaWidth = width - bgLeftWidth - bgRightWidth;
        areaHeight = height - bgTopHeight - bgBottomHeight;

        if (widget == null)
            return;

        // Get widget's desired width.
        float widgetWidth, widgetHeight;
        if (widget instanceof Layout) {
            Layout layout = (Layout) widget;
            widgetWidth = layout.getPrefWidth();
            widgetHeight = layout.getPrefHeight();
        } else {
            widgetWidth = widget.getWidth();
            widgetHeight = widget.getHeight();
        }

        // Determine if horizontal/vertical scrollbars are needed.
        scrollX = forceScrollX || (widgetWidth > areaWidth && !disableX);
        scrollY = forceScrollY || (widgetHeight > areaHeight && !disableY);

        boolean fade = fadeScrollBars;
        if (!fade) {
            // Check again, now taking into account the area that's taken up by any enabled scrollbars.
            if (scrollY) {
                areaWidth -= scrollbarWidth;
                if (!scrollX && widgetWidth > areaWidth && !disableX)
                    scrollX = true;
            }
            if (scrollX) {
                areaHeight -= scrollbarHeight;
                if (!scrollY && widgetHeight > areaHeight && !disableY) {
                    scrollY = true;
                    areaWidth -= scrollbarWidth;
                }
            }
        }

        // The bounds of the scrollable area for the widget.
        widgetAreaBounds.set(bgLeftWidth, bgBottomHeight, areaWidth, areaHeight);

        if (fade) {
            // Make sure widget is drawn under fading scrollbars.
            if (scrollX)
                areaHeight -= scrollbarHeight;
            if (scrollY)
                areaWidth -= scrollbarWidth;
        } else {
            if (scrollbarsOnTop) {
                // Make sure widget is drawn under non-fading scrollbars.
                if (scrollX)
                    widgetAreaBounds.height += scrollbarHeight;
                if (scrollY)
                    widgetAreaBounds.width += scrollbarWidth;
            } else {
                // Offset widget area y for horizontal scrollbar at bottom.
                if (scrollX && hScrollOnBottom)
                    widgetAreaBounds.y += scrollbarHeight;
                // Offset widget area x for vertical scrollbar at left.
                if (scrollY && !vScrollOnRight)
                    widgetAreaBounds.x += scrollbarWidth;
            }
        }

        // If the widget is smaller than the available space, make it take up the available space.
        widgetWidth = disableX ? width : Math.max(areaWidth, widgetWidth);
        widgetHeight = disableY ? height : Math.max(areaHeight, widgetHeight);

        maxX = widgetWidth - areaWidth;
        maxY = widgetHeight - areaHeight;
        if (fade) {
            // Make sure widget is drawn under fading scrollbars.
            if (scrollX)
                maxY -= scrollbarHeight;
            if (scrollY)
                maxX -= scrollbarWidth;
        }
        scrollX(MathUtils.clamp(amountX, 0, maxX));
        scrollY(MathUtils.clamp(amountY, 0, maxY));

        // Set the bounds and scroll knob sizes if scrollbars are needed.
        if (scrollX) {
            if (hScrollKnob != null) {
                float hScrollHeight = style.hScroll != null ? style.hScroll.getMinHeight()
                        : hScrollKnob.getMinHeight();
                // The corner gap where the two scroll bars intersect might have to flip from right to left.
                float boundsX = vScrollOnRight ? bgLeftWidth : bgLeftWidth + scrollbarWidth;
                // Scrollbar on the top or bottom.
                float boundsY = hScrollOnBottom ? bgBottomHeight : height - bgTopHeight - hScrollHeight;
                hScrollBounds.set(boundsX, boundsY, areaWidth, hScrollHeight);
                if (variableSizeKnobs)
                    hKnobBounds.width = Math.max(hScrollKnob.getMinWidth(),
                            (int) (hScrollBounds.width * areaWidth / widgetWidth));
                else
                    hKnobBounds.width = hScrollKnob.getMinWidth();

                hKnobBounds.height = hScrollKnob.getMinHeight();

                hKnobBounds.x = hScrollBounds.x
                        + (int) ((hScrollBounds.width - hKnobBounds.width) * getScrollPercentX());
                hKnobBounds.y = hScrollBounds.y;
            } else {
                hScrollBounds.set(0, 0, 0, 0);
                hKnobBounds.set(0, 0, 0, 0);
            }
        }
        if (scrollY) {
            if (vScrollKnob != null) {
                float vScrollWidth = style.vScroll != null ? style.vScroll.getMinWidth()
                        : vScrollKnob.getMinWidth();
                // the small gap where the two scroll bars intersect might have to flip from bottom to top
                float boundsX, boundsY;
                if (hScrollOnBottom) {
                    boundsY = height - bgTopHeight - areaHeight;
                } else {
                    boundsY = bgBottomHeight;
                }
                // bar on the left or right
                if (vScrollOnRight) {
                    boundsX = width - bgRightWidth - vScrollWidth;
                } else {
                    boundsX = bgLeftWidth;
                }
                vScrollBounds.set(boundsX, boundsY, vScrollWidth, areaHeight);
                vKnobBounds.width = vScrollKnob.getMinWidth();
                if (variableSizeKnobs)
                    vKnobBounds.height = Math.max(vScrollKnob.getMinHeight(),
                            (int) (vScrollBounds.height * areaHeight / widgetHeight));
                else
                    vKnobBounds.height = vScrollKnob.getMinHeight();

                if (vScrollOnRight) {
                    vKnobBounds.x = width - bgRightWidth - vScrollKnob.getMinWidth();
                } else {
                    vKnobBounds.x = bgLeftWidth;
                }
                vKnobBounds.y = vScrollBounds.y
                        + (int) ((vScrollBounds.height - vKnobBounds.height) * (1 - getScrollPercentY()));
            } else {
                vScrollBounds.set(0, 0, 0, 0);
                vKnobBounds.set(0, 0, 0, 0);
            }
        }

        widget.setSize(widgetWidth, widgetHeight);
        if (widget instanceof Layout)
            ((Layout) widget).validate();
    }

    @Override
    public void draw(Batch batch, float parentAlpha) {
        if (widget == null)
            return;

        validate();

        // Setup transform for this group.
        applyTransform(batch, computeTransform());

        if (scrollX)
            hKnobBounds.x = hScrollBounds.x
                    + (int) ((hScrollBounds.width - hKnobBounds.width) * getVisualScrollPercentX());
        if (scrollY)
            vKnobBounds.y = vScrollBounds.y
                    + (int) ((vScrollBounds.height - vKnobBounds.height) * (1 - getVisualScrollPercentY()));

        // Calculate the widget's position depending on the scroll state and available widget area.
        float y = widgetAreaBounds.y;
        if (!scrollY)
            y -= (int) maxY;
        else
            y -= (int) (maxY - visualAmountY);

        if (!fadeScrollBars && scrollbarsOnTop && scrollX) {
            float scrollbarHeight = 0;
            if (style.hScrollKnob != null)
                scrollbarHeight = style.hScrollKnob.getMinHeight();
            if (style.hScroll != null)
                scrollbarHeight = Math.max(scrollbarHeight, style.hScroll.getMinHeight());
            y += scrollbarHeight;
        }

        float x = widgetAreaBounds.x;
        if (scrollX)
            x -= (int) visualAmountX;
        widget.setPosition(x, y);

        if (widget instanceof Cullable) {
            widgetCullingArea.x = -widget.getX() + widgetAreaBounds.x;
            widgetCullingArea.y = -widget.getY() + widgetAreaBounds.y;
            widgetCullingArea.width = widgetAreaBounds.width;
            widgetCullingArea.height = widgetAreaBounds.height;
            ((Cullable) widget).setCullingArea(widgetCullingArea);
        }

        // Draw the background ninepatch.
        Color color = getColor();
        batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
        if (style.background != null) {
            style.background.draw(batch, 0, 0, getWidth(), getHeight());
            batch.flush();
        }

        // Caculate the scissor bounds based on the batch transform, the available widget area and the camera transform. We need to
        // project those to screen coordinates for OpenGL ES to consume.
        getStage().calculateScissors(widgetAreaBounds, scissorBounds);

        // Enable scissors for widget area and draw the widget.
        if (ScissorStack.pushScissors(scissorBounds)) {
            drawChildren(batch, parentAlpha);
            batch.flush();
            ScissorStack.popScissors();
        }

        // Render scrollbars and knobs on top.
        batch.setColor(color.r, color.g, color.b,
                color.a * parentAlpha * Interpolation.fade.apply(fadeAlpha / fadeAlphaSeconds));
        if (scrollX && scrollY) {
            if (style.corner != null) {
                style.corner.draw(batch, hScrollBounds.x + hScrollBounds.width, hScrollBounds.y,
                        vScrollBounds.width, vScrollBounds.y);
            }
        }
        if (scrollX) {
            if (style.hScroll != null)
                style.hScroll.draw(batch, hScrollBounds.x, hScrollBounds.y, hScrollBounds.width,
                        hScrollBounds.height);
            if (style.hScrollKnob != null)
                style.hScrollKnob.draw(batch, hKnobBounds.x, hKnobBounds.y, hKnobBounds.width, hKnobBounds.height);
        }
        if (scrollY) {
            if (style.vScroll != null)
                style.vScroll.draw(batch, vScrollBounds.x, vScrollBounds.y, vScrollBounds.width,
                        vScrollBounds.height);
            if (style.vScrollKnob != null)
                style.vScrollKnob.draw(batch, vKnobBounds.x, vKnobBounds.y, vKnobBounds.width, vKnobBounds.height);
        }

        resetTransform(batch);
    }

    /** Generate fling gesture.
     * @param flingTime Time in seconds for which you want to fling last.
     * @param velocityX Velocity for horizontal direction.
     * @param velocityY Velocity for vertical direction. */
    public void fling(float flingTime, float velocityX, float velocityY) {
        this.flingTimer = flingTime;
        this.velocityX = velocityX;
        this.velocityY = velocityY;
    }

    public float getPrefWidth() {
        if (widget instanceof Layout) {
            float width = ((Layout) widget).getPrefWidth();
            if (style.background != null)
                width += style.background.getLeftWidth() + style.background.getRightWidth();
            return width;
        }
        return 150;
    }

    public float getPrefHeight() {
        if (widget instanceof Layout) {
            float height = ((Layout) widget).getPrefHeight();
            if (style.background != null)
                height += style.background.getTopHeight() + style.background.getBottomHeight();
            return height;
        }
        return 150;
    }

    public float getMinWidth() {
        return 0;
    }

    public float getMinHeight() {
        return 0;
    }

    /** Sets the {@link Actor} embedded in this scroll pane.
     * @param widget May be null to remove any current actor. */
    public void setWidget(Actor widget) {
        if (widget == this)
            throw new IllegalArgumentException("widget cannot be the ScrollPane.");
        if (this.widget != null)
            super.removeActor(this.widget);
        this.widget = widget;
        if (widget != null)
            super.addActor(widget);
    }

    /** Returns the actor embedded in this scroll pane, or null. */
    public Actor getWidget() {
        return widget;
    }

    /** @deprecated ScrollPane may have only a single child.
     * @see #setWidget(Actor) */
    public void addActor(Actor actor) {
        throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
    }

    /** @deprecated ScrollPane may have only a single child.
     * @see #setWidget(Actor) */
    public void addActorAt(int index, Actor actor) {
        throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
    }

    /** @deprecated ScrollPane may have only a single child.
     * @see #setWidget(Actor) */
    public void addActorBefore(Actor actorBefore, Actor actor) {
        throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
    }

    /** @deprecated ScrollPane may have only a single child.
     * @see #setWidget(Actor) */
    public void addActorAfter(Actor actorAfter, Actor actor) {
        throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
    }

    public boolean removeActor(Actor actor) {
        if (actor != widget)
            return false;
        setWidget(null);
        return true;
    }

    public Actor hit(float x, float y, boolean touchable) {
        if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight())
            return null;
        if (scrollX && hScrollBounds.contains(x, y))
            return this;
        if (scrollY && vScrollBounds.contains(x, y))
            return this;
        return super.hit(x, y, touchable);
    }

    /** Called whenever the x scroll amount is changed. */
    protected void scrollX(float pixelsX) {
        this.amountX = pixelsX;
    }

    /** Called whenever the y scroll amount is changed. */
    protected void scrollY(float pixelsY) {
        this.amountY = pixelsY;
    }

    /** Called whenever the visual x scroll amount is changed. */
    protected void visualScrollX(float pixelsX) {
        this.visualAmountX = pixelsX;
    }

    /** Called whenever the visual y scroll amount is changed. */
    protected void visualScrollY(float pixelsY) {
        this.visualAmountY = pixelsY;
    }

    /** Returns the amount to scroll horizontally when the mouse wheel is scrolled. */
    protected float getMouseWheelX() {
        return Math.max(areaWidth * 0.9f, maxX * 0.1f) / 4;
    }

    /** Returns the amount to scroll vertically when the mouse wheel is scrolled. */
    protected float getMouseWheelY() {
        return Math.max(areaHeight * 0.9f, maxY * 0.1f) / 4;
    }

    public void setScrollX(float pixels) {
        scrollX(MathUtils.clamp(pixels, 0, maxX));
    }

    /** Returns the x scroll position in pixels, where 0 is the left of the scroll pane. */
    public float getScrollX() {
        return amountX;
    }

    public void setScrollY(float pixels) {
        scrollY(MathUtils.clamp(pixels, 0, maxY));
    }

    /** Returns the y scroll position in pixels, where 0 is the top of the scroll pane. */
    public float getScrollY() {
        return amountY;
    }

    /** Sets the visual scroll amount equal to the scroll amount. This can be used when setting the scroll amount without animating. */
    public void updateVisualScroll() {
        visualAmountX = amountX;
        visualAmountY = amountY;
    }

    public float getVisualScrollX() {
        return !scrollX ? 0 : visualAmountX;
    }

    public float getVisualScrollY() {
        return !scrollY ? 0 : visualAmountY;
    }

    public float getVisualScrollPercentX() {
        return MathUtils.clamp(visualAmountX / maxX, 0, 1);
    }

    public float getVisualScrollPercentY() {
        return MathUtils.clamp(visualAmountY / maxY, 0, 1);
    }

    public float getScrollPercentX() {
        return MathUtils.clamp(amountX / maxX, 0, 1);
    }

    public void setScrollPercentX(float percentX) {
        scrollX(maxX * MathUtils.clamp(percentX, 0, 1));
    }

    public float getScrollPercentY() {
        return MathUtils.clamp(amountY / maxY, 0, 1);
    }

    public void setScrollPercentY(float percentY) {
        scrollY(maxY * MathUtils.clamp(percentY, 0, 1));
    }

    public void setFlickScroll(boolean flickScroll) {
        if (this.flickScroll == flickScroll)
            return;
        this.flickScroll = flickScroll;
        if (flickScroll)
            addListener(flickScrollListener);
        else
            removeListener(flickScrollListener);
        invalidate();
    }

    public void setFlickScrollTapSquareSize(float halfTapSquareSize) {
        flickScrollListener.getGestureDetector().setTapSquareSize(halfTapSquareSize);
    }

    /** Sets the scroll offset so the specified rectangle is fully in view, if possible. Coordinates are in the scroll pane widget's
     * coordinate system. */
    public void scrollTo(float x, float y, float width, float height) {
        float amountX = this.amountX;
        if (x + width > amountX + areaWidth)
            amountX = x + width - areaWidth;
        if (x < amountX)
            amountX = x;
        scrollX(MathUtils.clamp(amountX, 0, maxX));

        float amountY = this.amountY;
        if (amountY > maxY - y - height + areaHeight)
            amountY = maxY - y - height + areaHeight;
        if (amountY < maxY - y)
            amountY = maxY - y;
        scrollY(MathUtils.clamp(amountY, 0, maxY));
    }

    /** Sets the scroll offset so the specified rectangle is fully in view and centered vertically in the scroll pane, if possible.
     * Coordinates are in the scroll pane widget's coordinate system. */
    public void scrollToCenter(float x, float y, float width, float height) {
        float amountX = this.amountX;
        if (x + width > amountX + areaWidth)
            amountX = x + width - areaWidth;
        if (x < amountX)
            amountX = x;
        scrollX(MathUtils.clamp(amountX, 0, maxX));

        float amountY = this.amountY;
        float centerY = maxY - y + areaHeight / 2 - height / 2;
        if (amountY < centerY - areaHeight / 4 || amountY > centerY + areaHeight / 4)
            amountY = centerY;
        scrollY(MathUtils.clamp(amountY, 0, maxY));
    }

    /** Returns the maximum scroll value in the x direction. */
    public float getMaxX() {
        return maxX;
    }

    /** Returns the maximum scroll value in the y direction. */
    public float getMaxY() {
        return maxY;
    }

    public float getScrollBarHeight() {
        return style.hScrollKnob == null || !scrollX ? 0 : style.hScrollKnob.getMinHeight();
    }

    public float getScrollBarWidth() {
        return style.vScrollKnob == null || !scrollY ? 0 : style.vScrollKnob.getMinWidth();
    }

    /** Returns the width of the scrolled viewport. */
    public float getScrollWidth() {
        return areaWidth;
    }

    /** Returns the height of the scrolled viewport. */
    public float getScrollHeight() {
        return areaHeight;
    }

    /** Returns true if the widget is larger than the scroll pane horizontally. */
    public boolean isScrollX() {
        return scrollX;
    }

    /** Returns true if the widget is larger than the scroll pane vertically. */
    public boolean isScrollY() {
        return scrollY;
    }

    /** Disables scrolling in a direction. The widget will be sized to the FlickScrollPane in the disabled direction. */
    public void setScrollingDisabled(boolean x, boolean y) {
        disableX = x;
        disableY = y;
    }

    public boolean isDragging() {
        return draggingPointer != -1;
    }

    public boolean isPanning() {
        return flickScrollListener.getGestureDetector().isPanning();
    }

    public boolean isFlinging() {
        return flingTimer > 0;
    }

    public void setVelocityX(float velocityX) {
        this.velocityX = velocityX;
    }

    /** Gets the flick scroll y velocity. */
    public float getVelocityX() {
        if (flingTimer <= 0)
            return 0;
        float alpha = flingTimer / flingTime;
        alpha = alpha * alpha * alpha;
        return velocityX * alpha * alpha * alpha;
    }

    public void setVelocityY(float velocityY) {
        this.velocityY = velocityY;
    }

    /** Gets the flick scroll y velocity. */
    public float getVelocityY() {
        return velocityY;
    }

    /** For flick scroll, if true the widget can be scrolled slightly past its bounds and will animate back to its bounds when
     * scrolling is stopped. Default is true. */
    public void setOverscroll(boolean overscrollX, boolean overscrollY) {
        this.overscrollX = overscrollX;
        this.overscrollY = overscrollY;
    }

    /** For flick scroll, sets the overscroll distance in pixels and the speed it returns to the widget's bounds in seconds. Default
     * is 50, 30, 200. */
    public void setupOverscroll(float distance, float speedMin, float speedMax) {
        overscrollDistance = distance;
        overscrollSpeedMin = speedMin;
        overscrollSpeedMax = speedMax;
    }

    /** Forces enabling scrollbars (for non-flick scroll) and overscrolling (for flick scroll) in a direction, even if the contents
     * do not exceed the bounds in that direction. */
    public void setForceScroll(boolean x, boolean y) {
        forceScrollX = x;
        forceScrollY = y;
    }

    public boolean isForceScrollX() {
        return forceScrollX;
    }

    public boolean isForceScrollY() {
        return forceScrollY;
    }

    /** For flick scroll, sets the amount of time in seconds that a fling will continue to scroll. Default is 1. */
    public void setFlingTime(float flingTime) {
        this.flingTime = flingTime;
    }

    /** For flick scroll, prevents scrolling out of the widget's bounds. Default is true. */
    public void setClamp(boolean clamp) {
        this.clamp = clamp;
    }

    /** Set the position of the vertical and horizontal scroll bars. */
    public void setScrollBarPositions(boolean bottom, boolean right) {
        hScrollOnBottom = bottom;
        vScrollOnRight = right;
    }

    /** When true the scrollbars don't reduce the scrollable size and fade out after some time of not being used. */
    public void setFadeScrollBars(boolean fadeScrollBars) {
        if (this.fadeScrollBars == fadeScrollBars)
            return;
        this.fadeScrollBars = fadeScrollBars;
        if (!fadeScrollBars)
            fadeAlpha = fadeAlphaSeconds;
        invalidate();
    }

    public void setupFadeScrollBars(float fadeAlphaSeconds, float fadeDelaySeconds) {
        this.fadeAlphaSeconds = fadeAlphaSeconds;
        this.fadeDelaySeconds = fadeDelaySeconds;
    }

    public void setSmoothScrolling(boolean smoothScrolling) {
        this.smoothScrolling = smoothScrolling;
    }

    /** When false (the default), the widget is clipped so it is not drawn under the scrollbars. When true, the widget is clipped to
     * the entire scroll pane bounds and the scrollbars are drawn on top of the widget. If {@link #setFadeScrollBars(boolean)} is
     * true, the scroll bars are always drawn on top. */
    public void setScrollbarsOnTop(boolean scrollbarsOnTop) {
        this.scrollbarsOnTop = scrollbarsOnTop;
        invalidate();
    }

    public boolean getVariableSizeKnobs() {
        return variableSizeKnobs;
    }

    /** If true, the scroll knobs are sized based on {@link #getMaxX()} or {@link #getMaxY()}. If false, the scroll knobs are sized
     * based on {@link Drawable#getMinWidth()} or {@link Drawable#getMinHeight()}. Default is true. */
    public void setVariableSizeKnobs(boolean variableSizeKnobs) {
        this.variableSizeKnobs = variableSizeKnobs;
    }

    /** When true (default), the {@link Stage#cancelTouchFocus()} touch focus} is cancelled when flick scrolling begins. This causes
     * widgets inside the scrollpane that have received touchDown to receive touchUp when flick scrolling begins. */
    public void setCancelTouchFocus(boolean cancelTouchFocus) {
        this.cancelTouchFocus = cancelTouchFocus;
    }

    public void drawDebug(ShapeRenderer shapes) {
        drawDebugBounds(shapes);
        applyTransform(shapes, computeTransform());
        if (ScissorStack.pushScissors(scissorBounds)) {
            drawDebugChildren(shapes);
            ScissorStack.popScissors();
        }
        resetTransform(shapes);
    }

    /** The style for a scroll pane, see {@link ScrollPane}.
     * @author mzechner
     * @author Nathan Sweet */
    static public class ScrollPaneStyle {
        /** Optional. */
        public Drawable background, corner;
        /** Optional. */
        public Drawable hScroll, hScrollKnob;
        /** Optional. */
        public Drawable vScroll, vScrollKnob;

        public ScrollPaneStyle() {
        }

        public ScrollPaneStyle(Drawable background, Drawable hScroll, Drawable hScrollKnob, Drawable vScroll,
                Drawable vScrollKnob) {
            this.background = background;
            this.hScroll = hScroll;
            this.hScrollKnob = hScrollKnob;
            this.vScroll = vScroll;
            this.vScrollKnob = vScrollKnob;
        }

        public ScrollPaneStyle(ScrollPaneStyle style) {
            this.background = style.background;
            this.hScroll = style.hScroll;
            this.hScrollKnob = style.hScrollKnob;
            this.vScroll = style.vScroll;
            this.vScrollKnob = style.vScrollKnob;
        }
    }
}