With the latest GDR 3 Update, Windows Phone 8 now support new 6″ devices like the Nokia Lumia 1520 and Nokia Lumia 1320. As always your existing apps will continue to work on these new devices without any change in the code. However, you can take additional steps to use this new screen real estate more efficiently.

There are a couple of posts on the web which tell how to modify your app for the new screen size. Have a look at them if that’s your thing.

I would like to show you a simper way of using all the above resource in you app.

1. Device Properties

Get the device properties using the DeviceExtendedProperties class.

object temp;

// Get PhysicalScreenResolution
if (!DeviceExtendedProperties.TryGetValue("PhysicalScreenResolution", out temp))
    return false;
screenResolution = (Size)temp;

if (!DeviceExtendedProperties.TryGetValue("RawDpiX", out temp) || (double)temp == 0d)
    return false;

dpi = (double)temp;

2. Compute scale factor

We now then use the above properties to determine the actual scaling factor. The simplified logic for scaling factor is
factor  = actual screen resolution / reported app width / aspect ratio.

However, in reality we need to apply certain calculation depending on physical device size and screen resolution. This is because not all 720p and 1080p devices have the same scaling ratio and dpi.

Here is the code adapted from the DisplayInformationEx class on msdn:

var width = App.Current.Host.Content.ActualWidth;
var physicalSize = new Size(screenResolution.Width / dpi, screenResolution.Height / dpi);
var scale = Math.Max(1, physicalSize.Width / DisplayConstants.BaselineWidthInInches);
var idealViewWidth = Math.Min(DisplayConstants.BaselineWidthInViewPixels * scale, screenResolution.Width);
var idealScale = screenResolution.Width / idealViewWidth;
RawPixelsPerViewPixel = idealScale.NudgeToClosestPoint(1); //bucketizedScale
var ViewResolution = new Size(screenResolution.Width / RawPixelsPerViewPixel, screenResolution.Height / RawPixelsPerViewPixel);

Scale = Math.Max(1, Math.Min(ViewResolution.Width / Application.Current.Host.Content.ActualWidth, ViewResolution.Height / Application.Current.Host.Content.ActualHeight));

If this logic is too complicated, well it is. Thanks to Microsoft for doing a wonderful job!

3. ZoomBox control

The next part in our change is to wrap any control we want to scale in a zoom box. This control is also adapted from the msdn sample without the frills

    [TemplatePart(Name = ZoomBox.ContentHolderPartName, Type = typeof(UIElement))]
    public class ZoomBox : ContentControl
    {
        public const string ContentHolderPartName = "contentHolder";

        readonly ScaleTransform transform = new ScaleTransform();
        UIElement contentHolder;

        public static readonly DependencyProperty ZoomFactorProperty = DependencyProperty.Register("ZoomFactor", typeof(double), typeof(ZoomBox), new PropertyMetadata(1.0, OnZoomFactorPropertyChanged));
        public double ZoomFactor
        {
            get { return (double)GetValue(ZoomFactorProperty); }
            set { SetValue(ZoomFactorProperty, value); }
        }

        static void OnZoomFactorPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            var control = (ZoomBox)source;
            if (double.IsNaN((double)e.NewValue) || (double)e.NewValue <= 0)
            {
                control.ZoomFactor = (double)e.OldValue;
                throw new ArgumentOutOfRangeException("ZoomFactor", "must be a positive number");
            }
            control.InvalidateMeasure();
        }

        public static ZoomBox GetForElement(UIElement element)
        {
            var currentElement = element;
            while (currentElement != null)
            {
                if (currentElement is ZoomBox)
                    return currentElement as ZoomBox;

                currentElement = VisualTreeHelper.GetParent(currentElement) as UIElement;
            }

            return null;
        }

        public ZoomBox()
        {
            transform = new ScaleTransform { ScaleX = 1, ScaleY = 1 };
            DefaultStyleKey = typeof(ZoomBox);
        }

        public override void OnApplyTemplate()
        {
            if (contentHolder != null)
                contentHolder.RenderTransform = null;
            contentHolder = null;

            base.OnApplyTemplate();

            var temp = GetTemplateChild(ContentHolderPartName) as UIElement;
            if (temp == null)
                return;

            contentHolder = temp;
            if (ZoomFactor != 1)
                contentHolder.RenderTransform = transform;
        }

        protected override Size ArrangeOverride(Size finalSizeInHostCoordinates)
        {
            var effectiveZoomFactor = ZoomFactor;
            var finalSizeInViewCoordinates = finalSizeInHostCoordinates.Scale(effectiveZoomFactor);
            var requiredSizeInViewCoordinates = base.ArrangeOverride(finalSizeInViewCoordinates);
            var requiredSizeInHostCoordinates = requiredSizeInViewCoordinates.Scale(1 / effectiveZoomFactor);

            if (effectiveZoomFactor != 1)
            {
                transform.ScaleX = transform.ScaleY = 1 / effectiveZoomFactor;
                contentHolder.RenderTransform = transform;
            }
            else
                contentHolder.RenderTransform = null;

            return requiredSizeInHostCoordinates;
        }

        protected override Size MeasureOverride(Size availableSizeInHostCoordinates)
        {
            var effectiveZoomFactor = ZoomFactor;
            var availableSizeInViewCoordinates = availableSizeInHostCoordinates.Scale(effectiveZoomFactor);
            var desiredSizeInViewCoordinates = base.MeasureOverride(availableSizeInViewCoordinates);
            var desiredSizeInHostCoordinates = desiredSizeInViewCoordinates.Scale(1 / effectiveZoomFactor);

            return desiredSizeInHostCoordinates;
        }
    }

You supply the scaling factor computed earlier to this control.

4. Usage

You use these controls as below:

<!-- App Resources section -->
<local:SizeHelper x:Key="SizeHelper" />

<!-- Page Content section -->
<controls:ScaledView
                   ZoomFactor="{Binding Scale,Source={StaticResource SizeHelper}}" >
    <Grid><!--You controls go here--></Grid>
</controls:ScaledView>

The ScaledView control is a wrapper to provide the scaling factor to the entire app without the need to compute it again. You can find the entire source code including the ScaledView control at the end of this post.

Sample

The general usage of this would be on lists and grids to use the extra space. I have implemented this in my app Maps+. here are the screenshots using WXGA and 1080p devices. Do download and rate my app.

If you observe closely, originally the 1080p screens appear enlarged with a little more space towards the bottom. After scaling the sizes are comparable with the WXGA screens and provide more space of results.

Download Maps+

(Note: I have just published the app update and will take some time to appear in the store. The original app is available.)

Conclusion

Updating your apps for ne large screen devices is almost simple. (it could have been simpler if MS wanted it to be). Adding these few little things will help provide a better user experience to users. If you have any questions, let me know.

Entire source code is below:

using Microsoft.Phone.Info;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace ScalingUI.Controls
{

/*
Put this in the App.xaml Resources section:
<local:SizeHelper x:Key="SizeHelper" />

Put this inside your page:
<controls:ScaledView
                   ZoomFactor="{Binding Scale,Source={StaticResource SizeHelper}}" >
    <Grid><!--You controls go here--></Grid>
</controls:ScaledView>

*/
    public static class DisplayConstants
    {
        public const double AspectRatio16To9 = 16.0 / 9.0;
        public const double AspectRatio15To9 = 15.0 / 9.0;

        public static readonly double DiagonalToWidthRatio16To9 = 9.0 / Math.Sqrt(Math.Pow(16, 2) + Math.Pow(9, 2));
        public static readonly double DiagonalToWidthRatio15To9 = 9.0 / Math.Sqrt(Math.Pow(15, 2) + Math.Pow(9, 2));

        public const double BaselineDiagonalInInches15To9HighRes = 4.5; // Lumia 920
        public const double BaselineDiagonalInInches15To9LoRes = 4.0; // Lumia 520
        public const double BaselineDiagonalInInches16To9 = 4.3; // HTC 8X

        // We use 15:9 aspect ratio, 4.5" diagonal with 480px view resolution as the baseline for scaling / relayout
        //  * Any size less than 4.5" will get scaled down
        //  * Any size greater than 4.5" will get more layout space
        // Note that 16:9 displays are skinnier than 15:9, so the cutover isn't exactly 4.5" for them
        internal static readonly double BaselineWidthInInches = BaselineDiagonalInInches15To9HighRes * DiagonalToWidthRatio15To9;
        internal const int BaselineWidthInViewPixels = 480;
    }

    public class SizeHelper : DependencyObject
    {
        public SizeHelper()
        {
            try
            {
                //Not for WVGA and WXGA.
                if (App.Current.Host.Content.ScaleFactor != 150) return;
                Size screenResolution;
                double dpi;
                if (!GetDeviceProperties(out screenResolution, out dpi)) return;

                //Calculation taken from DisplayInformationEx
                //http://blogs.windows.com/windows_phone/b/wpdev/archive/2013/11/22/taking-advantage-of-large-screen-windows-phones.aspx
                var width = App.Current.Host.Content.ActualWidth;
                var physicalSize = new Size(screenResolution.Width / dpi, screenResolution.Height / dpi);
                var scale = Math.Max(1, physicalSize.Width / DisplayConstants.BaselineWidthInInches);
                var idealViewWidth = Math.Min(DisplayConstants.BaselineWidthInViewPixels * scale, screenResolution.Width);
                var idealScale = screenResolution.Width / idealViewWidth;
                RawPixelsPerViewPixel = idealScale.NudgeToClosestPoint(1); //bucketizedScale
                var ViewResolution = new Size(screenResolution.Width / RawPixelsPerViewPixel, screenResolution.Height / RawPixelsPerViewPixel);

                Scale = Math.Max(1, Math.Min(ViewResolution.Width / Application.Current.Host.Content.ActualWidth, ViewResolution.Height / Application.Current.Host.Content.ActualHeight));

            }
            catch { }
        }

        private static bool GetDeviceProperties(out Size screenResolution, out double dpi)
        {
            screenResolution = new Size();
            dpi = 0;

            object temp;

            // Get PhysicalScreenResolution
            if (!DeviceExtendedProperties.TryGetValue("PhysicalScreenResolution", out temp))
                return false;
            screenResolution = (Size)temp;

            // Get RawDpi
#if DEBUG

            // Raw Dpi is not be available on the emulator.
            /*
             * Known RAW DPI for some devices
             * "HTC 8S or Lumia 520"        233.24  WVGA
             * "Lumia 822"                  222.13  WVGA
             * "HTC 8X"                     341.54  720P
             * "Lumia 920"                  331.72  WXGA
             * "Samsung ATIV S"             305.96  720P
             * "Generic 5\" 720p"           293.72  720P
             * "Generic 5.5\" Full HD",     400.53  1080P
             * "Lumia 1320",                244.77  720P
             * "Lumia 1520",                367.15  1080P
             * "Generic 6.5\" Full HD",     338.91  1080P
             */
            var w = screenResolution.Width;
            switch ((int)w)
            {
                // You can change thses values as per your test devices
                case 720: temp = 244D; break;
                case 1080: temp = 368D; break;
                default:
                    break;
            }
#else
            if (!DeviceExtendedProperties.TryGetValue("RawDpiX", out temp) || (double)temp == 0d)
                return false;

#endif
            dpi = (double)temp;

            return true;
        }

        public double Scale
        {
            get { return (double)GetValue(ScaleProperty); }
            set { SetValue(ScaleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Scale.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ScaleProperty =
            DependencyProperty.Register("Scale", typeof(double), typeof(SizeHelper), new PropertyMetadata(1D));

        public double RawPixelsPerViewPixel { get; set; }
    }

    // Source://http://blogs.windows.com/windows_phone/b/wpdev/archive/2013/11/22/taking-advantage-of-large-screen-windows-phones.aspx
    [TemplatePart(Name = ZoomBox.ContentHolderPartName, Type = typeof(UIElement))]
    public class ZoomBox : ContentControl
    {
        public const string ContentHolderPartName = "contentHolder";

        readonly ScaleTransform transform = new ScaleTransform();
        UIElement contentHolder;

        public static readonly DependencyProperty ZoomFactorProperty = DependencyProperty.Register("ZoomFactor", typeof(double), typeof(ZoomBox), new PropertyMetadata(1.0, OnZoomFactorPropertyChanged));
        public double ZoomFactor
        {
            get { return (double)GetValue(ZoomFactorProperty); }
            set { SetValue(ZoomFactorProperty, value); }
        }

        static void OnZoomFactorPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            var control = (ZoomBox)source;
            if (double.IsNaN((double)e.NewValue) || (double)e.NewValue <= 0)
            {
                control.ZoomFactor = (double)e.OldValue;
                throw new ArgumentOutOfRangeException("ZoomFactor", "must be a positive number");
            }
            control.InvalidateMeasure();
        }

        public static ZoomBox GetForElement(UIElement element)
        {
            var currentElement = element;
            while (currentElement != null)
            {
                if (currentElement is ZoomBox)
                    return currentElement as ZoomBox;

                currentElement = VisualTreeHelper.GetParent(currentElement) as UIElement;
            }

            return null;
        }

        public ZoomBox()
        {
            transform = new ScaleTransform { ScaleX = 1, ScaleY = 1 };
            DefaultStyleKey = typeof(ZoomBox);
        }

        public override void OnApplyTemplate()
        {
            if (contentHolder != null)
                contentHolder.RenderTransform = null;
            contentHolder = null;

            base.OnApplyTemplate();

            var temp = GetTemplateChild(ContentHolderPartName) as UIElement;
            if (temp == null)
                return;

            contentHolder = temp;
            if (ZoomFactor != 1)
                contentHolder.RenderTransform = transform;
        }

        protected override Size ArrangeOverride(Size finalSizeInHostCoordinates)
        {
            var effectiveZoomFactor = ZoomFactor;
            var finalSizeInViewCoordinates = finalSizeInHostCoordinates.Scale(effectiveZoomFactor);
            var requiredSizeInViewCoordinates = base.ArrangeOverride(finalSizeInViewCoordinates);
            var requiredSizeInHostCoordinates = requiredSizeInViewCoordinates.Scale(1 / effectiveZoomFactor);

            if (effectiveZoomFactor != 1)
            {
                transform.ScaleX = transform.ScaleY = 1 / effectiveZoomFactor;
                contentHolder.RenderTransform = transform;
            }
            else
                contentHolder.RenderTransform = null;

#if LOG_MEASURE_ARRANGE_OVERRIDE
      Debug.WriteLine("*** ArrangeOverride ***");
      Debug.WriteLine("  input size (host coordinates):    {0:#.} x {1:#.}", finalSizeInHostCoordinates.Width, finalSizeInHostCoordinates.Height);
      Debug.WriteLine("  converted (view coordinates):     {0:#.} x {1:#.}", finalSizeInViewCoordinates.Width, finalSizeInViewCoordinates.Height);
      Debug.WriteLine("  required size (view coordinates): {0:#} x {1:#.}", requiredSizeInViewCoordinates.Width, requiredSizeInViewCoordinates.Height);
      Debug.WriteLine("  scaling factor:                   {0:#.##}", ZoomFactor);
      Debug.WriteLine("  returning (host coordinates):     {0:#.} x {1:#.}", requiredSizeInHostCoordinates.Width, requiredSizeInHostCoordinates.Height);
#endif

            return requiredSizeInHostCoordinates;
        }

        protected override Size MeasureOverride(Size availableSizeInHostCoordinates)
        {
            var effectiveZoomFactor = ZoomFactor;
            var availableSizeInViewCoordinates = availableSizeInHostCoordinates.Scale(effectiveZoomFactor);
            var desiredSizeInViewCoordinates = base.MeasureOverride(availableSizeInViewCoordinates);
            var desiredSizeInHostCoordinates = desiredSizeInViewCoordinates.Scale(1 / effectiveZoomFactor);

#if LOG_MEASURE_ARRANGE_OVERRIDE
      Debug.WriteLine("*** MeasureOverride ***");
      Debug.WriteLine("  input size (host coordinates):    {0:#.} x {1:#.}", availableSizeInHostCoordinates.Width, availableSizeInHostCoordinates.Height);
      Debug.WriteLine("  converted (view coordinates):     {0:#.} x {1:#.}", availableSizeInViewCoordinates.Width, availableSizeInViewCoordinates.Height);
      Debug.WriteLine("  required size (view coordinates): {0:#} x {1:#.}", desiredSizeInViewCoordinates.Width, desiredSizeInViewCoordinates.Height);
      Debug.WriteLine("  scaling factor:                   {0:#.##}", ZoomFactor);
      Debug.WriteLine("  returning (host coordinates):     {0:#.} x {1:#.}", desiredSizeInHostCoordinates.Width, desiredSizeInHostCoordinates.Height);
#endif

            return desiredSizeInHostCoordinates;
        }
    }

    static class Ex
    {
        public const double Epsilon = 0.001;
        public static double NudgeToClosestPoint(this double currentValue, int nudgeValue)
        {
            var newValue = currentValue * 10 / nudgeValue;
            newValue = Math.Floor(newValue + Epsilon);
            return newValue / 10 * nudgeValue;
        }

        public static Size Scale(this Size size, double scaleFactor)
        {
            Size scaledSize = new Size();
            var h = size.Height;
            scaledSize.Height = Double.IsInfinity(h) ? h : h * scaleFactor;
            var w = size.Width;
            scaledSize.Width = Double.IsInfinity(w) ? w : w * scaleFactor;
            return scaledSize;
        }
    }

}

Advertisements

Share your thoughts

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s