A game framework written with osu! in mind.
at master 657 lines 27 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Diagnostics; 6using osu.Framework.Caching; 7using osu.Framework.Input; 8using osu.Framework.Input.Bindings; 9using osu.Framework.Input.Events; 10using osu.Framework.Utils; 11using osuTK; 12using osuTK.Input; 13 14namespace osu.Framework.Graphics.Containers 15{ 16 public abstract class ScrollContainer<T> : Container<T>, DelayedLoadWrapper.IOnScreenOptimisingContainer, IKeyBindingHandler<PlatformAction> 17 where T : Drawable 18 { 19 /// <summary> 20 /// Determines whether the scroll dragger appears on the left side. If not, then it always appears on the right side. 21 /// </summary> 22 public Anchor ScrollbarAnchor 23 { 24 get => Scrollbar.Anchor; 25 set 26 { 27 Scrollbar.Anchor = value; 28 Scrollbar.Origin = value; 29 updatePadding(); 30 } 31 } 32 33 private bool scrollbarVisible = true; 34 35 /// <summary> 36 /// Whether the scrollbar is visible. 37 /// </summary> 38 public bool ScrollbarVisible 39 { 40 get => scrollbarVisible; 41 set 42 { 43 scrollbarVisible = value; 44 scrollbarCache.Invalidate(); 45 } 46 } 47 48 protected readonly ScrollbarContainer Scrollbar; 49 50 private bool scrollbarOverlapsContent = true; 51 52 /// <summary> 53 /// Whether the scrollbar overlaps the content or resides in its own padded space. 54 /// </summary> 55 public bool ScrollbarOverlapsContent 56 { 57 get => scrollbarOverlapsContent; 58 set 59 { 60 scrollbarOverlapsContent = value; 61 updatePadding(); 62 } 63 } 64 65 /// <summary> 66 /// Size of available content (i.e. everything that can be scrolled to) in the scroll direction. 67 /// </summary> 68 public float AvailableContent => ScrollContent.DrawSize[ScrollDim]; 69 70 /// <summary> 71 /// Size of the viewport in the scroll direction. 72 /// </summary> 73 public float DisplayableContent => ChildSize[ScrollDim]; 74 75 /// <summary> 76 /// Controls the distance scrolled per unit of mouse scroll. 77 /// </summary> 78 public float ScrollDistance = 80; 79 80 /// <summary> 81 /// This limits how far out of clamping bounds we allow the target position to be at most. 82 /// Effectively, larger values result in bouncier behavior as the scroll boundaries are approached 83 /// with high velocity. 84 /// </summary> 85 public float ClampExtension = 500; 86 87 /// <summary> 88 /// This corresponds to the clamping force. A larger value means more aggressive clamping. Default is 0.012. 89 /// </summary> 90 private const double distance_decay_clamping = 0.012; 91 92 /// <summary> 93 /// Controls the rate with which the target position is approached after ending a drag. Default is 0.0035. 94 /// </summary> 95 public double DistanceDecayDrag = 0.0035; 96 97 /// <summary> 98 /// Controls the rate with which the target position is approached after scrolling. Default is 0.01 99 /// </summary> 100 public double DistanceDecayScroll = 0.01; 101 102 /// <summary> 103 /// Controls the rate with which the target position is approached after jumping to a specific location. Default is 0.01. 104 /// </summary> 105 public double DistanceDecayJump = 0.01; 106 107 /// <summary> 108 /// Controls the rate with which the target position is approached. It is automatically set after 109 /// dragging or scrolling. 110 /// </summary> 111 private double distanceDecay; 112 113 /// <summary> 114 /// The current scroll position. 115 /// </summary> 116 public float Current { get; private set; } 117 118 /// <summary> 119 /// The target scroll position which is exponentially approached by current via a rate of distanceDecay. 120 /// </summary> 121 protected float Target { get; private set; } 122 123 /// <summary> 124 /// The maximum distance that can be scrolled in the scroll direction. 125 /// </summary> 126 public float ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0); 127 128 /// <summary> 129 /// The maximum distance that the scrollbar can move in the scroll direction. 130 /// </summary> 131 public float ScrollbarMovementExtent => Math.Max(DrawSize[ScrollDim] - Scrollbar.DrawSize[ScrollDim], 0); 132 133 /// <summary> 134 /// Clamp a value to the available scroll range. 135 /// </summary> 136 /// <param name="position">The value to clamp.</param> 137 /// <param name="extension">An extension value beyond the normal extent.</param> 138 protected float Clamp(float position, float extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension); 139 140 protected override Container<T> Content => ScrollContent; 141 142 /// <summary> 143 /// Whether we are currently scrolled as far as possible into the scroll direction. 144 /// </summary> 145 /// <param name="lenience">How close to the extent we need to be.</param> 146 public bool IsScrolledToEnd(float lenience = Precision.FLOAT_EPSILON) => Precision.AlmostBigger(Target, ScrollableExtent, lenience); 147 148 /// <summary> 149 /// The container holding all children which are getting scrolled around. 150 /// </summary> 151 public Container<T> ScrollContent { get; } 152 153 protected virtual bool IsDragging { get; private set; } 154 155 public bool IsHandlingKeyboardScrolling 156 { 157 get 158 { 159 if (IsHovered) 160 return true; 161 162 InputManager inputManager = GetContainingInputManager(); 163 return inputManager != null && ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); 164 } 165 } 166 167 /// <summary> 168 /// The direction in which scrolling is supported. 169 /// </summary> 170 protected readonly Direction ScrollDirection; 171 172 /// <summary> 173 /// The direction in which scrolling is supported, converted to an int for array index lookups. 174 /// </summary> 175 protected int ScrollDim => ScrollDirection == Direction.Horizontal ? 0 : 1; 176 177 /// <summary> 178 /// Creates a scroll container. 179 /// </summary> 180 /// <param name="scrollDirection">The direction in which should be scrolled. Can be vertical or horizontal. Default is vertical.</param> 181 protected ScrollContainer(Direction scrollDirection = Direction.Vertical) 182 { 183 ScrollDirection = scrollDirection; 184 185 Masking = true; 186 187 Axes scrollAxis = scrollDirection == Direction.Horizontal ? Axes.X : Axes.Y; 188 AddRangeInternal(new Drawable[] 189 { 190 ScrollContent = new Container<T> 191 { 192 RelativeSizeAxes = Axes.Both & ~scrollAxis, 193 AutoSizeAxes = scrollAxis, 194 }, 195 Scrollbar = CreateScrollbar(scrollDirection) 196 }); 197 198 Scrollbar.Hide(); 199 Scrollbar.Dragged = onScrollbarMovement; 200 ScrollbarAnchor = scrollDirection == Direction.Vertical ? Anchor.TopRight : Anchor.BottomLeft; 201 } 202 203 private float lastUpdateDisplayableContent = -1; 204 private float lastAvailableContent = -1; 205 206 private void updateSize() 207 { 208 // ensure we only update scrollbar when something has changed, to avoid transform helpers resetting their transform every frame. 209 // also avoids creating many needless Transforms every update frame. 210 if (lastAvailableContent != AvailableContent || lastUpdateDisplayableContent != DisplayableContent) 211 { 212 lastAvailableContent = AvailableContent; 213 lastUpdateDisplayableContent = DisplayableContent; 214 scrollbarCache.Invalidate(); 215 } 216 } 217 218 private readonly Cached scrollbarCache = new Cached(); 219 220 private void updatePadding() 221 { 222 if (scrollbarOverlapsContent || AvailableContent <= DisplayableContent) 223 ScrollContent.Padding = new MarginPadding(); 224 else 225 { 226 if (ScrollDirection == Direction.Vertical) 227 { 228 ScrollContent.Padding = ScrollbarAnchor == Anchor.TopLeft 229 ? new MarginPadding { Left = Scrollbar.Width + Scrollbar.Margin.Left } 230 : new MarginPadding { Right = Scrollbar.Width + Scrollbar.Margin.Right }; 231 } 232 else 233 { 234 ScrollContent.Padding = ScrollbarAnchor == Anchor.TopLeft 235 ? new MarginPadding { Top = Scrollbar.Height + Scrollbar.Margin.Top } 236 : new MarginPadding { Bottom = Scrollbar.Height + Scrollbar.Margin.Bottom }; 237 } 238 } 239 } 240 241 protected override bool OnDragStart(DragStartEvent e) 242 { 243 if (IsDragging || e.Button != MouseButton.Left || Content.AliveInternalChildren.Count == 0) 244 return false; 245 246 lastDragTime = Time.Current; 247 averageDragDelta = averageDragTime = 0; 248 249 IsDragging = true; 250 251 dragButtonManager = GetContainingInputManager().GetButtonEventManagerFor(e.Button); 252 253 return true; 254 } 255 256 protected override bool OnKeyDown(KeyDownEvent e) 257 { 258 if (IsHandlingKeyboardScrolling && !IsDragging) 259 { 260 switch (e.Key) 261 { 262 case Key.PageUp: 263 OnUserScroll(Target - DisplayableContent); 264 return true; 265 266 case Key.PageDown: 267 OnUserScroll(Target + DisplayableContent); 268 return true; 269 } 270 } 271 272 return base.OnKeyDown(e); 273 } 274 275 protected override bool OnMouseDown(MouseDownEvent e) 276 { 277 if (IsDragging || e.Button != MouseButton.Left) return false; 278 279 // Continue from where we currently are scrolled to. 280 Target = Current; 281 282 return true; 283 } 284 285 // We keep track of this because input events may happen at different intervals than update frames 286 // and we are interested in the time difference between drag _input_ events. 287 private double lastDragTime; 288 289 // These keep track of a sliding average (w.r.t. time) of the time between drag events 290 // and the delta of drag events. Both of these moving averages are decayed at the same 291 // rate and thus the velocity remains constant across time. The overall magnitude 292 // of averageDragTime and averageDragDelta simple decreases such that more recent movements 293 // have a larger weight. 294 private double averageDragTime; 295 private double averageDragDelta; 296 297 private MouseButtonEventManager dragButtonManager; 298 299 private bool dragBlocksClick; 300 301 public override bool DragBlocksClick => dragBlocksClick; 302 303 protected override void OnDrag(DragEvent e) 304 { 305 Trace.Assert(IsDragging, "We should never receive OnDrag if we are not dragging."); 306 307 double currentTime = Time.Current; 308 double timeDelta = currentTime - lastDragTime; 309 double decay = Math.Pow(0.95, timeDelta); 310 311 averageDragTime = averageDragTime * decay + timeDelta; 312 averageDragDelta = averageDragDelta * decay - e.Delta[ScrollDim]; 313 314 lastDragTime = currentTime; 315 316 Vector2 childDelta = ToLocalSpace(e.ScreenSpaceMousePosition) - ToLocalSpace(e.ScreenSpaceLastMousePosition); 317 318 float scrollOffset = -childDelta[ScrollDim]; 319 float clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target); 320 321 Debug.Assert(Precision.AlmostBigger(Math.Abs(scrollOffset), clampedScrollOffset * Math.Sign(scrollOffset))); 322 323 // If we are dragging past the extent of the scrollable area, half the offset 324 // such that the user can feel it. 325 scrollOffset = clampedScrollOffset + (scrollOffset - clampedScrollOffset) / 2; 326 327 // similar calculation to what is already done in MouseButtonEventManager.HandlePositionChange 328 // handles the case where a drag was triggered on an axis we are not interested in. 329 // can be removed if/when drag events are split out per axis or contain direction information. 330 dragBlocksClick |= Math.Abs(e.MouseDownPosition[ScrollDim] - e.MousePosition[ScrollDim]) > dragButtonManager.ClickDragDistance; 331 332 scrollByOffset(scrollOffset, false); 333 } 334 335 protected override void OnDragEnd(DragEndEvent e) 336 { 337 Trace.Assert(IsDragging, "We should never receive OnDragEnd if we are not dragging."); 338 339 dragBlocksClick = false; 340 dragButtonManager = null; 341 IsDragging = false; 342 343 if (averageDragTime <= 0.0) 344 return; 345 346 double velocity = averageDragDelta / averageDragTime; 347 348 // Detect whether we halted at the end of the drag and in fact should _not_ 349 // perform a flick event. 350 const double velocity_cutoff = 0.1; 351 if (Math.Abs(Math.Pow(0.95, Time.Current - lastDragTime) * velocity) < velocity_cutoff) 352 velocity = 0; 353 354 // Differentiate f(t) = distance * (1 - exp(-t)) w.r.t. "t" to obtain 355 // velocity w.r.t. time. Then rearrange to solve for distance given velocity. 356 double distance = velocity / (1 - Math.Exp(-DistanceDecayDrag)); 357 358 scrollByOffset((float)distance, true, DistanceDecayDrag); 359 } 360 361 protected override bool OnScroll(ScrollEvent e) 362 { 363 if (Content.AliveInternalChildren.Count == 0) 364 return false; 365 366 bool isPrecise = e.IsPrecise; 367 368 Vector2 scrollDelta = e.ScrollDelta; 369 float scrollDeltaFloat = scrollDelta.Y; 370 if (ScrollDirection == Direction.Horizontal && scrollDelta.X != 0) 371 scrollDeltaFloat = scrollDelta.X; 372 373 scrollByOffset((isPrecise ? 10 : ScrollDistance) * -scrollDeltaFloat, true, isPrecise ? 0.05 : DistanceDecayScroll); 374 return true; 375 } 376 377 private void onScrollbarMovement(float value) => OnUserScroll(Clamp(fromScrollbarPosition(value)), false); 378 379 /// <summary> 380 /// Immediately offsets the current and target scroll position. 381 /// </summary> 382 /// <param name="offset">The scroll offset.</param> 383 public void OffsetScrollPosition(float offset) 384 { 385 Target += offset; 386 Current += offset; 387 } 388 389 private void scrollByOffset(float value, bool animated, double distanceDecay = float.PositiveInfinity) => 390 OnUserScroll(Target + value, animated, distanceDecay); 391 392 /// <summary> 393 /// Scroll to the start of available content. 394 /// </summary> 395 /// <param name="animated">Whether to animate the movement.</param> 396 /// <param name="allowDuringDrag">Whether we should interrupt a user's active drag.</param> 397 public void ScrollToStart(bool animated = true, bool allowDuringDrag = false) 398 { 399 if (!IsDragging || allowDuringDrag) 400 scrollTo(0, animated, DistanceDecayJump); 401 } 402 403 /// <summary> 404 /// Scroll to the end of available content. 405 /// </summary> 406 /// <param name="animated">Whether to animate the movement.</param> 407 /// <param name="allowDuringDrag">Whether we should interrupt a user's active drag.</param> 408 public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false) 409 { 410 if (!IsDragging || allowDuringDrag) 411 scrollTo(ScrollableExtent, animated, DistanceDecayJump); 412 } 413 414 /// <summary> 415 /// Scrolls to a new position relative to the current scroll offset. 416 /// </summary> 417 /// <param name="offset">The amount by which we should scroll.</param> 418 /// <param name="animated">Whether to animate the movement.</param> 419 public void ScrollBy(float offset, bool animated = true) => scrollTo(Target + offset, animated); 420 421 /// <summary> 422 /// Handle a scroll to an absolute position from a user input. 423 /// </summary> 424 /// <param name="value">The position to scroll to.</param> 425 /// <param name="animated">Whether to animate the movement.</param> 426 /// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param> 427 protected virtual void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) => 428 ScrollTo(value, animated, distanceDecay); 429 430 /// <summary> 431 /// Scrolls to an absolute position. 432 /// </summary> 433 /// <param name="value">The position to scroll to.</param> 434 /// <param name="animated">Whether to animate the movement.</param> 435 /// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param> 436 public void ScrollTo(float value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump); 437 438 private void scrollTo(float value, bool animated, double distanceDecay = float.PositiveInfinity) 439 { 440 Target = Clamp(value, ClampExtension); 441 442 if (animated) 443 this.distanceDecay = distanceDecay; 444 else 445 Current = Target; 446 } 447 448 /// <summary> 449 /// Scrolls a <see cref="Drawable"/> to the top. 450 /// </summary> 451 /// <param name="d">The <see cref="Drawable"/> to scroll to.</param> 452 /// <param name="animated">Whether to animate the movement.</param> 453 public void ScrollTo(Drawable d, bool animated = true) => ScrollTo(GetChildPosInContent(d), animated); 454 455 /// <summary> 456 /// Scrolls a <see cref="Drawable"/> into view. 457 /// </summary> 458 /// <param name="d">The <see cref="Drawable"/> to scroll into view.</param> 459 /// <param name="animated">Whether to animate the movement.</param> 460 public void ScrollIntoView(Drawable d, bool animated = true) 461 { 462 float childPos0 = GetChildPosInContent(d); 463 float childPos1 = GetChildPosInContent(d, d.DrawSize); 464 465 float minPos = Math.Min(childPos0, childPos1); 466 float maxPos = Math.Max(childPos0, childPos1); 467 468 if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent)) 469 ScrollTo(minPos, animated); 470 else if (maxPos > Current + DisplayableContent) 471 ScrollTo(maxPos - DisplayableContent, animated); 472 } 473 474 /// <summary> 475 /// Determines the position of a child in the content. 476 /// </summary> 477 /// <param name="d">The child to get the position from.</param> 478 /// <param name="offset">Positional offset in the child's space.</param> 479 /// <returns>The position of the child.</returns> 480 public float GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim]; 481 482 /// <summary> 483 /// Determines the position of a child in the content. 484 /// </summary> 485 /// <param name="d">The child to get the position from.</param> 486 /// <returns>The position of the child.</returns> 487 public float GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero); 488 489 private void updatePosition() 490 { 491 double localDistanceDecay = distanceDecay; 492 493 // If we are not currently dragging the content, and we have scrolled out of bounds, 494 // then we should handle the clamping force. Note, that if the target is _within_ 495 // acceptable bounds, then we do not need special handling of the clamping force, as 496 // we will naturally scroll back into acceptable bounds. 497 if (!IsDragging && Current != Clamp(Current) && Target != Clamp(Target, -0.01f)) 498 { 499 // Firstly, we want to limit how far out the target may go to limit overly bouncy 500 // behaviour with extreme scroll velocities. 501 Target = Clamp(Target, ClampExtension); 502 503 // Secondly, we would like to quickly approach the target while we are out of bounds. 504 // This is simulating a "strong" clamping force towards the target. 505 if (Current < Target && Target < 0 || Current > Target && Target > ScrollableExtent) 506 localDistanceDecay = distance_decay_clamping * 2; 507 508 // Lastly, we gradually nudge the target towards valid bounds. 509 Target = (float)Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed)); 510 511 float clampedTarget = Clamp(Target); 512 if (Precision.AlmostEquals(clampedTarget, Target)) 513 Target = clampedTarget; 514 } 515 516 // Exponential interpolation between the target and our current scroll position. 517 Current = (float)Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed)); 518 519 // This prevents us from entering the de-normalized range of floating point numbers when approaching target closely. 520 if (Precision.AlmostEquals(Current, Target)) 521 Current = Target; 522 } 523 524 protected override void UpdateAfterChildren() 525 { 526 base.UpdateAfterChildren(); 527 528 updateSize(); 529 updatePosition(); 530 531 if (!scrollbarCache.IsValid) 532 { 533 var size = ScrollDirection == Direction.Horizontal ? DrawWidth : DrawHeight; 534 if (size > 0) 535 Scrollbar.ResizeTo(Math.Clamp(AvailableContent > 0 ? DisplayableContent / AvailableContent : 0, Math.Min(Scrollbar.MinimumDimSize / size, 1), 1), 200, Easing.OutQuint); 536 Scrollbar.FadeTo(ScrollbarVisible && AvailableContent - 1 > DisplayableContent ? 1 : 0, 200); 537 updatePadding(); 538 539 scrollbarCache.Validate(); 540 } 541 542 if (ScrollDirection == Direction.Horizontal) 543 { 544 Scrollbar.X = toScrollbarPosition(Current); 545 ScrollContent.X = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.X; 546 } 547 else 548 { 549 Scrollbar.Y = toScrollbarPosition(Current); 550 ScrollContent.Y = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; 551 } 552 } 553 554 /// <summary> 555 /// Converts a scroll position to a scrollbar position. 556 /// </summary> 557 /// <param name="scrollPosition">The absolute scroll position (e.g. <see cref="Current"/>).</param> 558 /// <returns>The scrollbar position.</returns> 559 private float toScrollbarPosition(float scrollPosition) 560 { 561 if (Precision.AlmostEquals(0, ScrollableExtent)) 562 return 0; 563 564 return ScrollbarMovementExtent * (scrollPosition / ScrollableExtent); 565 } 566 567 /// <summary> 568 /// Converts a scrollbar position to a scroll position. 569 /// </summary> 570 /// <param name="scrollbarPosition">The scrollbar position.</param> 571 /// <returns>The absolute scroll position.</returns> 572 private float fromScrollbarPosition(float scrollbarPosition) 573 { 574 if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) 575 return 0; 576 577 return ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent); 578 } 579 580 /// <summary> 581 /// Creates the scrollbar for this <see cref="ScrollContainer{T}"/>. 582 /// </summary> 583 /// <param name="direction">The scrolling direction.</param> 584 protected abstract ScrollbarContainer CreateScrollbar(Direction direction); 585 586 protected internal abstract class ScrollbarContainer : Container 587 { 588 private float dragOffset; 589 590 internal Action<float> Dragged; 591 592 protected readonly Direction ScrollDirection; 593 594 /// <summary> 595 /// The minimum size of this <see cref="ScrollbarContainer"/>. Defaults to the size in the non-scrolling direction. 596 /// </summary> 597 protected internal virtual float MinimumDimSize => Size[ScrollDirection == Direction.Vertical ? 0 : 1]; 598 599 protected ScrollbarContainer(Direction direction) 600 { 601 ScrollDirection = direction; 602 603 RelativeSizeAxes = direction == Direction.Horizontal ? Axes.X : Axes.Y; 604 } 605 606 public abstract void ResizeTo(float val, int duration = 0, Easing easing = Easing.None); 607 608 protected override bool OnClick(ClickEvent e) => true; 609 610 protected override bool OnDragStart(DragStartEvent e) 611 { 612 if (e.Button != MouseButton.Left) return false; 613 614 dragOffset = e.MousePosition[(int)ScrollDirection] - Position[(int)ScrollDirection]; 615 return true; 616 } 617 618 protected override bool OnMouseDown(MouseDownEvent e) 619 { 620 if (e.Button != MouseButton.Left) return false; 621 622 dragOffset = Position[(int)ScrollDirection]; 623 Dragged?.Invoke(dragOffset); 624 return true; 625 } 626 627 protected override void OnDrag(DragEvent e) 628 { 629 Dragged?.Invoke(e.MousePosition[(int)ScrollDirection] - dragOffset); 630 } 631 } 632 633 public bool OnPressed(PlatformAction action) 634 { 635 if (!IsHandlingKeyboardScrolling) 636 return false; 637 638 switch (action) 639 { 640 case PlatformAction.MoveBackwardLine: 641 ScrollToStart(); 642 return true; 643 644 case PlatformAction.MoveForwardLine: 645 ScrollToEnd(); 646 return true; 647 648 default: 649 return false; 650 } 651 } 652 653 public void OnReleased(PlatformAction action) 654 { 655 } 656 } 657}