Constraining manipulations

Touch screens are cool. Being able to drag and manipulate pictures and objects on the screen is fun, and the Xaml manipulation system makes it easy to drag and zoom controls by touch. Touch down, zip your finger across the screen, and watch the control slide away.

But what if we want to apply more control to the element? A recurring question is how to allow the control to pan and zoom within specific boundaries. If you don’t put any limits it is easy for the user to size or move the control beyond the ability to easily recover it.

Happily, the manipulation events provide the base information to the app but don’t automatically apply the results. We can constrain or modify the results in code.

This can allow for straightforward results like I demonstrate here: limiting a rectangle’s pan, rotate, and zoom to within a bounding box and detecting when an element is over another element to change its color. It could also be used for other effects: you can multiply the effects to make them bigger or slower, have them affect other parts of the screen, convert position to acceleration, or whatever else will make your app more awesome. The most common request I've heard is to limit dragging an image to on-screen. Another interesting case was to create a "touch pad" in the corner of the screen to allow manipulating objects elsewhere. The color mixing code path could be used in a game.

Example image of rectangle's movement stopped at the edge of a containing area:

Here’s some example code. It uses axis aligned bounding boxes even if the rectangle is rotated. A refinement for the hit testing would be to hit test over the rotated rectangle.

//
// IntersectElements: compute if two elements intersect.
// If “contains” is true then the inner element must be completely inside
// the outer element. Otherwise any intersection will return true
//
IntersectElements(FrameworkElement inner, FrameworkElement outer, bool contains)
{
            GeneralTransform testTransform = inner.TransformToVisual(outer);

            Rect boundsInner = new Rect(0, 0, inner.ActualWidth, inner.ActualHeight);
            Rect bboxOuter = new Rect(0, 0, outer.ActualWidth, outer.ActualHeight);

            Rect bboxInner = testTransform.TransformBounds(boundsInner);
           
            if (contains)
            {
                return bboxInner.X > bboxOuter.X &&
                       bboxInner.Y > bboxOuter.Y &&
                       bboxInner.Right < bboxOuter.Right &&
                       bboxInner.Bottom < bboxOuter.Bottom;              
            }
            else
            {
                bboxOuter.Intersect(bboxInner);
                return !bboxOuter.IsEmpty;
            }
}

//
// Arbitrary limits to ensure the element stays at a workable size
//
double minScale = 0.5;
double maxScale = 3.0;
CompositeTransform savedTransform = new CompositeTransform();

//
// For our purposes everything interesting is done in ManipulatinDelta
//    
void TestRectangle_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    FrameworkElement elem = sender as FrameworkElement;
    CompositeTransform transform = elem.RenderTransform as CompositeTransform;
   
    // Copy the original transform so we can undo if the results are out of our range
    CopyTransform(transform,savedTransform);

    // Apply the translation, scale, and rotations
    transform.TranslateX += e.Delta.Translation.X;
    transform.TranslateY += e.Delta.Translation.Y;
    transform.ScaleX *= e.Delta.Scale;     
    transform.ScaleY *= e.Delta.Scale;
    transform.Rotation += e.Delta.Rotation;

    // If we’re set to ConstrainScale then make sure we stay within our scale limits
    if (ConstrainScale)
    {
        if (transform.ScaleX < minScale) transform.ScaleX = minScale;
        if (transform.ScaleY < minScale) transform.ScaleY = minScale;
         if (transform.ScaleX > maxScale) transform.ScaleX = maxScale;
        if (transform.ScaleY > maxScale) transform.ScaleY = maxScale;
    }   

    // If we’re set to ConstrainTranslate then keep the rectangle inside the boundary
    if (ConstrainTranslate)
    {
         if (elem != null)
        {
            if (!IntersectElements(elem, this.BoundaryRectangle,true))
            {
                // We're outside the boundary so undo the Transforms
                CopyTransform(savedTransform, transform);
            }
        }
    }

    // If we’re set to MixColors then swap the colors if it’s over the TargetRectangle
    if (MixColors)
    {
        Rectangle rect = sender as Rectangle;
        if (rect != null)
        {
            if (IntersectElements(rect, this.TargetRectangle, false))
             {
                // We're over the yellow rectangle, so change our color
                rect.Fill = Resources["OverBrush"] as Brush;
            }
            else
            {
                 rect.Fill = Resources["NormalBrush"] as Brush;
             }
        }
    }

private void CopyTransform(CompositeTransform orig, CompositeTransform copy)
{
    copy.TranslateX = orig.TranslateX;
    copy.TranslateY = orig.TranslateY;
    copy.ScaleX = orig.ScaleX;
    copy.ScaleY = orig.ScaleY;
    copy.Rotation = orig.Rotation;
}

I hope that this article gives you some ideas for how to use manipulation events to make your app zing!

Don’t forget to follow the Windows Store Developer Solutions team on Twitter @wsdevsol. Comments are welcome!

- Rob

ConstrainedManip.zip