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