//-----------------------------------------------------------------------------
// PageFlipTracker.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework;
using System.Diagnostics;
namespace UserInterfaceSample.Controls
{
///
/// PageFlipTracker watches the touchpanel for drag and flick gestures, and computes the appropriate
/// offsets for flipping horizontally through a multi-page display. It is used by PageFlipControl
/// to handle the scroll logic. PageFlipTracker is broken out into a separate class so that XNA apps
/// with their own scheme for UI controls can still use it to handle the scroll logic.
///
/// 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.
///
/// Handling screen rotation
/// ------------------------
/// This class uses TouchPanel.DisplayWidth to determine the width of the screen. DisplayWidth
/// is automatically updated by the system when the orientation changes.
///
public class PageFlipTracker
{
public const GestureType GesturesNeeded = GestureType.Flick | GestureType.HorizontalDrag | GestureType.DragComplete;
#region Tuning options
public static TimeSpan FlipDuration = TimeSpan.FromSeconds(0.3);
///
/// Exponent on curve to make page flips and springbacks start quickly and slow to a stop.
///
/// Interpolation formula is (1-TransitionAlpha)^TransitionExponent, where
/// TransitionAlpha animates uniformly from 0 to 1 over timespan FlipDuration.
///
public static double FlipExponent = 3.0;
///
/// By default, this many pixels of the next page will be visible
/// on the right-hand edge of the screen, unless the current page's
/// contentWidth is too large.
///
public static int PreviewMargin = 20;
///
/// How far (as a fraction of the total screen width) you have
/// to drag a screen past its edge to trigger a flip by dragging.
///
public static float DragToFlipTheshold = 1.0f / 3.0f;
#endregion
#region Private fields
// Time stamp when transition started
private DateTime flipStartTime;
// Horizontal offset at start of current transition. Target offset is always 0.
private float flipStartOffset;
#endregion
#region Properties
// Current active page. If we're in a transition, this is the page we're transitioning TO.
public int CurrentPage { get; private set; }
// Offset in pixels to render currentPage at. If this is positive, other
// pages may be visible to the left.
//
// This is always relative to the current page.
public float CurrentPageOffset { get; private set; }
public bool IsLeftPageVisible
{
get
{
return PageWidthList.Count >= 2 && CurrentPageOffset > 0;
}
}
public bool IsRightPageVisible
{
get
{
return PageWidthList.Count >= 2 && CurrentPageOffset + EffectivePageWidth(CurrentPage) <= TouchPanel.DisplayWidth;
}
}
// True if we're currently in a transition
public bool InFlip { get; private set; }
// Alpha value that animates from 0 to 1 during a spring. Will be 1 when not springing.
public float FlipAlpha { get; private set; }
// PageWidthList contains the width in pixels of each page. Pages can be added or removed at any time by
// changing this list.
public List PageWidthList = new List();
#endregion
public PageFlipTracker()
{
}
public int EffectivePageWidth(int page)
{
int displayWidth = TouchPanel.DisplayWidth - PreviewMargin;
return Math.Max(displayWidth, PageWidthList[page]);
}
// Update is called once per frame.
public void Update()
{
if (InFlip)
{
TimeSpan transitionClock = DateTime.Now - flipStartTime;
if (transitionClock >= FlipDuration)
{
EndFlip();
}
else
{
double f = transitionClock.TotalSeconds / FlipDuration.TotalSeconds;
f = Math.Max(f, 0.0); // this shouldn't happen, but just in case time goes crazy
FlipAlpha = (float)(1 - Math.Pow(1 - f, FlipExponent));
CurrentPageOffset = flipStartOffset * (1 - FlipAlpha);
}
}
}
public void HandleInput(InputState input)
{
foreach (GestureSample sample in input.Gestures)
{
switch (sample.GestureType)
{
case GestureType.HorizontalDrag:
CurrentPageOffset += sample.Delta.X;
flipStartOffset = CurrentPageOffset;
break;
case GestureType.DragComplete:
if (!InFlip)
{
if (CurrentPageOffset < -TouchPanel.DisplayWidth * DragToFlipTheshold)
{
// flip to next page
BeginFlip(1);
}
else if (CurrentPageOffset + TouchPanel.DisplayWidth * (1 - DragToFlipTheshold) > EffectivePageWidth(CurrentPage))
{
// flip to previous page
BeginFlip(-1);
}
else
{
// "snap back" effect when you drag a little and let go
BeginFlip(0);
}
}
break;
case GestureType.Flick:
// Only respond to mostly-horizontal flicks
if (Math.Abs(sample.Delta.X) > Math.Abs(sample.Delta.Y))
{
if (sample.Delta.X > 0)
{
BeginFlip(-1);
}
else
{
BeginFlip(1);
}
}
break;
}
}
}
void BeginFlip(int pageDelta)
{
if(PageWidthList.Count == 0)
return;
int pageFrom = CurrentPage;
CurrentPage = (CurrentPage + pageDelta + PageWidthList.Count) % PageWidthList.Count;
if (pageDelta > 0)
{
// going to next page; offset starts out large
CurrentPageOffset += EffectivePageWidth(pageFrom);
}
else if(pageDelta < 0)
{
// going to previous page; offset starts out negative
CurrentPageOffset -= EffectivePageWidth(CurrentPage);
}
InFlip = true;
FlipAlpha = 0;
flipStartOffset = CurrentPageOffset;
flipStartTime = DateTime.Now;
}
// FIXME: private
void EndFlip()
{
InFlip = false;
FlipAlpha = 1;
CurrentPageOffset = 0;
}
}
}