This post describes the design of the PanView custom control, discussing both the C# and XAML that achieves the desired functionality.
Posts in this series:
PanView is a custom control with the following dependency properties:
TransformGroup TransformGroup
– The transform to be applied to the canvas double MinTranslateX
– A translation constraint double MaxTranslateX
– A translation constraint double MinTranslateY
– A translation constraint double MaxTranslateY
– A translation constraint
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:PanViewLibrary">
<Style
TargetType="local:PanView">
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="local:PanView">
<Grid
Background="Transparent">
<ContentPresenter
RenderTransform="{TemplateBinding TransformGroup}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The XAML above shows how the RenderTransform
of the content is bound to the custom control’s TransformGroup
dependency property.
One approach for developing PanView is to keep appending transformations into the TransformGroup
each time the user performs a manipulation. If PanView kept appending each transformation into a transformation group, then the number of transformations would grow each time the user touched the control. This growth would make PanView unreliable in commercial applications. The PanView code demonstrates how to keep the TransformGroup
from growing beyond two transformations.
The public
method Reset
is a quick programmatic way of resetting the transformation back to the default.
The method ConstrainDelta
is employed to enforce the user-defined constraints on x/y panning distances.
using Windows.UI.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
namespace PanViewLibrary
{
[ContentProperty(Name = "Content")]
public class PanView : ContentControl
{
public PanView()
{
DefaultStyleKey = typeof(PanView);
ManipulationMode = ManipulationModes.All;
_currentTransformation = new CompositeTransform();
_previousTransformations = new MatrixTransform() { Matrix = Matrix.Identity };
TransformGroup = new TransformGroup();
TransformGroup.Children.Add(_previousTransformations);
TransformGroup.Children.Add(_currentTransformation);
ManipulationStarting += (sender, args) => { args.Handled = true; };
ManipulationStarted += OnManipulationStarted;
ManipulationDelta += OnManipulationDelta;
ManipulationCompleted += (sender, args) => { args.Handled = true; };
ManipulationInertiaStarting += (sender, args) => { args.Handled = true; };
MinTranslateX = double.MinValue;
MaxTranslateX = double.MaxValue;
MinTranslateY = double.MinValue;
MaxTranslateY = double.MaxValue;
}
public void Reset()
{
_currentTransformation.Reset();
_previousTransformations.Matrix = Matrix.Identity;
}
CompositeTransform _currentTransformation;
MatrixTransform _previousTransformations;
void OnManipulationStarted(object sender, ManipulationStartedRoutedEventArgs args)
{
_previousTransformations.Matrix = TransformGroup.Value;
_currentTransformation.Reset();
_currentTransformation.CenterX = args.Position.X;
_currentTransformation.CenterY = args.Position.Y;
args.Handled = true;
}
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs args)
{
var delta = ConstrainDelta(args.Delta);
_currentTransformation.TranslateX += delta.Translation.X;
_currentTransformation.TranslateY += delta.Translation.Y;
_currentTransformation.Rotation += delta.Rotation;
_currentTransformation.ScaleX *= delta.Scale;
_currentTransformation.ScaleY *= delta.Scale;
args.Handled = true;
}
private ManipulationDelta ConstrainDelta(ManipulationDelta delta)
{
var newTranslateX = _previousTransformations.Matrix.OffsetX +
_currentTransformation.TranslateX + delta.Translation.X;
var tooLittleX = MinTranslateX - newTranslateX;
if (tooLittleX > 0)
{
delta.Translation.X += tooLittleX;
}
var tooMuchX = newTranslateX - MaxTranslateX;
if (tooMuchX > 0)
{
delta.Translation.X -= tooMuchX;
}
var newTranslateY = _previousTransformations.Matrix.OffsetY +
_currentTransformation.TranslateY + delta.Translation.Y;
var tooLittleY = MinTranslateY - newTranslateY;
if (tooLittleY > 0)
{
delta.Translation.Y += tooLittleY;
}
var tooMuchY = newTranslateY - MaxTranslateY;
if (tooMuchY > 0)
{
delta.Translation.Y -= tooMuchY;
}
return delta;
}
public TransformGroup TransformGroup
{
get { return (TransformGroup)GetValue(TransformGroupProperty); }
private set { SetValue(TransformGroupProperty, value); }
}
public static readonly DependencyProperty TransformGroupProperty =
DependencyProperty.Register("TransformGroup",
typeof(TransformGroup), typeof(PanView), new PropertyMetadata(null));
public double MinTranslateX
{
get { return (double)GetValue(MinTranslateXProperty); }
set { SetValue(MinTranslateXProperty, value); }
}
public static readonly DependencyProperty MinTranslateXProperty =
DependencyProperty.Register("MinTranslateX",
typeof(double), typeof(PanView), new PropertyMetadata(null));
public double MaxTranslateX
{
get { return (double)GetValue(MaxTranslateXProperty); }
set { SetValue(MaxTranslateXProperty, value); }
}
public static readonly DependencyProperty MaxTranslateXProperty =
DependencyProperty.Register("MaxTranslateX",
typeof(double), typeof(PanView), new PropertyMetadata(null));
public double MinTranslateY
{
get { return (double)GetValue(MinTranslateYProperty); }
set { SetValue(MinTranslateYProperty, value); }
}
public static readonly DependencyProperty MinTranslateYProperty =
DependencyProperty.Register("MinTranslateY",
typeof(double), typeof(PanView), new PropertyMetadata(null));
public double MaxTranslateY
{
get { return (double)GetValue(MaxTranslateYProperty); }
set { SetValue(MaxTranslateYProperty, value); }
}
public static readonly DependencyProperty MaxTranslateYProperty =
DependencyProperty.Register("MaxTranslateY",
typeof(double), typeof(PanView), new PropertyMetadata(null));
}
}
The code above is the entirety of the PanView custom control.
We construct the TransformGroup
to consist of the two transforms, previousTransformations
and currentTransformation
.
When starting a manipulation, we make sure to copy (flatten) the TransformGroup
’s end result (Value
) into _previousTransformations
. We also reset _currentTransformation
. At this point, the TransformGroups
’ end result should be the same.
We record the center point of the manipulation at the start of the manipulation. I’ve seen some code that places this assignment in the ManipulationDelta
handler. As far as I can tell on my Samsung Slate, the end result of that variation is very shaky and unreliable performance.
For each ManipulationDelta
, we simply update currentTransformation
.
That’s it!