Introduction
This is a custom implementation of a WPF panel that simulates the action of choosing a card from a deck. The selected card jumps to the front of the deck. Some animations and transformations are performed, and the other cards are subsequently moved to the back of the deck, maintaining their initial order. The card animations are essentially random, which creates a unique look each time a card is selected. Henceforth, each card selection precipitates a different animation speed, resulting in a life-like look and feel to the entire event. This custom card panel can be used with any UIElement
object, and is not purely limited to images.
Background
In the past, I have implemented Windows Controls, and ultimately became very fond of the numerous techniques that had to be employed. I was forced to use the GDI or GDI+ APIs, and was ultimately a very happy man even with the Immediate Mode rendering engine. I took pride in my controls because they were not trivial to implement. Then came WPF (a moment of silence for Windows Controls...). WPF made it almost trivial to modify and create professional looking controls. While creating and modifying Windows Controls were a lot of fun, WPF, in my opinion, has replaced the hours of coding with hours of creativity. It did not take me long to learn WPF. I would say, it naturally fell into my lap. I wanted to create this Card Panel to show developers how relatively easy it has now become to create controls and animations. While we are using DirectX under the hood, for example, most, if not all, of these implementation details are under the hood. (I do feel that developers should still get intimate with these technologies just to walk the walk and talk the talk, so to speak.)
Using the code
Firstly, the Card Panel actually is not as complicated as one would initially assume. I wanted to be able to provide a custom behavior that the WPF panels did not possess. In order to create our own custom layout, we have the option to derive from the WPF Panel
class and override the MeasureOverride()
and ArrangeOverride()
methods. WPF has a two step layout process that is responsible for sizing and arranging children. The first stage is dubbed the measure phase. In this phase, the children tell their parent how large (or small) they would like to be. The second phase is dubbed the layout phase. This phase is very important as the children are actually laid out and given their bounds.
During the measure phase, we have to iterate through all of the children in the panel and call the Measure()
method on each one. At this point, the children get to tell their parent their desired size. After the call to Measure()
, each child's DesiredSize
property will contain the size they want. At the end of the MeasureOverride()
method, the panel will know how much space its children need altogether and returns it.
protected override Size MeasureOverride(Size availableSize)
{
Size idealSize = new Size(0, 0);
Size size = new Size(this.ItemWidth, this.ItemHeight);
foreach (UIElement child in Children)
{
child.Measure(size);
idealSize.Width =
Math.Max(child.DesiredSize.Width, idealSize.Width);
idealSize.Height += child.DesiredSize.Height + childPadding;
}
totalChildHeight = idealSize.Height;
forMoving.Clear();
if (double.IsInfinity(availableSize.Height) ||
double.IsInfinity(availableSize.Width))
{
return idealSize;
}
else
{
return availableSize;
}
}
During the layout phase, the ArrangeOverride()
method is called. At this point, each of the panel's children has already been measured in the MeasureOverride()
procedure. By calling the Arrange()
method of each child, you can set their size and position in the panel.
protected override Size ArrangeOverride(Size finalSize)
{
if (this.Children == null || this.Children.Count == 0)
{
return finalSize;
}
ourSize = finalSize;
totalChildWidth = 0;
foreach (UIElement child in this.Children)
{
if (child.RenderTransform as TransformGroup == null)
{
child.RenderTransformOrigin =
new Point(transformOriginX, transformOriginY);
TransformGroup group = new TransformGroup();
child.RenderTransform = group;
group.Children.Add(new TranslateTransform());
group.Children.Add(new ScaleTransform());
}
child.Arrange(new Rect(10, 0,
child.DesiredSize.Width, child.DesiredSize.Height));
totalChildWidth += child.DesiredSize.Width;
}
forMoving.Clear();
AnimateChildren();
return finalSize;
}
You can disregard all of the animation code in the methods. They are not at all involved in the layout process.
Because each child, when clicked, needs to pop-up, I used a scaling transformation to increase their size. This simulates the pop up behavior. Subsequently, the selected child needs to 'pop-down' into the deck. Again, I employed the ScaleTranform
, however, to scale down rather than up. To actually move the card up and down the deck, I employed the use of another transformed called a TranslateTransform
.
Here is the complete code for the WPF Card Panel:
[Serializable]
public class CardPanel : Panel
{
#region Private Members
private double totalChildHeight;
private double totalChildWidth;
private Size ourSize;
private double transformOriginY = 1;
private double transformOriginX = 0.8;
private double duration = 700;
private double childPadding = 5;
private bool childClicked;
private int clickedIndex;
private UIElement clickedChild;
private const int RAND_FROM = 700;
private const int RAND_TO = 4000;
private IDictionary<uielement,> elementLocations =
new Dictionary<uielement,>();
private ICollection<uielement> forMoving =
new Collection<uielement>();
private Collection<uielement> list =
new Collection<uielement>();
public static readonly DependencyProperty ItemHeightProperty =
DependencyProperty.Register("ItemHeight", typeof(double),
typeof(CardPanel), new FrameworkPropertyMetadata((double)50,
FrameworkPropertyMetadataOptions.AffectsMeasure));
#endregion
#region Public Properties
public double ItemWidth
{
get { return (double)GetValue(ItemWidthProperty); }
set { SetValue(ItemWidthProperty, value); }
}
public static readonly DependencyProperty ItemWidthProperty =
DependencyProperty.Register("ItemWidth",
typeof(double),typeof(CardPanel),
new FrameworkPropertyMetadata((double)50,
FrameworkPropertyMetadataOptions.AffectsMeasure));
public double ItemHeight
{
get { return (double)GetValue(ItemHeightProperty); }
set { SetValue(ItemHeightProperty, value); }
}
#endregion
#region Constructors
public CardPanel()
{
}
#endregion
#region Helper Procedures
private void AddElementToLocations(UIElement element, double x, double y)
{
if (!elementLocations.ContainsKey(element))
{
elementLocations.Add(element, new Point(x, y));
}
else
{
elementLocations[element] = new Point(x, y);
}
}
private int GetClickedIndex(object child)
{
int i = 0;
foreach (UIElement element in list)
{
if (element == child)
{
return i;
}
else
{
i++;
}
}
return i;
}
private UIElement GetElement(int index)
{
if (index > list.Count || list.Count == 0)
{
InvalidateMeasure();
return null;
}
return list[index];
}
private void ReAddChildrenToPanel(IEnumerable<uielement> childrenToAdd)
{
int i = 0;
foreach (UIElement element in list)
{
CardPanel.SetZIndex(element, i);
i++;
}
}
private double GetSeparation(double childHeight)
{
double relativeSeparation = 0;
if (totalChildHeight < ourSize.Height)
{
relativeSeparation = childHeight + childPadding;
}
else
{
relativeSeparation = (ourSize.Height -
childHeight - 10) / (Children.Count - 1);
}
return relativeSeparation;
}
#endregion
#region Overrides
protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
UIElement element = visualAdded as UIElement;
if (element != null)
{
element.AddHandler(UIElement.MouseLeftButtonDownEvent,
new RoutedEventHandler(element_MouseLeftButtonDown), true);
}
}
protected override Size MeasureOverride(Size availableSize)
{
Size idealSize = new Size(0, 0);
Size size = new Size(this.ItemWidth, this.ItemHeight);
foreach (UIElement child in Children)
{
child.Measure(size);
idealSize.Width = Math.Max(child.DesiredSize.Width, idealSize.Width);
idealSize.Height += child.DesiredSize.Height + childPadding;
}
totalChildHeight = idealSize.Height;
forMoving.Clear();
if (double.IsInfinity(availableSize.Height) ||
double.IsInfinity(availableSize.Width))
{
return idealSize;
}
else
{
return availableSize;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
if (this.Children == null || this.Children.Count == 0)
{
return finalSize;
}
ourSize = finalSize;
totalChildWidth = 0;
foreach (UIElement child in this.Children)
{
if (child.RenderTransform as TransformGroup == null)
{
child.RenderTransformOrigin =
new Point(transformOriginX, transformOriginY);
TransformGroup group = new TransformGroup();
child.RenderTransform = group;
group.Children.Add(new TranslateTransform());
group.Children.Add(new ScaleTransform());
}
child.Arrange(new Rect(10, 0,
child.DesiredSize.Width, child.DesiredSize.Height));
totalChildWidth += child.DesiredSize.Width;
}
forMoving.Clear();
AnimateChildren();
return finalSize;
}
#endregion
#region Element Events
private void element_MouseLeftButtonDown(object sender, RoutedEventArgs e)
{
if (!childClicked)
{
childClicked = true;
clickedIndex = GetClickedIndex(sender);
this.InvalidateArrange();
}
}
#endregion
#region Animation procedures
private void AnimateChildren()
{
if (this.Children == null || this.Children.Count == 0)
return;
double x = 10;
double y = 0;
if (childClicked)
{
clickedChild = GetElement(clickedIndex);
if (clickedChild == null) return;
if (Children.Count > 0)
{
UIElement bottomChild = list[list.Count - 1];
if (clickedChild != bottomChild)
{
AnimateToScaleUp(clickedChild, duration);
}
Point point = elementLocations[bottomChild];
AnimateTo(clickedChild, point.X, point.Y, duration);
elementLocations[clickedChild] = new Point(point.X, point.Y);
for (int i = clickedIndex + 1; i < list.Count; i++)
{
UIElement element = GetElement(i);
if (element != null)
forMoving.Add(element);
}
x = 10;
y = 0;
Random rand = new Random();
int currentAddIndex = 0;
foreach (UIElement element in forMoving)
{
elementLocations[element] = new Point(x, y + childPadding);
list.Remove(element);
list.Insert(currentAddIndex, element);
AnimateTo(element, x, y, rand.Next(RAND_FROM, RAND_TO));
y += GetSeparation(element.DesiredSize.Height);
currentAddIndex++;
}
foreach (UIElement element in Children)
{
if (!forMoving.Contains(element) && element != clickedChild)
{
elementLocations[element] = new Point(x, y + 10);
list.Remove(element);
list.Insert(currentAddIndex, element);
AnimateTo(element, x, y, rand.Next(RAND_FROM, RAND_TO));
y += GetSeparation(element.DesiredSize.Height);
currentAddIndex++;
}
}
ReAddChildrenToPanel(list);
childClicked = false;
}
}
else
{
list.Clear();
elementLocations.Clear();
foreach (UIElement element in base.InternalChildren)
{
AddElementToLocations(element, x, y + childPadding);
list.Add(element);
AnimateTo(element, x, y + childPadding, duration);
y += GetSeparation(element.DesiredSize.Height);
}
}
}
private void AnimateToScaleUp(UIElement child, double duration)
{
if (child == null) return;
TransformGroup group = (TransformGroup)child.RenderTransform;
ScaleTransform scale = (ScaleTransform)group.Children[1];
{
scale.BeginAnimation(ScaleTransform.ScaleXProperty,
MakeAnimation(1.2, duration / 2, animation_Completed));
scale.BeginAnimation(ScaleTransform.ScaleYProperty,
MakeAnimation(1.2, duration / 2));
}
}
private void ScaleDown(UIElement child, double duration)
{
if (child == null) return;
TransformGroup group = (TransformGroup)child.RenderTransform;
ScaleTransform scale = (ScaleTransform)group.Children[1];
scale.CenterX = -0.5;
scale.CenterY = 0;
scale.BeginAnimation(ScaleTransform.ScaleXProperty,
MakeAnimation(1, duration / 3));
scale.BeginAnimation(ScaleTransform.ScaleYProperty,
MakeAnimation(1, duration / 3));
}
private void AnimateTo(UIElement child, double x,
double y, double duration)
{
if (child == null) return;
TransformGroup group = (TransformGroup)child.RenderTransform;
TranslateTransform trans = (TranslateTransform)group.Children[0];
ScaleTransform scale = (ScaleTransform)group.Children[1];
trans.BeginAnimation(TranslateTransform.XProperty,
MakeAnimation(x, duration, animation_Completed));
trans.BeginAnimation(TranslateTransform.YProperty,
MakeAnimation(y, duration));
}
private DoubleAnimation MakeAnimation(double to, double duration)
{
return MakeAnimation(to, duration, null);
}
private DoubleAnimation MakeAnimation(double to,
double duration, EventHandler endEvent)
{
DoubleAnimation anim =
new DoubleAnimation(to, TimeSpan.FromMilliseconds(duration));
anim.AccelerationRatio = 0.2;
anim.DecelerationRatio = 0.7;
if (endEvent != null)
{
anim.Completed += endEvent;
}
return anim;
}
private void animation_Completed(object sender, EventArgs e)
{
ScaleDown(clickedChild, duration);
}
#endregion
}
Conclusion
I had quite a bit of fun creating this custom panel. I have not fully tested it yet. I appreciate your feedback.
Currently pursuing 'Programming Nirvana' (The ineffable ultimate in which one has attained disinterested wisdom and compassion as it relates to programming)
Respected Technologies
1. Confusor (https://confuser.codeplex.com/)
2. Power Threading (http://www.wintellect.com/Resources/visit-the-power-threading-library)
3. EDI Parsers (http://www.rdpcrystal.com)
Acknowledgements:
Microsoft Certified Technologist for WPF and .Net 3.5 (MCTS)
Microsoft Certified Technologist for WCF and .Net 3.5 (MCTS)
Microsoft Certified Application Developer for .Net (MCAD)
Microsoft Certified Systems Engineer (MCSE)
Microsoft Certified Professional (MCP)
Sun Certified Developer for Java 2 Platform (SCD)
Sun Certified Programmer for Java 2 Platform (SCP)
Sun Certified Web Component Developer (SCWCD)
CompTIA A+ Certified Professional
Registered Business School Teacher for Computer Programming and Computer Applications (2004)
(University of the State of New York Education Department)
Graduated from University At Stony Brook