forty3degrees

Windows Phone Development

Archive for the ‘Windows Phone’ Category

Creating a swipable ContentControl

leave a comment »

One of the new features in the latest release of feed me was the ability to swipe from one article to the next. Once I had got the HTML parsing finished so the article was actually displayed properly the next job was to create a ‘swipable’ container for the article. I needed to create a reusable container so that YouTube video feeds could support the swipe gesture as well (these are shown using a seperate UserControl).

After using the Windows Phone Toolkit GestureListener in version 1.7 for the first time I thought this would be a good exercise. So I stayed away from Google – and Bing for that matter 😉
This basically means that there may be much better solutions out there.

The first step is to create a class deriving from ContentControl and add two dependency properties CanSwipeLeft and CanSwipeRight. We won’t add support for vertical swiping in this version. In a future version I will implement an animated vertical swipe to go to the next or previous page of a block of text, but that’s a whole new blog post as it gets into finding the start and end of each page in the text.
We also need to add two events SwipeLeft and SwipeRight to let the containing page know when to update the content of the control. We could, of course, implement a single event (Swipe) and add an EventArgs class to indicate the direction, but that’s just a question of style – of which I don’t have much 🙂

So now we have a class that looks like this:

    public class SwipeContentControl : ContentControl
    {
        public event EventHandler SwipeLeft;
        protected virtual void OnSwipeLeft()
        {
            if (this.SwipeLeft != null)
            {
                this.SwipeLeft(this, new EventArgs());
            }
        }

        public event EventHandler SwipeRight;
        protected virtual void OnSwipeRight()
        {
            if (this.SwipeRight != null)
            {
                this.SwipeRight(this, new EventArgs());
            }
        }

        public static readonly DependencyProperty CanSwipeLeftProperty =
            DependencyProperty.Register("CanSwipeLeft", typeof(bool), 
            typeof(SwipeContentControl), new PropertyMetadata(false));
        public bool CanSwipeLeft
        {
            get { return (bool)GetValue(CanSwipeLeftProperty); }
            set { SetValue(CanSwipeLeftProperty, value); }
        }

        public static readonly DependencyProperty CanSwipeRightProperty =
            DependencyProperty.Register("CanSwipeRight", typeof(bool), 
            typeof(SwipeContentControl), new PropertyMetadata(false));
        public bool CanSwipeRight
        {
            get { return (bool)GetValue(CanSwipeRightProperty); }
            set { SetValue(CanSwipeRightProperty, value); }
        }

        public SwipeContentControl()
        {
            this.DefaultStyleKey = typeof(ContentControl);
        }
    }

Notice that the DefaultStyleKey is set to the ContentControl type, this is because we do not need a custom style for this control.

Now that we have the class set up, we need to handle a horizontal swipe. To do this we use the DragDelta and DragCompleted events of the GestureListener. DragDelta fires repeatedly when the user drags a finger across the screen. We use this event to alter the RenderTransform of the control so it follows the users finger. DragCompleted is where we decide whether the drag was far enough for it to be classified as a swipe.

The first thing to do is to set the controls RenderTransform to a new instance of a TranslateTransform. This can then be manipulated in the event handlers. Then we attach event handlers to the GestureListener events. Our constructor now looks like this:

    public SwipeContentControl()
    {
        this.RenderTransform = new TranslateTransform();

        this.DefaultStyleKey = typeof(ContentControl);

        GestureListener listener = GestureService.GetGestureListener(this);
        listener.DragDelta += DragDelta;
        listener.DragCompleted += DragCompleted;
    }

I tried to store the transform in a private variable but found that after an animation the variable no longer pointed to the same object as the RenderTransform member – maybe I just needed some more coffee…

Now we can finally get down to business. The DragDelta event handler is pretty simple. We just need to use the DragDeltaGestureEventArgs to set the X and Y properties on the TranslateTransform. I added some code here to make sure the movement was a minimum of 15 units so that the control didn’t move horizontally when the user was scrolling vertically through the article. As the DragDeltaGestureEventArgs.HorizontalChange member is not the total drag distance but rather the delta [hence the name ;)] since the last time the event fired, this also means that the swipe must have a certain amount of speed before the control starts moving (15.0 units in one event cycle). After the control is moving then the minimum distance check is skipped, this makes sure the control ‘sticks’ to the users finger regardless of the amount of movement.

Here’s the code for the DragDelta event handler:

    private void DragDelta(object sender, Microsoft.Phone.Controls.DragDeltaGestureEventArgs e)
    {
        if (((e.HorizontalChange  0) && this.CanSwipeRight))
        {
            TranslateTransform transform = (TranslateTransform)this.RenderTransform;
                
            /* Check if the control is being dragged */
            if (transform.X == 0)
            {
                if ((e.HorizontalChange > -15.0d) && (e.HorizontalChange < 15.0d))
                {
                    /* The change is too small to start moving the control */
                    return;
                }
            }
            transform.X += e.HorizontalChange;
        }
    }

Now we get to the actual animation. The swipe animates the TranslateTransform so that the entire control is shown 20 units to the appropriate side of the original position. At the same time the control is faded out. After this the control is moved to the other side and the animations are performed in reverse. The 20.0 unit spacing is just to make sure the control goes off the screen (I noticed the control flickered before moving back into place when running on NoDo)
In order to perform the animations I use some of my standard extension methods that are used throughout all my Silverlight, WPF and WP7 projects. These methods animate a single property of a UIElement. These methods (Show, Hide and Animate) all delegate to a single overload:

    public static void Show(this UIElement element, int duration)
    {
        VisualExtensions.Show(element, duration, null);
    }
    
    ... more overloads...
    
    public static void Animate(this UIElement element, string property, int duration, double value, Action callback)
    {
        Storyboard storyBoard = new Storyboard();

        DoubleAnimation animation = new DoubleAnimation();
        animation.To = value;
        animation.Duration = new Duration(TimeSpan.FromMilliseconds(duration));

        if (callback != null)
        {
            animation.Completed += delegate { callback(element); };
        }

        Storyboard.SetTarget(animation, element);
        Storyboard.SetTargetProperty(animation, new PropertyPath(property));

        storyBoard.Children.Add(animation);
        storyBoard.Begin();
    }

In this scenario it would probably be more efficient if I modified these methods to accept two properties, so that the animations were performed in a single Storyboard. I may add that to my to-do list, but I have never run into performance problems when calling the methods silmutaneously as we will in the code below.

Now for the last piece of the puzzle – the DragCompleted event handler. The code I had here for Mango was pretty simple. After testing on NoDo, however, I realised I needed to slow things down a bit. Hence the ’empty’ animation (animation from X to X over 100ms). Here’s the code with 120.0 units being the magic number required for a swipe:

    private void DragCompleted(object sender, Microsoft.Phone.Controls.DragCompletedGestureEventArgs e)
    {
        TranslateTransform transform = (TranslateTransform)this.RenderTransform;
        if ((transform.X > 120.0d) && this.CanSwipeRight)
        {
            /* Calculate the offset */
            double offset = this.ActualWidth + 20.0d;

            /* Fade out and shift to the right */
            this.Animate("Opacity", 100, 0.0d);
            this.Animate("(RenderTransform).(TranslateTransform.X)", 100, offset, (element) =>
            {
                /* Raise the event so the containing page can set the new content */
                this.OnSwipeRight();

                /* Delay 100ms with an 'empty' animation for a smooth animation */
                this.Animate("(RenderTransform).(TranslateTransform.X)", 100, offset, (element2) =>
                {

                    /* Move the control to the left hand side - we need to retrieve the 
                     * transform again here as it no longer points to the same object. I
                     * haven't figured out if there is a reason for this or if it is a bug. */
                    transform = (TranslateTransform)this.RenderTransform;
                    transform .X = -offset;
                                      
                    /* Fade in and slide in from the left */
                    this.Animate("Opacity", 100, 1.0d);
                    this.Animate("(RenderTransform).(TranslateTransform.X)", 150, 0.0d);                   
                });
            });
        }
        else if ((transform.X < -120.0d) && this.CanSwipeLeft)
        {
            /* Same as above but in reverse */
            double offset = this.ActualWidth + 20.0d;
            this.Animate("Opacity", 100, 0.0d);
            this.Animate("(RenderTransform).(TranslateTransform.X)", 100, -offset, (element) =>
            {
                this.OnSwipeLeft();
                /* Delay 100ms for a smooth animation */
                this.Animate("(RenderTransform).(TranslateTransform.X)", 100, -offset, (element2) =>
                {                    
                    transform = (TranslateTransform)this.RenderTransform;
                    transform .X = offset;
                                      
                    this.Animate("Opacity", 100, 1.0d);
                    this.Animate("(RenderTransform).(TranslateTransform.X)", 150, 0.0d);
                });
            });
        }
        else if (transform.X != 0.0d)
        {
            this.Animate("(RenderTransform).(TranslateTransform.X)", 200, 0.0d);
        }      
    }

Now all that’s left is to add this control to your UI, setting CanSwipeLeft and CanSwipeRight and listening for the swipe events.
You can download a demo project that does just that here (I have included the binaries in case you don’t have the toolkit installed).

Written by calum

July 19, 2011 at 7:01 pm

Posted in Apps, feed me, Windows Phone

Tagged with , , , ,

Breaking Change in the Latest Mango Build

with 2 comments

Recently I received an error report from feed me that had me stumped. I couldn’t reproduce the problem no matter how hard I tried.

As it turned out the report came from an internal beta tester for Mango who was running the latest build from last week. Thankfully he was willing to do some ‘remote debugging’ (me sending him a xap with extra logging) so I could locate the cause of the problem.

The error was caused by a change in the behaviour of the WebBrowser control. This change is only in the lastest build and not in the Mango beta recently distributed to developers and reviewers. In previous builds when calling the WebBrowser.NavigateToString(string) method the URI passed in the LoadCompleted event was null. I had the following code in the article page in feed me:

private void uxWebBrowser_LoadCompleted(object sender, NavigationEventArgs e)
{
    if (e.Uri == null)
    {
        ...
    }
}

In the latest build the Uri member passed is an empty Uri, meaning that I needed to check against null or an empty Uri:

if ((e.Uri == null) || (e.Uri.OriginalString == string.Empty))
{
    ...
}

I thought I should post this just in case anyone else out there relies on this behaviour. I’m not sure if this will make it into the final release of Mango, but if it does it has the potential to break a few apps. I was lucky enough to have two very understanding (and patient) people report this error, allowing me to fix it in feed me V1.7.

Written by calum

July 5, 2011 at 12:36 pm

Posted in Windows Phone

Tagged with ,