ailon's DevBlog: Development related stuff in my life

Obstruction Detection in Silverlight for Windows Phone

4/3/2011 1:12:14 PM

image

AdDuplex Silverlight control is used quite often as a fallback for other networks. As a result it could be obstructed by the other network’s control at times. Problem is that in general case we can’t rely on developers always collapsing AdDuplex control when it’s not visible or informing it about this state in any way.

The other problem is that some of the other network controls (namely Microsoft’s pubCenter) don’t collapse when they have no ad to show, but rather either collapse some internal elements or just make them transparent. This allows underlying elements to show through but they still don’t get the touch events.

So I decided to solve this programmatically. It sounded really easy, but as usual there are some small things that stand in the way.

Meet ObstructionDetector

I’ve created a class with some helper methods that could be useful both in the above scenario and probably some other. You can find it in full at the end of this post.

The “title” method of this class is IsElementObstructed:

public static bool IsElementObstructed(FrameworkElement target)
{
var page = GetPage(target);
var frame = GetFrame(target);
var transform = target.TransformToVisual(frame);
var rect = TransformBoundsToPortrait(transform.TransformBounds(new Rect(0, 0, target.ActualWidth, target.ActualHeight)), page);
var covers = VisualTreeHelper.FindElementsInHostCoordinates(rect, page);

if (covers != null && covers.Count() > 0)
{
return !IsElementChildOf(covers.First(), target);
}

return false;
}

You just pass an element in question to it and it returns true if there’s something in the way of user’s eyes or fingers in front of this element.

The key technique is to use VisualTreeHelper.FindElementsInHostCoordinates() method. It returns elements residing in the same rectangle on the screen as our target. The elements are returned in the descending z-index order. So what’s left to do is to check if the first element in returned collection belongs to (is a child of) our target. If so, then there’s nothing in the way.

A few gocthas

Everything is portrait to VisualTreeHelper.FindElementsInHostCoordinates()

I’ve spent quite some time trying to figure out why the code worked fine in Portrait pages but didn’t work in Landscape. Turns out FindElementsInHostCoordinates() considers everything as portrait oriented even when other parts operate correctly. Apparently this is “by design”. So we need to adjust to this “feature”. For that I’ve added TransformBoundsToPortrait() method.

Get PhoneApplicationPage by going up the visual tree

It’s tempting to use Application.Current.RootVisual and assume it’s child is our PhoneApplicationPage, but it’s not the case in some cases (like page transitions in Silverlight Toolkit). So it’s safer to just go up the visual tree until we find the page.

Account for SystemTray

We need to create a transform based on PhoneApplicationFrame (not Page) since transforming from PhoneApplicationPage doesn’t include optional SystemTray and can result in offset in the results.

Known Issues

  • VisualTreeHelper.FindElementsInHostCoordinates() doesn’t return elements with IsHitTestVisible set to false. So if someone creates a cover for your control and set this property to false it won’t be detected. Any ideas?

Feedback

I know this code is far from perfect, but it’s what I’ve got so far. I would really appreciated your feedback on how to improve it. The class is too small for a separate OS project or something like that, but I’d be happy if someone included it in some WP7 toolkit and improved on it.

The code

using System.Windows;
using System.Windows.Media;
using System.Linq;
using Microsoft.Phone.Controls;
 
namespace Ailon.Phone.Tools
{
    public static class ObstructionDetector
    {
        public static PhoneApplicationPage GetPage(DependencyObject target)
        {
            var parent = VisualTreeHelper.GetParent(target);
            while (parent != null && !(parent is PhoneApplicationPage))
            {
                parent = VisualTreeHelper.GetParent(parent);
            }
 
            return (parent as PhoneApplicationPage);
        }
 
        public static PhoneApplicationFrame GetFrame(DependencyObject target)
        {
            var parent = VisualTreeHelper.GetParent(target);
            while (parent != null && !(parent is PhoneApplicationFrame))
            {
                parent = VisualTreeHelper.GetParent(parent);
            }
 
            return (parent as PhoneApplicationFrame);
        }
 
        public static Rect TransformBoundsToPortrait(Rect bounds, PhoneApplicationPage page)
        {
            if ((page.Orientation & PageOrientation.Portrait) > 0)
            {
                return bounds;
            }
            else
            {
                Rect result;
 
                if (page.Orientation == PageOrientation.LandscapeLeft)
                {
                    result = new Rect(bounds.Y, bounds.X, bounds.Height, bounds.Width);
                }
                else
                {
                    result = new Rect(bounds.Y, 800 - bounds.X - bounds.Width, bounds.Height, bounds.Width);
                }
 
                return result;
            }
        }
 
        public static bool IsElementObstructed(FrameworkElement target)
        {
            var page = GetPage(target);
            var frame = GetFrame(target);
            var transform = target.TransformToVisual(frame);
            var rect = TransformBoundsToPortrait(transform.TransformBounds(new Rect(0, 0, target.ActualWidth, target.ActualHeight)), page);
            var covers = VisualTreeHelper.FindElementsInHostCoordinates(rect, page);
 
            if (covers != null && covers.Count() > 0)
            {
                return !IsElementChildOf(covers.First(), target);
            }
 
            return false;
        }
 
        public static bool IsElementChildOf(DependencyObject element, DependencyObject parent)
        {
            var current = element;
            while (current != null)
            {
                if (current == parent)
                {
                    return true;
                }
 
                current = VisualTreeHelper.GetParent(current);
            }
 
            return false;
        }
 
    }
}

Tags: ,

blog comments powered by Disqus
Copyright © 2003 - 2018 Alan Mendelevich
Powered by BlogEngine.NET 2.5.0.6