// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
// This is a very special primitive control that works around a limitation in
// the core animation subsystem of Silverlight: there is no way to declare in
// VSM states relative properties, such as animating from 0 to 33% the width of
// the control, using double animations for translation.
//
// It's a tough problem to solve property, but this primitive, unsupported
// control does offer a solution based on magic numbers that still allows a
// designer to make alterations to their animation values to present their
// vision for custom templates.
//
// This is instrumental in offering a Windows Phone ProgressBar implementation
// that uses the render thread instead of animating UI thread-only properties.
//
// For questions, please see
// http://www.jeff.wilcox.name/performanceprogressbar/
//
// This control is licensed Ms-PL and as such comes with no warranties or
// official support.
//
// Style Note
// - - -
// The style that must be used with this is present at the bottom of this file.
//
namespace Microsoft.Phone.Controls.Unsupported
{
///
/// A very specialized primitive control that works around a specific visual
/// state manager issue. The platform does not support relative sized
/// translation values, and this special control walks through visual state
/// animation storyboards looking for magic numbers to use as percentages.
/// This control is not supported, unofficial, and is a hack in many ways.
/// It is used to enable a Windows Phone native platform-style progress bar
/// experience in indeterminate mode that remains performant.
///
public class RelativeAnimatingContentControl : ContentControl
{
///
/// A simple Epsilon-style value used for trying to determine the magic
/// state, if any, of a double.
///
private const double SimpleDoubleComparisonEpsilon = 0.000009;
///
/// The last known width of the control.
///
private double _knownWidth;
///
/// The last known height of the control.
///
private double _knownHeight;
///
/// A set of custom animation adapters used to update the animation
/// storyboards when the size of the control changes.
///
private List _specialAnimations;
///
/// Initializes a new instance of the RelativeAnimatingContentControl
/// type.
///
public RelativeAnimatingContentControl()
{
SizeChanged += OnSizeChanged;
}
///
/// Handles the size changed event.
///
/// The source object.
/// The event arguments.
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (e != null && e.NewSize.Height > 0 && e.NewSize.Width > 0)
{
_knownWidth = e.NewSize.Width;
_knownHeight = e.NewSize.Height;
Clip = new RectangleGeometry { Rect = new Rect(0, 0, _knownWidth, _knownHeight), };
UpdateAnyAnimationValues();
}
}
///
/// Walks through the known storyboards in the control's template that
/// may contain magic double animation values, storing them for future
/// use and updates.
///
private void UpdateAnyAnimationValues()
{
if (_knownHeight > 0 && _knownWidth > 0)
{
// Initially, before any special animations have been found,
// the visual state groups of the control must be explored.
// By definition they must be at the implementation root of the
// control, and this is designed to not walk into any other
// depth.
if (_specialAnimations == null)
{
_specialAnimations = new List();
foreach (VisualStateGroup group in VisualStateManager.GetVisualStateGroups(this))
{
if (group == null)
{
continue;
}
foreach (VisualState state in group.States)
{
if (state != null)
{
Storyboard sb = state.Storyboard;
if (sb != null)
{
// Examine all children of the storyboards,
// looking for either type of double
// animation.
foreach (Timeline timeline in sb.Children)
{
DoubleAnimation da = timeline as DoubleAnimation;
DoubleAnimationUsingKeyFrames dakeys = timeline as DoubleAnimationUsingKeyFrames;
if (da != null)
{
ProcessDoubleAnimation(da);
}
else if (dakeys != null)
{
ProcessDoubleAnimationWithKeys(dakeys);
}
}
}
}
}
}
}
// Update special animation values relative to the current size.
UpdateKnownAnimations();
}
}
///
/// Walks through all special animations, updating based on the current
/// size of the control.
///
private void UpdateKnownAnimations()
{
foreach (AnimationValueAdapter adapter in _specialAnimations)
{
adapter.UpdateWithNewDimension(_knownWidth, _knownHeight);
}
}
///
/// Processes a double animation with keyframes, looking for known
/// special values to store with an adapter.
///
/// The double animation using key frames instance.
private void ProcessDoubleAnimationWithKeys(DoubleAnimationUsingKeyFrames da)
{
// Look through all keyframes in the instance.
foreach (DoubleKeyFrame frame in da.KeyFrames)
{
var d = DoubleAnimationFrameAdapter.GetDimensionFromMagicNumber(frame.Value);
if (d.HasValue)
{
_specialAnimations.Add(new DoubleAnimationFrameAdapter(d.Value, frame));
}
}
}
///
/// Processes a double animation looking for special values.
///
/// The double animation instance.
private void ProcessDoubleAnimation(DoubleAnimation da)
{
// Look for a special value in the To property.
if (da.To.HasValue)
{
var d = DoubleAnimationToAdapter.GetDimensionFromMagicNumber(da.To.Value);
if (d.HasValue)
{
_specialAnimations.Add(new DoubleAnimationToAdapter(d.Value, da));
}
}
// Look for a special value in the From property.
if (da.From.HasValue)
{
var d = DoubleAnimationFromAdapter.GetDimensionFromMagicNumber(da.To.Value);
if (d.HasValue)
{
_specialAnimations.Add(new DoubleAnimationFromAdapter(d.Value, da));
}
}
}
#region Private animation updating system
///
/// A selection of dimensions of interest for updating an animation.
///
private enum DoubleAnimationDimension
{
///
/// The width (horizontal) dimension.
///
Width,
///
/// The height (vertical) dimension.
///
Height,
}
///
/// A simple class designed to store information about a specific
/// animation instance and its properties. Able to update the values at
/// runtime.
///
private abstract class AnimationValueAdapter
{
///
/// Gets or sets the original double value.
///
protected double OriginalValue { get; set; }
///
/// Initializes a new instance of the AnimationValueAdapter type.
///
/// The dimension of interest for updates.
public AnimationValueAdapter(DoubleAnimationDimension dimension)
{
Dimension = dimension;
}
///
/// Gets the dimension of interest for the control.
///
public DoubleAnimationDimension Dimension { get; private set; }
///
/// Updates the original instance based on new dimension information
/// from the control. Takes both and allows the subclass to make the
/// decision on which ratio, values, and dimension to use.
///
/// The width of the control.
/// The height of the control.
public abstract void UpdateWithNewDimension(double width, double height);
}
private abstract class GeneralAnimationValueAdapter : AnimationValueAdapter
{
///
/// Stores the animation instance.
///
protected T Instance { get; set; }
///
/// Gets the value of the underlying property of interest.
///
/// Returns the value of the property.
protected abstract double GetValue();
///
/// Sets the value for the underlying property of interest.
///
/// The new value for the property.
protected abstract void SetValue(double newValue);
///
/// Gets the initial value (minus the magic number portion) that the
/// designer stored within the visual state animation property.
///
protected double InitialValue { get; private set; }
///
/// The ratio based on the original magic value, used for computing
/// the updated animation property of interest when the size of the
/// control changes.
///
private double _ratio;
///
/// Initializes a new instance of the GeneralAnimationValueAdapter
/// type.
///
/// The dimension of interest.
/// The animation type instance.
public GeneralAnimationValueAdapter(DoubleAnimationDimension d, T instance)
: base(d)
{
Instance = instance;
InitialValue = StripMagicNumberOff(GetValue());
_ratio = InitialValue / 100;
}
///
/// Approximately removes the magic number state from a value.
///
/// The initial number.
/// Returns a double with an adjustment for the magic
/// portion of the number.
public double StripMagicNumberOff(double number)
{
return Dimension == DoubleAnimationDimension.Width ? number - .1 : number - .2;
}
///
/// Retrieves the dimension, if any, from the number. If the number
/// is not magic, null is returned instead.
///
/// The double value.
/// Returs a double animation dimension, if the number was
/// partially magic; otherwise, returns null.
public static DoubleAnimationDimension? GetDimensionFromMagicNumber(double number)
{
double floor = Math.Floor(number);
double remainder = number - floor;
if (remainder >= .1 - SimpleDoubleComparisonEpsilon && remainder <= .1 + SimpleDoubleComparisonEpsilon)
{
return DoubleAnimationDimension.Width;
}
if (remainder >= .2 - SimpleDoubleComparisonEpsilon && remainder <= .2 + SimpleDoubleComparisonEpsilon)
{
return DoubleAnimationDimension.Height;
}
return null;
}
///
/// Updates the animation instance based on the dimensions of the
/// control.
///
/// The width of the control.
/// The height of the control.
public override void UpdateWithNewDimension(double width, double height)
{
double size = Dimension == DoubleAnimationDimension.Width ? width : height;
UpdateValue(size);
}
///
/// Updates the value of the property.
///
/// The size of interest to use with a ratio
/// computation.
private void UpdateValue(double sizeToUse)
{
SetValue(sizeToUse * _ratio);
}
}
///
/// Adapter for DoubleAnimation's To property.
///
private class DoubleAnimationToAdapter : GeneralAnimationValueAdapter
{
///
/// Gets the value of the underlying property of interest.
///
/// Returns the value of the property.
protected override double GetValue()
{
return (double)Instance.To;
}
///
/// Sets the value for the underlying property of interest.
///
/// The new value for the property.
protected override void SetValue(double newValue)
{
Instance.To = newValue;
}
///
/// Initializes a new instance of the DoubleAnimationToAdapter type.
///
/// The dimension of interest.
/// The instance of the animation type.
public DoubleAnimationToAdapter(DoubleAnimationDimension dimension, DoubleAnimation instance)
: base(dimension, instance)
{
}
}
///
/// Adapter for DoubleAnimation's From property.
///
private class DoubleAnimationFromAdapter : GeneralAnimationValueAdapter
{
///
/// Gets the value of the underlying property of interest.
///
/// Returns the value of the property.
protected override double GetValue()
{
return (double)Instance.From;
}
///
/// Sets the value for the underlying property of interest.
///
/// The new value for the property.
protected override void SetValue(double newValue)
{
Instance.From = newValue;
}
///
/// Initializes a new instance of the DoubleAnimationFromAdapter
/// type.
///
/// The dimension of interest.
/// The instance of the animation type.
public DoubleAnimationFromAdapter(DoubleAnimationDimension dimension, DoubleAnimation instance)
: base(dimension, instance)
{
}
}
///
/// Adapter for double key frames.
///
private class DoubleAnimationFrameAdapter : GeneralAnimationValueAdapter
{
///
/// Gets the value of the underlying property of interest.
///
/// Returns the value of the property.
protected override double GetValue()
{
return Instance.Value;
}
///
/// Sets the value for the underlying property of interest.
///
/// The new value for the property.
protected override void SetValue(double newValue)
{
Instance.Value = newValue;
}
///
/// Initializes a new instance of the DoubleAnimationFrameAdapter
/// type.
///
/// The dimension of interest.
/// The instance of the animation type.
public DoubleAnimationFrameAdapter(DoubleAnimationDimension dimension, DoubleKeyFrame frame)
: base(dimension, frame)
{
}
}
#endregion
}
/*
This is the style that should be used with the control. Make sure to define
the XMLNS at the top of the style file similar to this:
xmlns:unsupported="clr-namespace:Microsoft.Phone.Controls.Unsupported"
*/
}