• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright 2011 See AUTHORS file.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  ******************************************************************************/
16 
17 package com.badlogic.gdx.scenes.scene2d.ui;
18 
19 import com.badlogic.gdx.Gdx;
20 import com.badlogic.gdx.graphics.Color;
21 import com.badlogic.gdx.graphics.g2d.Batch;
22 import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
23 import com.badlogic.gdx.math.Interpolation;
24 import com.badlogic.gdx.math.MathUtils;
25 import com.badlogic.gdx.math.Rectangle;
26 import com.badlogic.gdx.math.Vector2;
27 import com.badlogic.gdx.scenes.scene2d.Actor;
28 import com.badlogic.gdx.scenes.scene2d.Event;
29 import com.badlogic.gdx.scenes.scene2d.InputEvent;
30 import com.badlogic.gdx.scenes.scene2d.InputListener;
31 import com.badlogic.gdx.scenes.scene2d.Stage;
32 import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener;
33 import com.badlogic.gdx.scenes.scene2d.utils.Cullable;
34 import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
35 import com.badlogic.gdx.scenes.scene2d.utils.Layout;
36 import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack;
37 
38 /** A group that scrolls a child widget using scrollbars and/or mouse or touch dragging.
39  * <p>
40  * 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,
41  * it is set to the size of this scroll pane. Scrollbars appear when the widget is larger than the scroll pane.
42  * <p>
43  * 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
44  * scroll pane is typically sized by ignoring the preferred size in one or both directions.
45  * @author mzechner
46  * @author Nathan Sweet */
47 public class ScrollPane extends WidgetGroup {
48 	private ScrollPaneStyle style;
49 	private Actor widget;
50 
51 	final Rectangle hScrollBounds = new Rectangle();
52 	final Rectangle vScrollBounds = new Rectangle();
53 	final Rectangle hKnobBounds = new Rectangle();
54 	final Rectangle vKnobBounds = new Rectangle();
55 	private final Rectangle widgetAreaBounds = new Rectangle();
56 	private final Rectangle widgetCullingArea = new Rectangle();
57 	private final Rectangle scissorBounds = new Rectangle();
58 	private ActorGestureListener flickScrollListener;
59 
60 	boolean scrollX, scrollY;
61 	boolean vScrollOnRight = true;
62 	boolean hScrollOnBottom = true;
63 	float amountX, amountY;
64 	float visualAmountX, visualAmountY;
65 	float maxX, maxY;
66 	boolean touchScrollH, touchScrollV;
67 	final Vector2 lastPoint = new Vector2();
68 	float areaWidth, areaHeight;
69 	private boolean fadeScrollBars = true, smoothScrolling = true;
70 	float fadeAlpha, fadeAlphaSeconds = 1, fadeDelay, fadeDelaySeconds = 1;
71 	boolean cancelTouchFocus = true;
72 
73 	boolean flickScroll = true;
74 	float velocityX, velocityY;
75 	float flingTimer;
76 	private boolean overscrollX = true, overscrollY = true;
77 	float flingTime = 1f;
78 	private float overscrollDistance = 50, overscrollSpeedMin = 30, overscrollSpeedMax = 200;
79 	private boolean forceScrollX, forceScrollY;
80 	private boolean disableX, disableY;
81 	private boolean clamp = true;
82 	private boolean scrollbarsOnTop;
83 	private boolean variableSizeKnobs = true;
84 	int draggingPointer = -1;
85 
86 	/** @param widget May be null. */
ScrollPane(Actor widget)87 	public ScrollPane (Actor widget) {
88 		this(widget, new ScrollPaneStyle());
89 	}
90 
91 	/** @param widget May be null. */
ScrollPane(Actor widget, Skin skin)92 	public ScrollPane (Actor widget, Skin skin) {
93 		this(widget, skin.get(ScrollPaneStyle.class));
94 	}
95 
96 	/** @param widget May be null. */
ScrollPane(Actor widget, Skin skin, String styleName)97 	public ScrollPane (Actor widget, Skin skin, String styleName) {
98 		this(widget, skin.get(styleName, ScrollPaneStyle.class));
99 	}
100 
101 	/** @param widget May be null. */
ScrollPane(Actor widget, ScrollPaneStyle style)102 	public ScrollPane (Actor widget, ScrollPaneStyle style) {
103 		if (style == null) throw new IllegalArgumentException("style cannot be null.");
104 		this.style = style;
105 		setWidget(widget);
106 		setSize(150, 150);
107 
108 		addCaptureListener(new InputListener() {
109 			private float handlePosition;
110 
111 			public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
112 				if (draggingPointer != -1) return false;
113 				if (pointer == 0 && button != 0) return false;
114 				getStage().setScrollFocus(ScrollPane.this);
115 
116 				if (!flickScroll) resetFade();
117 
118 				if (fadeAlpha == 0) return false;
119 
120 				if (scrollX && hScrollBounds.contains(x, y)) {
121 					event.stop();
122 					resetFade();
123 					if (hKnobBounds.contains(x, y)) {
124 						lastPoint.set(x, y);
125 						handlePosition = hKnobBounds.x;
126 						touchScrollH = true;
127 						draggingPointer = pointer;
128 						return true;
129 					}
130 					setScrollX(amountX + areaWidth * (x < hKnobBounds.x ? -1 : 1));
131 					return true;
132 				}
133 				if (scrollY && vScrollBounds.contains(x, y)) {
134 					event.stop();
135 					resetFade();
136 					if (vKnobBounds.contains(x, y)) {
137 						lastPoint.set(x, y);
138 						handlePosition = vKnobBounds.y;
139 						touchScrollV = true;
140 						draggingPointer = pointer;
141 						return true;
142 					}
143 					setScrollY(amountY + areaHeight * (y < vKnobBounds.y ? 1 : -1));
144 					return true;
145 				}
146 				return false;
147 			}
148 
149 			public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
150 				if (pointer != draggingPointer) return;
151 				cancel();
152 			}
153 
154 			public void touchDragged (InputEvent event, float x, float y, int pointer) {
155 				if (pointer != draggingPointer) return;
156 				if (touchScrollH) {
157 					float delta = x - lastPoint.x;
158 					float scrollH = handlePosition + delta;
159 					handlePosition = scrollH;
160 					scrollH = Math.max(hScrollBounds.x, scrollH);
161 					scrollH = Math.min(hScrollBounds.x + hScrollBounds.width - hKnobBounds.width, scrollH);
162 					float total = hScrollBounds.width - hKnobBounds.width;
163 					if (total != 0) setScrollPercentX((scrollH - hScrollBounds.x) / total);
164 					lastPoint.set(x, y);
165 				} else if (touchScrollV) {
166 					float delta = y - lastPoint.y;
167 					float scrollV = handlePosition + delta;
168 					handlePosition = scrollV;
169 					scrollV = Math.max(vScrollBounds.y, scrollV);
170 					scrollV = Math.min(vScrollBounds.y + vScrollBounds.height - vKnobBounds.height, scrollV);
171 					float total = vScrollBounds.height - vKnobBounds.height;
172 					if (total != 0) setScrollPercentY(1 - ((scrollV - vScrollBounds.y) / total));
173 					lastPoint.set(x, y);
174 				}
175 			}
176 
177 			public boolean mouseMoved (InputEvent event, float x, float y) {
178 				if (!flickScroll) resetFade();
179 				return false;
180 			}
181 		});
182 
183 		flickScrollListener = new ActorGestureListener() {
184 			public void pan (InputEvent event, float x, float y, float deltaX, float deltaY) {
185 				resetFade();
186 				amountX -= deltaX;
187 				amountY += deltaY;
188 				clamp();
189 				if (cancelTouchFocus) cancelTouchFocus();
190 			}
191 
192 			public void fling (InputEvent event, float x, float y, int button) {
193 				if (Math.abs(x) > 150) {
194 					flingTimer = flingTime;
195 					velocityX = x;
196 					if (cancelTouchFocus) cancelTouchFocus();
197 				}
198 				if (Math.abs(y) > 150) {
199 					flingTimer = flingTime;
200 					velocityY = -y;
201 					if (cancelTouchFocus) cancelTouchFocus();
202 				}
203 			}
204 
205 			public boolean handle (Event event) {
206 				if (super.handle(event)) {
207 					if (((InputEvent)event).getType() == InputEvent.Type.touchDown) flingTimer = 0;
208 					return true;
209 				}
210 				return false;
211 			}
212 		};
213 		addListener(flickScrollListener);
214 
215 		addListener(new InputListener() {
216 			public boolean scrolled (InputEvent event, float x, float y, int amount) {
217 				resetFade();
218 				if (scrollY)
219 					setScrollY(amountY + getMouseWheelY() * amount);
220 				else if (scrollX) //
221 					setScrollX(amountX + getMouseWheelX() * amount);
222 				else
223 					return false;
224 				return true;
225 			}
226 		});
227 	}
228 
resetFade()229 	void resetFade () {
230 		fadeAlpha = fadeAlphaSeconds;
231 		fadeDelay = fadeDelaySeconds;
232 	}
233 
234 	/** Cancels the stage's touch focus for all listeners except this scroll pane's flick scroll listener. This causes any widgets
235 	 * inside the scrollpane that have received touchDown to receive touchUp.
236 	 * @see #setCancelTouchFocus(boolean) */
cancelTouchFocus()237 	public void cancelTouchFocus () {
238 		Stage stage = getStage();
239 		if (stage != null) stage.cancelTouchFocusExcept(flickScrollListener, this);
240 	}
241 
242 	/** If currently scrolling by tracking a touch down, stop scrolling. */
cancel()243 	public void cancel () {
244 		draggingPointer = -1;
245 		touchScrollH = false;
246 		touchScrollV = false;
247 		flickScrollListener.getGestureDetector().cancel();
248 	}
249 
clamp()250 	void clamp () {
251 		if (!clamp) return;
252 		scrollX(overscrollX ? MathUtils.clamp(amountX, -overscrollDistance, maxX + overscrollDistance)
253 			: MathUtils.clamp(amountX, 0, maxX));
254 		scrollY(overscrollY ? MathUtils.clamp(amountY, -overscrollDistance, maxY + overscrollDistance)
255 			: MathUtils.clamp(amountY, 0, maxY));
256 	}
257 
setStyle(ScrollPaneStyle style)258 	public void setStyle (ScrollPaneStyle style) {
259 		if (style == null) throw new IllegalArgumentException("style cannot be null.");
260 		this.style = style;
261 		invalidateHierarchy();
262 	}
263 
264 	/** Returns the scroll pane's style. Modifying the returned style may not have an effect until
265 	 * {@link #setStyle(ScrollPaneStyle)} is called. */
getStyle()266 	public ScrollPaneStyle getStyle () {
267 		return style;
268 	}
269 
act(float delta)270 	public void act (float delta) {
271 		super.act(delta);
272 
273 		boolean panning = flickScrollListener.getGestureDetector().isPanning();
274 		boolean animating = false;
275 
276 		if (fadeAlpha > 0 && fadeScrollBars && !panning && !touchScrollH && !touchScrollV) {
277 			fadeDelay -= delta;
278 			if (fadeDelay <= 0) fadeAlpha = Math.max(0, fadeAlpha - delta);
279 			animating = true;
280 		}
281 
282 		if (flingTimer > 0) {
283 			resetFade();
284 
285 			float alpha = flingTimer / flingTime;
286 			amountX -= velocityX * alpha * delta;
287 			amountY -= velocityY * alpha * delta;
288 			clamp();
289 
290 			// Stop fling if hit overscroll distance.
291 			if (amountX == -overscrollDistance) velocityX = 0;
292 			if (amountX >= maxX + overscrollDistance) velocityX = 0;
293 			if (amountY == -overscrollDistance) velocityY = 0;
294 			if (amountY >= maxY + overscrollDistance) velocityY = 0;
295 
296 			flingTimer -= delta;
297 			if (flingTimer <= 0) {
298 				velocityX = 0;
299 				velocityY = 0;
300 			}
301 
302 			animating = true;
303 		}
304 
305 		if (smoothScrolling && flingTimer <= 0 && !panning && //
306 		// Scroll smoothly when grabbing the scrollbar if one pixel of scrollbar movement is > 20% of the scroll area.
307 		((!touchScrollH || (scrollX && maxX / (hScrollBounds.width - hKnobBounds.width) > areaWidth * 0.1f)) //
308 			&& (!touchScrollV || (scrollY && maxY / (vScrollBounds.height - vKnobBounds.height) > areaHeight * 0.1f))) //
309 		) {
310 			if (visualAmountX != amountX) {
311 				if (visualAmountX < amountX)
312 					visualScrollX(Math.min(amountX, visualAmountX + Math.max(200 * delta, (amountX - visualAmountX) * 7 * delta)));
313 				else
314 					visualScrollX(Math.max(amountX, visualAmountX - Math.max(200 * delta, (visualAmountX - amountX) * 7 * delta)));
315 				animating = true;
316 			}
317 			if (visualAmountY != amountY) {
318 				if (visualAmountY < amountY)
319 					visualScrollY(Math.min(amountY, visualAmountY + Math.max(200 * delta, (amountY - visualAmountY) * 7 * delta)));
320 				else
321 					visualScrollY(Math.max(amountY, visualAmountY - Math.max(200 * delta, (visualAmountY - amountY) * 7 * delta)));
322 				animating = true;
323 			}
324 		} else {
325 			if (visualAmountX != amountX) visualScrollX(amountX);
326 			if (visualAmountY != amountY) visualScrollY(amountY);
327 		}
328 
329 		if (!panning) {
330 			if (overscrollX && scrollX) {
331 				if (amountX < 0) {
332 					resetFade();
333 					amountX += (overscrollSpeedMin + (overscrollSpeedMax - overscrollSpeedMin) * -amountX / overscrollDistance)
334 						* delta;
335 					if (amountX > 0) scrollX(0);
336 					animating = true;
337 				} else if (amountX > maxX) {
338 					resetFade();
339 					amountX -= (overscrollSpeedMin
340 						+ (overscrollSpeedMax - overscrollSpeedMin) * -(maxX - amountX) / overscrollDistance) * delta;
341 					if (amountX < maxX) scrollX(maxX);
342 					animating = true;
343 				}
344 			}
345 			if (overscrollY && scrollY) {
346 				if (amountY < 0) {
347 					resetFade();
348 					amountY += (overscrollSpeedMin + (overscrollSpeedMax - overscrollSpeedMin) * -amountY / overscrollDistance)
349 						* delta;
350 					if (amountY > 0) scrollY(0);
351 					animating = true;
352 				} else if (amountY > maxY) {
353 					resetFade();
354 					amountY -= (overscrollSpeedMin
355 						+ (overscrollSpeedMax - overscrollSpeedMin) * -(maxY - amountY) / overscrollDistance) * delta;
356 					if (amountY < maxY) scrollY(maxY);
357 					animating = true;
358 				}
359 			}
360 		}
361 
362 		if (animating) {
363 			Stage stage = getStage();
364 			if (stage != null && stage.getActionsRequestRendering()) Gdx.graphics.requestRendering();
365 		}
366 	}
367 
layout()368 	public void layout () {
369 		final Drawable bg = style.background;
370 		final Drawable hScrollKnob = style.hScrollKnob;
371 		final Drawable vScrollKnob = style.vScrollKnob;
372 
373 		float bgLeftWidth = 0, bgRightWidth = 0, bgTopHeight = 0, bgBottomHeight = 0;
374 		if (bg != null) {
375 			bgLeftWidth = bg.getLeftWidth();
376 			bgRightWidth = bg.getRightWidth();
377 			bgTopHeight = bg.getTopHeight();
378 			bgBottomHeight = bg.getBottomHeight();
379 		}
380 
381 		float width = getWidth();
382 		float height = getHeight();
383 
384 		float scrollbarHeight = 0;
385 		if (hScrollKnob != null) scrollbarHeight = hScrollKnob.getMinHeight();
386 		if (style.hScroll != null) scrollbarHeight = Math.max(scrollbarHeight, style.hScroll.getMinHeight());
387 		float scrollbarWidth = 0;
388 		if (vScrollKnob != null) scrollbarWidth = vScrollKnob.getMinWidth();
389 		if (style.vScroll != null) scrollbarWidth = Math.max(scrollbarWidth, style.vScroll.getMinWidth());
390 
391 		// Get available space size by subtracting background's padded area.
392 		areaWidth = width - bgLeftWidth - bgRightWidth;
393 		areaHeight = height - bgTopHeight - bgBottomHeight;
394 
395 		if (widget == null) return;
396 
397 		// Get widget's desired width.
398 		float widgetWidth, widgetHeight;
399 		if (widget instanceof Layout) {
400 			Layout layout = (Layout)widget;
401 			widgetWidth = layout.getPrefWidth();
402 			widgetHeight = layout.getPrefHeight();
403 		} else {
404 			widgetWidth = widget.getWidth();
405 			widgetHeight = widget.getHeight();
406 		}
407 
408 		// Determine if horizontal/vertical scrollbars are needed.
409 		scrollX = forceScrollX || (widgetWidth > areaWidth && !disableX);
410 		scrollY = forceScrollY || (widgetHeight > areaHeight && !disableY);
411 
412 		boolean fade = fadeScrollBars;
413 		if (!fade) {
414 			// Check again, now taking into account the area that's taken up by any enabled scrollbars.
415 			if (scrollY) {
416 				areaWidth -= scrollbarWidth;
417 				if (!scrollX && widgetWidth > areaWidth && !disableX) scrollX = true;
418 			}
419 			if (scrollX) {
420 				areaHeight -= scrollbarHeight;
421 				if (!scrollY && widgetHeight > areaHeight && !disableY) {
422 					scrollY = true;
423 					areaWidth -= scrollbarWidth;
424 				}
425 			}
426 		}
427 
428 		// The bounds of the scrollable area for the widget.
429 		widgetAreaBounds.set(bgLeftWidth, bgBottomHeight, areaWidth, areaHeight);
430 
431 		if (fade) {
432 			// Make sure widget is drawn under fading scrollbars.
433 			if (scrollX && scrollY) {
434 				areaHeight -= scrollbarHeight;
435 				areaWidth -= scrollbarWidth;
436 			}
437 		} else {
438 			if (scrollbarsOnTop) {
439 				// Make sure widget is drawn under non-fading scrollbars.
440 				if (scrollX) widgetAreaBounds.height += scrollbarHeight;
441 				if (scrollY) widgetAreaBounds.width += scrollbarWidth;
442 			} else {
443 				// Offset widget area y for horizontal scrollbar at bottom.
444 				if (scrollX && hScrollOnBottom) widgetAreaBounds.y += scrollbarHeight;
445 				// Offset widget area x for vertical scrollbar at left.
446 				if (scrollY && !vScrollOnRight) widgetAreaBounds.x += scrollbarWidth;
447 			}
448 		}
449 
450 		// If the widget is smaller than the available space, make it take up the available space.
451 		widgetWidth = disableX ? areaWidth : Math.max(areaWidth, widgetWidth);
452 		widgetHeight = disableY ? areaHeight : Math.max(areaHeight, widgetHeight);
453 
454 		maxX = widgetWidth - areaWidth;
455 		maxY = widgetHeight - areaHeight;
456 		if (fade) {
457 			// Make sure widget is drawn under fading scrollbars.
458 			if (scrollX && scrollY) {
459 				maxY -= scrollbarHeight;
460 				maxX -= scrollbarWidth;
461 			}
462 		}
463 		scrollX(MathUtils.clamp(amountX, 0, maxX));
464 		scrollY(MathUtils.clamp(amountY, 0, maxY));
465 
466 		// Set the bounds and scroll knob sizes if scrollbars are needed.
467 		if (scrollX) {
468 			if (hScrollKnob != null) {
469 				float hScrollHeight = style.hScroll != null ? style.hScroll.getMinHeight() : hScrollKnob.getMinHeight();
470 				// The corner gap where the two scroll bars intersect might have to flip from right to left.
471 				float boundsX = vScrollOnRight ? bgLeftWidth : bgLeftWidth + scrollbarWidth;
472 				// Scrollbar on the top or bottom.
473 				float boundsY = hScrollOnBottom ? bgBottomHeight : height - bgTopHeight - hScrollHeight;
474 				hScrollBounds.set(boundsX, boundsY, areaWidth, hScrollHeight);
475 				if (variableSizeKnobs)
476 					hKnobBounds.width = Math.max(hScrollKnob.getMinWidth(), (int)(hScrollBounds.width * areaWidth / widgetWidth));
477 				else
478 					hKnobBounds.width = hScrollKnob.getMinWidth();
479 
480 				hKnobBounds.height = hScrollKnob.getMinHeight();
481 
482 				hKnobBounds.x = hScrollBounds.x + (int)((hScrollBounds.width - hKnobBounds.width) * getScrollPercentX());
483 				hKnobBounds.y = hScrollBounds.y;
484 			} else {
485 				hScrollBounds.set(0, 0, 0, 0);
486 				hKnobBounds.set(0, 0, 0, 0);
487 			}
488 		}
489 		if (scrollY) {
490 			if (vScrollKnob != null) {
491 				float vScrollWidth = style.vScroll != null ? style.vScroll.getMinWidth() : vScrollKnob.getMinWidth();
492 				// the small gap where the two scroll bars intersect might have to flip from bottom to top
493 				float boundsX, boundsY;
494 				if (hScrollOnBottom) {
495 					boundsY = height - bgTopHeight - areaHeight;
496 				} else {
497 					boundsY = bgBottomHeight;
498 				}
499 				// bar on the left or right
500 				if (vScrollOnRight) {
501 					boundsX = width - bgRightWidth - vScrollWidth;
502 				} else {
503 					boundsX = bgLeftWidth;
504 				}
505 				vScrollBounds.set(boundsX, boundsY, vScrollWidth, areaHeight);
506 				vKnobBounds.width = vScrollKnob.getMinWidth();
507 				if (variableSizeKnobs)
508 					vKnobBounds.height = Math.max(vScrollKnob.getMinHeight(), (int)(vScrollBounds.height * areaHeight / widgetHeight));
509 				else
510 					vKnobBounds.height = vScrollKnob.getMinHeight();
511 
512 				if (vScrollOnRight) {
513 					vKnobBounds.x = width - bgRightWidth - vScrollKnob.getMinWidth();
514 				} else {
515 					vKnobBounds.x = bgLeftWidth;
516 				}
517 				vKnobBounds.y = vScrollBounds.y + (int)((vScrollBounds.height - vKnobBounds.height) * (1 - getScrollPercentY()));
518 			} else {
519 				vScrollBounds.set(0, 0, 0, 0);
520 				vKnobBounds.set(0, 0, 0, 0);
521 			}
522 		}
523 
524 		widget.setSize(widgetWidth, widgetHeight);
525 		if (widget instanceof Layout) ((Layout)widget).validate();
526 	}
527 
528 	@Override
draw(Batch batch, float parentAlpha)529 	public void draw (Batch batch, float parentAlpha) {
530 		if (widget == null) return;
531 
532 		validate();
533 
534 		// Setup transform for this group.
535 		applyTransform(batch, computeTransform());
536 
537 		if (scrollX) hKnobBounds.x = hScrollBounds.x + (int)((hScrollBounds.width - hKnobBounds.width) * getVisualScrollPercentX());
538 		if (scrollY)
539 			vKnobBounds.y = vScrollBounds.y + (int)((vScrollBounds.height - vKnobBounds.height) * (1 - getVisualScrollPercentY()));
540 
541 		// Calculate the widget's position depending on the scroll state and available widget area.
542 		float y = widgetAreaBounds.y;
543 		if (!scrollY)
544 			y -= (int)maxY;
545 		else
546 			y -= (int)(maxY - visualAmountY);
547 
548 		float x = widgetAreaBounds.x;
549 		if (scrollX) x -= (int)visualAmountX;
550 
551 		if (!fadeScrollBars && scrollbarsOnTop) {
552 			if (scrollX && hScrollOnBottom) {
553 				float scrollbarHeight = 0;
554 				if (style.hScrollKnob != null) scrollbarHeight = style.hScrollKnob.getMinHeight();
555 				if (style.hScroll != null) scrollbarHeight = Math.max(scrollbarHeight, style.hScroll.getMinHeight());
556 				y += scrollbarHeight;
557 			}
558 			if (scrollY && !vScrollOnRight) {
559 				float scrollbarWidth = 0;
560 				if (style.hScrollKnob != null) scrollbarWidth = style.hScrollKnob.getMinWidth();
561 				if (style.hScroll != null) scrollbarWidth = Math.max(scrollbarWidth, style.hScroll.getMinWidth());
562 				x += scrollbarWidth;
563 			}
564 		}
565 
566 		widget.setPosition(x, y);
567 
568 		if (widget instanceof Cullable) {
569 			widgetCullingArea.x = -widget.getX() + widgetAreaBounds.x;
570 			widgetCullingArea.y = -widget.getY() + widgetAreaBounds.y;
571 			widgetCullingArea.width = widgetAreaBounds.width;
572 			widgetCullingArea.height = widgetAreaBounds.height;
573 			((Cullable)widget).setCullingArea(widgetCullingArea);
574 		}
575 
576 		// Draw the background ninepatch.
577 		Color color = getColor();
578 		batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
579 		if (style.background != null) style.background.draw(batch, 0, 0, getWidth(), getHeight());
580 
581 		// Caculate the scissor bounds based on the batch transform, the available widget area and the camera transform. We need to
582 		// project those to screen coordinates for OpenGL ES to consume.
583 		getStage().calculateScissors(widgetAreaBounds, scissorBounds);
584 
585 		// Enable scissors for widget area and draw the widget.
586 		batch.flush();
587 		if (ScissorStack.pushScissors(scissorBounds)) {
588 			drawChildren(batch, parentAlpha);
589 			batch.flush();
590 			ScissorStack.popScissors();
591 		}
592 
593 		// Render scrollbars and knobs on top.
594 		batch.setColor(color.r, color.g, color.b, color.a * parentAlpha * Interpolation.fade.apply(fadeAlpha / fadeAlphaSeconds));
595 		if (scrollX && scrollY) {
596 			if (style.corner != null) {
597 				style.corner.draw(batch, hScrollBounds.x + hScrollBounds.width, hScrollBounds.y, vScrollBounds.width,
598 					vScrollBounds.y);
599 			}
600 		}
601 		if (scrollX) {
602 			if (style.hScroll != null)
603 				style.hScroll.draw(batch, hScrollBounds.x, hScrollBounds.y, hScrollBounds.width, hScrollBounds.height);
604 			if (style.hScrollKnob != null)
605 				style.hScrollKnob.draw(batch, hKnobBounds.x, hKnobBounds.y, hKnobBounds.width, hKnobBounds.height);
606 		}
607 		if (scrollY) {
608 			if (style.vScroll != null)
609 				style.vScroll.draw(batch, vScrollBounds.x, vScrollBounds.y, vScrollBounds.width, vScrollBounds.height);
610 			if (style.vScrollKnob != null)
611 				style.vScrollKnob.draw(batch, vKnobBounds.x, vKnobBounds.y, vKnobBounds.width, vKnobBounds.height);
612 		}
613 
614 		resetTransform(batch);
615 	}
616 
617 	/** Generate fling gesture.
618 	 * @param flingTime Time in seconds for which you want to fling last.
619 	 * @param velocityX Velocity for horizontal direction.
620 	 * @param velocityY Velocity for vertical direction. */
fling(float flingTime, float velocityX, float velocityY)621 	public void fling (float flingTime, float velocityX, float velocityY) {
622 		this.flingTimer = flingTime;
623 		this.velocityX = velocityX;
624 		this.velocityY = velocityY;
625 	}
626 
getPrefWidth()627 	public float getPrefWidth () {
628 		if (widget instanceof Layout) {
629 			float width = ((Layout)widget).getPrefWidth();
630 			if (style.background != null) width += style.background.getLeftWidth() + style.background.getRightWidth();
631 			if (forceScrollY) {
632 				float scrollbarWidth = 0;
633 				if (style.vScrollKnob != null) scrollbarWidth = style.vScrollKnob.getMinWidth();
634 				if (style.vScroll != null) scrollbarWidth = Math.max(scrollbarWidth, style.vScroll.getMinWidth());
635 				width += scrollbarWidth;
636 			}
637 			return width;
638 		}
639 		return 150;
640 	}
641 
getPrefHeight()642 	public float getPrefHeight () {
643 		if (widget instanceof Layout) {
644 			float height = ((Layout)widget).getPrefHeight();
645 			if (style.background != null) height += style.background.getTopHeight() + style.background.getBottomHeight();
646 			if (forceScrollX) {
647 				float scrollbarHeight = 0;
648 				if (style.hScrollKnob != null) scrollbarHeight = style.hScrollKnob.getMinHeight();
649 				if (style.hScroll != null) scrollbarHeight = Math.max(scrollbarHeight, style.hScroll.getMinHeight());
650 				height += scrollbarHeight;
651 			}
652 			return height;
653 		}
654 		return 150;
655 	}
656 
getMinWidth()657 	public float getMinWidth () {
658 		return 0;
659 	}
660 
getMinHeight()661 	public float getMinHeight () {
662 		return 0;
663 	}
664 
665 	/** Sets the {@link Actor} embedded in this scroll pane.
666 	 * @param widget May be null to remove any current actor. */
setWidget(Actor widget)667 	public void setWidget (Actor widget) {
668 		if (widget == this) throw new IllegalArgumentException("widget cannot be the ScrollPane.");
669 		if (this.widget != null) super.removeActor(this.widget);
670 		this.widget = widget;
671 		if (widget != null) super.addActor(widget);
672 	}
673 
674 	/** Returns the actor embedded in this scroll pane, or null. */
getWidget()675 	public Actor getWidget () {
676 		return widget;
677 	}
678 
679 	/** @deprecated ScrollPane may have only a single child.
680 	 * @see #setWidget(Actor) */
addActor(Actor actor)681 	public void addActor (Actor actor) {
682 		throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
683 	}
684 
685 	/** @deprecated ScrollPane may have only a single child.
686 	 * @see #setWidget(Actor) */
addActorAt(int index, Actor actor)687 	public void addActorAt (int index, Actor actor) {
688 		throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
689 	}
690 
691 	/** @deprecated ScrollPane may have only a single child.
692 	 * @see #setWidget(Actor) */
addActorBefore(Actor actorBefore, Actor actor)693 	public void addActorBefore (Actor actorBefore, Actor actor) {
694 		throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
695 	}
696 
697 	/** @deprecated ScrollPane may have only a single child.
698 	 * @see #setWidget(Actor) */
addActorAfter(Actor actorAfter, Actor actor)699 	public void addActorAfter (Actor actorAfter, Actor actor) {
700 		throw new UnsupportedOperationException("Use ScrollPane#setWidget.");
701 	}
702 
removeActor(Actor actor)703 	public boolean removeActor (Actor actor) {
704 		if (actor != widget) return false;
705 		setWidget(null);
706 		return true;
707 	}
708 
hit(float x, float y, boolean touchable)709 	public Actor hit (float x, float y, boolean touchable) {
710 		if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return null;
711 		if (scrollX && hScrollBounds.contains(x, y)) return this;
712 		if (scrollY && vScrollBounds.contains(x, y)) return this;
713 		return super.hit(x, y, touchable);
714 	}
715 
716 	/** Called whenever the x scroll amount is changed. */
scrollX(float pixelsX)717 	protected void scrollX (float pixelsX) {
718 		this.amountX = pixelsX;
719 	}
720 
721 	/** Called whenever the y scroll amount is changed. */
scrollY(float pixelsY)722 	protected void scrollY (float pixelsY) {
723 		this.amountY = pixelsY;
724 	}
725 
726 	/** Called whenever the visual x scroll amount is changed. */
visualScrollX(float pixelsX)727 	protected void visualScrollX (float pixelsX) {
728 		this.visualAmountX = pixelsX;
729 	}
730 
731 	/** Called whenever the visual y scroll amount is changed. */
visualScrollY(float pixelsY)732 	protected void visualScrollY (float pixelsY) {
733 		this.visualAmountY = pixelsY;
734 	}
735 
736 	/** Returns the amount to scroll horizontally when the mouse wheel is scrolled. */
getMouseWheelX()737 	protected float getMouseWheelX () {
738 		return Math.min(areaWidth, Math.max(areaWidth * 0.9f, maxX * 0.1f) / 4);
739 	}
740 
741 	/** Returns the amount to scroll vertically when the mouse wheel is scrolled. */
getMouseWheelY()742 	protected float getMouseWheelY () {
743 		return Math.min(areaHeight, Math.max(areaHeight * 0.9f, maxY * 0.1f) / 4);
744 	}
745 
setScrollX(float pixels)746 	public void setScrollX (float pixels) {
747 		scrollX(MathUtils.clamp(pixels, 0, maxX));
748 	}
749 
750 	/** Returns the x scroll position in pixels, where 0 is the left of the scroll pane. */
getScrollX()751 	public float getScrollX () {
752 		return amountX;
753 	}
754 
setScrollY(float pixels)755 	public void setScrollY (float pixels) {
756 		scrollY(MathUtils.clamp(pixels, 0, maxY));
757 	}
758 
759 	/** Returns the y scroll position in pixels, where 0 is the top of the scroll pane. */
getScrollY()760 	public float getScrollY () {
761 		return amountY;
762 	}
763 
764 	/** Sets the visual scroll amount equal to the scroll amount. This can be used when setting the scroll amount without
765 	 * animating. */
updateVisualScroll()766 	public void updateVisualScroll () {
767 		visualAmountX = amountX;
768 		visualAmountY = amountY;
769 	}
770 
getVisualScrollX()771 	public float getVisualScrollX () {
772 		return !scrollX ? 0 : visualAmountX;
773 	}
774 
getVisualScrollY()775 	public float getVisualScrollY () {
776 		return !scrollY ? 0 : visualAmountY;
777 	}
778 
getVisualScrollPercentX()779 	public float getVisualScrollPercentX () {
780 		return MathUtils.clamp(visualAmountX / maxX, 0, 1);
781 	}
782 
getVisualScrollPercentY()783 	public float getVisualScrollPercentY () {
784 		return MathUtils.clamp(visualAmountY / maxY, 0, 1);
785 	}
786 
getScrollPercentX()787 	public float getScrollPercentX () {
788 		return MathUtils.clamp(amountX / maxX, 0, 1);
789 	}
790 
setScrollPercentX(float percentX)791 	public void setScrollPercentX (float percentX) {
792 		scrollX(maxX * MathUtils.clamp(percentX, 0, 1));
793 	}
794 
getScrollPercentY()795 	public float getScrollPercentY () {
796 		return MathUtils.clamp(amountY / maxY, 0, 1);
797 	}
798 
setScrollPercentY(float percentY)799 	public void setScrollPercentY (float percentY) {
800 		scrollY(maxY * MathUtils.clamp(percentY, 0, 1));
801 	}
802 
setFlickScroll(boolean flickScroll)803 	public void setFlickScroll (boolean flickScroll) {
804 		if (this.flickScroll == flickScroll) return;
805 		this.flickScroll = flickScroll;
806 		if (flickScroll)
807 			addListener(flickScrollListener);
808 		else
809 			removeListener(flickScrollListener);
810 		invalidate();
811 	}
812 
setFlickScrollTapSquareSize(float halfTapSquareSize)813 	public void setFlickScrollTapSquareSize (float halfTapSquareSize) {
814 		flickScrollListener.getGestureDetector().setTapSquareSize(halfTapSquareSize);
815 	}
816 
817 	/** Sets the scroll offset so the specified rectangle is fully in view, if possible. Coordinates are in the scroll pane
818 	 * widget's coordinate system. */
scrollTo(float x, float y, float width, float height)819 	public void scrollTo (float x, float y, float width, float height) {
820 		scrollTo(x, y, width, height, false, false);
821 	}
822 
823 	/** Sets the scroll offset so the specified rectangle is fully in view, and optionally centered vertically and/or horizontally,
824 	 * if possible. Coordinates are in the scroll pane widget's coordinate system. */
scrollTo(float x, float y, float width, float height, boolean centerHorizontal, boolean centerVertical)825 	public void scrollTo (float x, float y, float width, float height, boolean centerHorizontal, boolean centerVertical) {
826 		float amountX = this.amountX;
827 		if (centerHorizontal) {
828 			amountX = x - areaWidth / 2 + width / 2;
829 		} else {
830 			if (x + width > amountX + areaWidth) amountX = x + width - areaWidth;
831 			if (x < amountX) amountX = x;
832 		}
833 		scrollX(MathUtils.clamp(amountX, 0, maxX));
834 
835 		float amountY = this.amountY;
836 		if (centerVertical) {
837 			amountY = maxY - y + areaHeight / 2 - height / 2;
838 		} else {
839 			if (amountY > maxY - y - height + areaHeight) amountY = maxY - y - height + areaHeight;
840 			if (amountY < maxY - y) amountY = maxY - y;
841 		}
842 		scrollY(MathUtils.clamp(amountY, 0, maxY));
843 	}
844 
845 	/** Returns the maximum scroll value in the x direction. */
getMaxX()846 	public float getMaxX () {
847 		return maxX;
848 	}
849 
850 	/** Returns the maximum scroll value in the y direction. */
getMaxY()851 	public float getMaxY () {
852 		return maxY;
853 	}
854 
getScrollBarHeight()855 	public float getScrollBarHeight () {
856 		if (!scrollX) return 0;
857 		float height = 0;
858 		if (style.hScrollKnob != null) height = style.hScrollKnob.getMinHeight();
859 		if (style.hScroll != null) height = Math.max(height, style.hScroll.getMinHeight());
860 		return height;
861 	}
862 
getScrollBarWidth()863 	public float getScrollBarWidth () {
864 		if (!scrollY) return 0;
865 		float width = 0;
866 		if (style.vScrollKnob != null) width = style.vScrollKnob.getMinWidth();
867 		if (style.vScroll != null) width = Math.max(width, style.vScroll.getMinWidth());
868 		return width;
869 	}
870 
871 	/** Returns the width of the scrolled viewport. */
getScrollWidth()872 	public float getScrollWidth () {
873 		return areaWidth;
874 	}
875 
876 	/** Returns the height of the scrolled viewport. */
getScrollHeight()877 	public float getScrollHeight () {
878 		return areaHeight;
879 	}
880 
881 	/** Returns true if the widget is larger than the scroll pane horizontally. */
isScrollX()882 	public boolean isScrollX () {
883 		return scrollX;
884 	}
885 
886 	/** Returns true if the widget is larger than the scroll pane vertically. */
isScrollY()887 	public boolean isScrollY () {
888 		return scrollY;
889 	}
890 
891 	/** Disables scrolling in a direction. The widget will be sized to the FlickScrollPane in the disabled direction. */
setScrollingDisabled(boolean x, boolean y)892 	public void setScrollingDisabled (boolean x, boolean y) {
893 		disableX = x;
894 		disableY = y;
895 	}
896 
isScrollingDisabledX()897 	public boolean isScrollingDisabledX () {
898 		return disableX;
899 	}
900 
isScrollingDisabledY()901 	public boolean isScrollingDisabledY () {
902 		return disableY;
903 	}
904 
isLeftEdge()905 	public boolean isLeftEdge () {
906 		return !scrollX || amountX <= 0;
907 	}
908 
isRightEdge()909 	public boolean isRightEdge () {
910 		return !scrollX || amountX >= maxX;
911 	}
912 
isTopEdge()913 	public boolean isTopEdge () {
914 		return !scrollY || amountY <= 0;
915 	}
916 
isBottomEdge()917 	public boolean isBottomEdge () {
918 		return !scrollY || amountY >= maxY;
919 	}
920 
isDragging()921 	public boolean isDragging () {
922 		return draggingPointer != -1;
923 	}
924 
isPanning()925 	public boolean isPanning () {
926 		return flickScrollListener.getGestureDetector().isPanning();
927 	}
928 
isFlinging()929 	public boolean isFlinging () {
930 		return flingTimer > 0;
931 	}
932 
setVelocityX(float velocityX)933 	public void setVelocityX (float velocityX) {
934 		this.velocityX = velocityX;
935 	}
936 
937 	/** Gets the flick scroll x velocity. */
getVelocityX()938 	public float getVelocityX () {
939 		return velocityX;
940 	}
941 
setVelocityY(float velocityY)942 	public void setVelocityY (float velocityY) {
943 		this.velocityY = velocityY;
944 	}
945 
946 	/** Gets the flick scroll y velocity. */
getVelocityY()947 	public float getVelocityY () {
948 		return velocityY;
949 	}
950 
951 	/** For flick scroll, if true the widget can be scrolled slightly past its bounds and will animate back to its bounds when
952 	 * scrolling is stopped. Default is true. */
setOverscroll(boolean overscrollX, boolean overscrollY)953 	public void setOverscroll (boolean overscrollX, boolean overscrollY) {
954 		this.overscrollX = overscrollX;
955 		this.overscrollY = overscrollY;
956 	}
957 
958 	/** For flick scroll, sets the overscroll distance in pixels and the speed it returns to the widget's bounds in seconds.
959 	 * Default is 50, 30, 200. */
setupOverscroll(float distance, float speedMin, float speedMax)960 	public void setupOverscroll (float distance, float speedMin, float speedMax) {
961 		overscrollDistance = distance;
962 		overscrollSpeedMin = speedMin;
963 		overscrollSpeedMax = speedMax;
964 	}
965 
966 	/** Forces enabling scrollbars (for non-flick scroll) and overscrolling (for flick scroll) in a direction, even if the contents
967 	 * do not exceed the bounds in that direction. */
setForceScroll(boolean x, boolean y)968 	public void setForceScroll (boolean x, boolean y) {
969 		forceScrollX = x;
970 		forceScrollY = y;
971 	}
972 
isForceScrollX()973 	public boolean isForceScrollX () {
974 		return forceScrollX;
975 	}
976 
isForceScrollY()977 	public boolean isForceScrollY () {
978 		return forceScrollY;
979 	}
980 
981 	/** For flick scroll, sets the amount of time in seconds that a fling will continue to scroll. Default is 1. */
setFlingTime(float flingTime)982 	public void setFlingTime (float flingTime) {
983 		this.flingTime = flingTime;
984 	}
985 
986 	/** For flick scroll, prevents scrolling out of the widget's bounds. Default is true. */
setClamp(boolean clamp)987 	public void setClamp (boolean clamp) {
988 		this.clamp = clamp;
989 	}
990 
991 	/** Set the position of the vertical and horizontal scroll bars. */
setScrollBarPositions(boolean bottom, boolean right)992 	public void setScrollBarPositions (boolean bottom, boolean right) {
993 		hScrollOnBottom = bottom;
994 		vScrollOnRight = right;
995 	}
996 
997 	/** When true the scrollbars don't reduce the scrollable size and fade out after some time of not being used. */
setFadeScrollBars(boolean fadeScrollBars)998 	public void setFadeScrollBars (boolean fadeScrollBars) {
999 		if (this.fadeScrollBars == fadeScrollBars) return;
1000 		this.fadeScrollBars = fadeScrollBars;
1001 		if (!fadeScrollBars) fadeAlpha = fadeAlphaSeconds;
1002 		invalidate();
1003 	}
1004 
setupFadeScrollBars(float fadeAlphaSeconds, float fadeDelaySeconds)1005 	public void setupFadeScrollBars (float fadeAlphaSeconds, float fadeDelaySeconds) {
1006 		this.fadeAlphaSeconds = fadeAlphaSeconds;
1007 		this.fadeDelaySeconds = fadeDelaySeconds;
1008 	}
1009 
setSmoothScrolling(boolean smoothScrolling)1010 	public void setSmoothScrolling (boolean smoothScrolling) {
1011 		this.smoothScrolling = smoothScrolling;
1012 	}
1013 
1014 	/** When false (the default), the widget is clipped so it is not drawn under the scrollbars. When true, the widget is clipped
1015 	 * to the entire scroll pane bounds and the scrollbars are drawn on top of the widget. If {@link #setFadeScrollBars(boolean)}
1016 	 * is true, the scroll bars are always drawn on top. */
setScrollbarsOnTop(boolean scrollbarsOnTop)1017 	public void setScrollbarsOnTop (boolean scrollbarsOnTop) {
1018 		this.scrollbarsOnTop = scrollbarsOnTop;
1019 		invalidate();
1020 	}
1021 
getVariableSizeKnobs()1022 	public boolean getVariableSizeKnobs () {
1023 		return variableSizeKnobs;
1024 	}
1025 
1026 	/** If true, the scroll knobs are sized based on {@link #getMaxX()} or {@link #getMaxY()}. If false, the scroll knobs are sized
1027 	 * based on {@link Drawable#getMinWidth()} or {@link Drawable#getMinHeight()}. Default is true. */
setVariableSizeKnobs(boolean variableSizeKnobs)1028 	public void setVariableSizeKnobs (boolean variableSizeKnobs) {
1029 		this.variableSizeKnobs = variableSizeKnobs;
1030 	}
1031 
1032 	/** When true (default) and flick scrolling begins, {@link #cancelTouchFocus()} is called. This causes any widgets inside the
1033 	 * scrollpane that have received touchDown to receive touchUp when flick scrolling begins. */
setCancelTouchFocus(boolean cancelTouchFocus)1034 	public void setCancelTouchFocus (boolean cancelTouchFocus) {
1035 		this.cancelTouchFocus = cancelTouchFocus;
1036 	}
1037 
drawDebug(ShapeRenderer shapes)1038 	public void drawDebug (ShapeRenderer shapes) {
1039 		shapes.flush();
1040 		applyTransform(shapes, computeTransform());
1041 		if (ScissorStack.pushScissors(scissorBounds)) {
1042 			drawDebugChildren(shapes);
1043 			ScissorStack.popScissors();
1044 		}
1045 		resetTransform(shapes);
1046 	}
1047 
1048 	/** The style for a scroll pane, see {@link ScrollPane}.
1049 	 * @author mzechner
1050 	 * @author Nathan Sweet */
1051 	static public class ScrollPaneStyle {
1052 		/** Optional. */
1053 		public Drawable background, corner;
1054 		/** Optional. */
1055 		public Drawable hScroll, hScrollKnob;
1056 		/** Optional. */
1057 		public Drawable vScroll, vScrollKnob;
1058 
ScrollPaneStyle()1059 		public ScrollPaneStyle () {
1060 		}
1061 
ScrollPaneStyle(Drawable background, Drawable hScroll, Drawable hScrollKnob, Drawable vScroll, Drawable vScrollKnob)1062 		public ScrollPaneStyle (Drawable background, Drawable hScroll, Drawable hScrollKnob, Drawable vScroll,
1063 			Drawable vScrollKnob) {
1064 			this.background = background;
1065 			this.hScroll = hScroll;
1066 			this.hScrollKnob = hScrollKnob;
1067 			this.vScroll = vScroll;
1068 			this.vScrollKnob = vScrollKnob;
1069 		}
1070 
ScrollPaneStyle(ScrollPaneStyle style)1071 		public ScrollPaneStyle (ScrollPaneStyle style) {
1072 			this.background = style.background;
1073 			this.hScroll = style.hScroll;
1074 			this.hScrollKnob = style.hScrollKnob;
1075 			this.vScroll = style.vScroll;
1076 			this.vScrollKnob = style.vScrollKnob;
1077 		}
1078 	}
1079 }
1080