//----------------------------------------------------------------------------- // ScrollTracker.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input.Touch; namespace UserInterfaceSample.Controls { /// /// ScrollTracker watches the touchpanel for drag and flick gestures, and computes the appropriate /// position and scale for a viewport within a larger canvas to emulate the behavior of the Silverlight /// scrolling controls. /// /// This class only handles computation of the view rectangle; how that rectangle is used for rendering /// is up tot the client code. /// public class ScrollTracker { /// Handling TouchPanel.EnabledGestures /// -------------------------- /// This class watches for HorizontalDrag, DragComplete, and Flick gestures. However, it cannot just /// set TouchPanel.EnabledGestures, because that would most likely interfere with gestures needed /// elsewhere in the application. So it just exposes a const 'HandledGestures' field and relies on /// the client code to set TouchPanel.EnabledGestures appropriately. public const GestureType GesturesNeeded = GestureType.Flick | GestureType.VerticalDrag | GestureType.DragComplete; #region Tuning constants // How far the user is allowed to drag past the "real" border const float SpringMaxDrag = 400; // How far the display moves when dragged to 'SpringMaxDrag' const float SpringMaxOffset = SpringMaxDrag / 3; const float SpringReturnRate = 0.1f; const float SpringReturnMin = 2.0f; const float Deceleration = 500.0f; // pixels/second^2 const float MaxVelocity = 2000.0f; // pixels/second #endregion /// /// A rectangle (set by the client code) giving the area of the canvas we want to scroll around in. /// Normally taller or wider than the viewport. /// public Rectangle CanvasRect; /// /// A rectangle describing the currently visible view area. Normally the caller will set this once /// to set the viewport size and initial position, and from then on let ScrollTracker move it around; /// however, you can set it at any time to change the position or size of the viewport. /// public Rectangle ViewRect; /// /// FullCanvasRect is the same as CanvasRect, except it's extended to be at least as large as ViewRect. /// The is the true canvas area that we scroll around in. /// public Rectangle FullCanvasRect { get { Rectangle value = CanvasRect; if (value.Width < ViewRect.Width) value.Width = ViewRect.Width; if (value.Height < ViewRect.Height) value.Height = ViewRect.Height; return value; } } // Current flick velocity. Vector2 Velocity; // What the view offset would be if we didn't do any clamping Vector2 ViewOrigin; // What the ViewOrigin would be if we didn't do any clamping Vector2 UnclampedViewOrigin; // True if we're currently tracking a drag gesture public bool IsTracking { get; private set; } public bool IsMoving { get { return IsTracking || Velocity.X != 0 || Velocity.Y != 0 || !FullCanvasRect.Contains(ViewRect); } } public ScrollTracker() { ViewRect = new Rectangle { Width = TouchPanel.DisplayWidth, Height = TouchPanel.DisplayHeight }; CanvasRect = ViewRect; } // This must be called manually each tick that the ScrollTracker is active. public void Update(GameTime gametime) { // Apply velocity and clamping float dt = (float)gametime.ElapsedGameTime.TotalSeconds; Vector2 viewMin = new Vector2 { X = 0, Y = 0 }; Vector2 viewMax = new Vector2 { X = CanvasRect.Width - ViewRect.Width, Y = CanvasRect.Height - ViewRect.Height }; viewMax.X = Math.Max(viewMin.X, viewMax.X); viewMax.Y = Math.Max(viewMin.Y, viewMax.Y); if (IsTracking) { // ViewOrigin is a soft-clamped version of UnclampedOffset ViewOrigin.X = SoftClamp(UnclampedViewOrigin.X, viewMin.X, viewMax.X); ViewOrigin.Y = SoftClamp(UnclampedViewOrigin.Y, viewMin.Y, viewMax.Y); } else { // Apply velocity ApplyVelocity(dt, ref ViewOrigin.X, ref Velocity.X, viewMin.X, viewMax.X); ApplyVelocity(dt, ref ViewOrigin.Y, ref Velocity.Y, viewMin.Y, viewMax.Y); } ViewRect.X = (int)ViewOrigin.X; ViewRect.Y = (int)ViewOrigin.Y; } // This must be called manually each tick that the ScrollTracker is active. public void HandleInput(InputState input) { // Turn on tracking as soon as we seen any kind of touch. We can't use gestures for this // because no gesture data is returned on the initial touch. We have to be careful to // pick out only 'Pressed' locations, because TouchState can return other events a frame // *after* we've seen GestureType.Flick or GestureType.DragComplete. if (!IsTracking) { for (int i = 0; i < input.TouchState.Count; i++) { if (input.TouchState[i].State == TouchLocationState.Pressed) { Velocity = Vector2.Zero; UnclampedViewOrigin = ViewOrigin; IsTracking = true; break; } } } foreach (GestureSample sample in input.Gestures) { switch (sample.GestureType) { case GestureType.VerticalDrag: UnclampedViewOrigin.Y -= sample.Delta.Y; break; case GestureType.Flick: // Only respond to mostly-vertical flicks if (Math.Abs(sample.Delta.X) < Math.Abs(sample.Delta.Y)) { IsTracking = false; Velocity = -sample.Delta; } break; case GestureType.DragComplete: IsTracking = false; break; } } } // If x is within the range (min,max), just return x. Otherwise return a value outside of (min,max) // but only partway to where x is. This is used to get the partial-overdrag effect at the edges // of the list. static float SoftClamp(float x, float min, float max) { if (x < min) { return Math.Max(x - min, -SpringMaxDrag) * SpringMaxOffset / SpringMaxDrag + min; } if (x > max) { return Math.Min(x - max, SpringMaxDrag) * SpringMaxOffset / SpringMaxDrag + max; } return x; } // Integrate the given position and velocity over a timespan. Min and max give the // soft limits that the position is allowed to move to. void ApplyVelocity(float dt, ref float x, ref float v, float min, float max) { float x0 = x; x += v * dt; // Apply deceleration to gradually reduce velocity v = MathHelper.Clamp(v, -MaxVelocity, MaxVelocity); v = Math.Max(Math.Abs(v) - dt * Deceleration, 0.0f) * Math.Sign(v); // If we've scrolled past the edge, gradually reset to edge if (x < min) { x = Math.Min(x + (min - x) * SpringReturnRate + SpringReturnMin, min); v = 0; } if (x > max) { x = Math.Max(x - (x - max) * SpringReturnRate - SpringReturnMin, max); v = 0; } } } }