// (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" */ }