Detecting page orientation change for contentpages

NMackayNMackay GBInsider, University mod
edited February 2017 in Xamarin.Forms

Hi,

I came up with a custom content page to check if the device was rotated, my usage was I wanted to change the listview data template if the device rotated for a dashboard view.

Content page

using System;
using Xamarin.Forms;

namespace Foobar.CustomPages
{
    public class OrientationContentPage : ContentPage
    {
        private double _width;
        private double _height;

        public event EventHandler<PageOrientationEventArgs> OnOrientationChanged = (e, a) => { };

        public OrientationContentPage()
            : base()
        {
            Init();
        }

        private void Init()
        {
            _width = this.Width;
            _height = this.Height;
        }

        protected override void OnSizeAllocated(double width, double height)
        {
            var oldWidth = _width;
            const double sizenotallocated = -1;

            base.OnSizeAllocated(width, height);
            if (Equals(_width, width) && Equals(_height, height)) return;

            _width = width;
            _height = height;

            // ignore if the previous height was size unallocated
            if (Equals(oldWidth, sizenotallocated)) return;

            // Has the device been rotated ?
            if (!Equals(width, oldWidth))
                OnOrientationChanged.Invoke(this,new PageOrientationEventArgs((width < height) ? PageOrientation.Vertical : PageOrientation.Horizontal));
        }
    }
}

Enum

using System;

namespace Foobar.CustomPages
{
    public class PageOrientationEventArgs: EventArgs
    {
        public PageOrientationEventArgs(PageOrientation orientation)
        {
            Orientation = orientation;
        }

        public PageOrientation Orientation { get; }
    }

    public enum PageOrientation
    {
        Horizontal = 0,
        Vertical = 1,
    }
}

Usage

using System;
using System.Diagnostics;
using Foobar.CustomPages;
using Xamarin.Forms.Xaml;

namespace Foobar.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class HomePagePhone : OrientationContentPage
    {
        public HomePagePhone()
        {
            InitializeComponent();
            OnOrientationChanged += DeviceRotated;
        }

        private void DeviceRotated(object s, PageOrientationEventArgs e)
        {

            switch (e.Orientation)
            {
                case PageOrientation.Horizontal:
                    break;
                case PageOrientation.Vertical:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            Debug.WriteLine(e.Orientation.ToString());
        }
    }
}

Seems to work okay on hardware, tested with Forms 2.3.3.180 on iPhone6, IPad, Sansumg S6, S2 Tablet and UWP on a nokia 650 lumia.

Might be useful for someone but would be good if it was added to the Forms framework, I'll add a new idea on the evolution forms if there's interest and post up any issues.

@ClintStLaurent @DavidOrtinau @PierceBoggan

Posts

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    @NMackay
    Thanks for sharing that. Nice to see I'm not the only one who does a base Page type to inherit from, for all my other pages.
    This will fit in the pattern perfectly.
    Thanks!

  • JohnHardmanJohnHardman GBUniversity mod

    I suspect many of us have done the same sort of thing :-)

    This will (correctly) also fire the orientation change event when the app's window is resized (e.g. on UWP desktop) such that which is greater out of width and height changes.

  • NMackayNMackay GBInsider, University mod

    Something like this should really be in the Forms Framework imo.

    Anyway, does the job for me :smile:

  • DavidDancyDavidDancy AUMember ✭✭✭✭

    Interesting. I remember one recent Apple DevCon where they advised developers to move away from explicitly detecting rotations, to only detecting resize events. After all, so the reasoning went, a rotation (as far as the window's content is concerned) is only a size change (unless the device/window is square).

    So if we follow the Apple advice all we actually need is the SizeAllocated event, which we already have. The fact that you've based your rotation detection on it seems to me to indicate that rotation detection isn't really needed since all the information is already available.

    So I'm curious: what use is rotation detection for you? I see your use case, but how does rotation detection help where SizeAllocated doesn't?

  • NMackayNMackay GBInsider, University mod
    edited February 2017

    @DavidDancy

    The main point is the Forms framework doesn't provide a convenient hook/event as it stands to change for example a template when flipping from portrait to landscape, also my scenario doesn't cover actually figuring out what orientation your PCL forms app is in at startup to set the initial template, you need the dependency service (it's documented but again, it would help if it was part of the API). These are small quick wins

    Besides, it's Forms and we can't follow Apple's advice :)

  • JohnHardmanJohnHardman GBUniversity mod

    In my case, I switch between a single page view and a split screen view (similar to master/detail) depending on whether height > width and whether width > the minimum width that the split screen makes sense for. Whilst I could do that without a concept of portrait and landscape, I do still use the concept to drive the first test (height > width). It's a convenience that I've built into my base page class in similar fashion to how @NMackay has done it. Could be done either way. I'm not sure I'd merge it into the main XF codebase though at this point, as it might encourage people to go against Apple's guidance.

  • DavidDancyDavidDancy AUMember ✭✭✭✭

    I just wrote a reply to say I think the SizeAllocated event should be sufficient, but thinking about it I've unconvinced myself so I have to reply differently. :)

    Reasoning: the SizeAllocated callback gets called multiple times, with multiple different size values - including some where Height and Width are set to -1 (unknown). This means that we need an extra "filter" on that callback so we can detect when the size is a) stable and b) different than the previous stable size.

    So I see your rotation event could be a nice way to package up that information to make sure that we only respond when a response is actually needed.

    But I think the real problem is in the fact that Forms calls the SizeAllocated callback before the View's size is stable.

    If Forms were to wait until the size had stable, non-undefined values for both Width and Height, there would be no need to filter anything.

    We could then do as Apple advises and handle both rotation and resize scenarios by simply observing the resize.

  • NMackayNMackay GBInsider, University mod
    edited February 2017

    @DavidDancy

    Good shout, OnSizeAllocated is currently called 2/3 times in iOS tested on iPhone6 and iPad4 hardware (and indicated in the guide) and once in Android and UWP (again, tested on hardware) hence my code workarounds (and in the guidance docs). Since I've added a property to set the orientation based on a platform dependency call to get the orientation at startup, I did research a lot and did see if there was a nice way in iOS and again, so much conflicting information, I'm just going on the official recommendation for the dependency service.

    https://developer.xamarin.com/guides/xamarin-forms/dependency-service/device-orientation/

    Thanks for your feedback & insight on the iOS side.

  • NMackayNMackay GBInsider, University mod
    edited February 2017

    Finished implementing it my app by adding a property to my dependency service to check if the device is in portrait or landscape. Basically when the user flips the device it shows the dashboard widgets in a grid view (landscape) or a linear list (portrait). The dependency check at startup just means the app presents the widgets in the correct way at 1st load.

    using System;
    using Foobar.Common.CustomPages;
    using Foobar.Common.Services;
    using Prism.Navigation;
    using Telerik.XamarinForms.DataControls.ListView;
    using Xamarin.Forms;
    using Xamarin.Forms.Xaml;
    
    namespace Foobar.Views.Phone
    {
        [XamlCompilation(XamlCompilationOptions.Compile)]
        public partial class HomePagePhone : OrientationContentPage, IDestructible
        {
    
            public HomePagePhone(IHandsetCommService handset)
            {
                InitializeComponent();
                OnOrientationChanged += DeviceRotated;
                SetLayout(handset.DeviceOrientation);
            }
    
            public void Destroy()
            {
                ListviewShortcut.Behaviors.Clear();
                OnOrientationChanged -= DeviceRotated;
            }
    
    
            private void DeviceRotated(object s, PageOrientationEventArgs e)
            {
                SetLayout(e.Orientation);
            }
    
            private void SetLayout(PageOrientation orientation)
            {
                switch (orientation)
                {
                    case PageOrientation.Landscape:
                        ListviewShell.LayoutDefinition = new ListViewGridLayout
                        {
                            SpanCount = 2,
                            Orientation = Telerik.XamarinForms.Common.Orientation.Vertical,
                            HorizontalItemSpacing = Device.OnPlatform<double>(4, 4, 4),
                            VerticalItemSpacing = Device.OnPlatform<double>(4, 4, 4),
                            ItemLength = Device.OnPlatform<double>(180, -1, -1)
                        };
                        break;
                    case PageOrientation.Portrait:
                        ListviewShell.LayoutDefinition = new ListViewLinearLayout
                        {
                            VerticalItemSpacing = Device.OnPlatform<double>(4, 4, 1),
                            ItemLength = Device.OnPlatform<double>(150, -1, -1)
                        };
                        break;
                    case PageOrientation.Unknown:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(orientation), orientation, null);
                }
            }
        }
    }
    

    Code behind is fine in this case as it's just about adjusting the presentation layer :smile:

  • DeveshMishraDeveshMishra USMember ✭✭

    @NMackay said:
    Something like this should really be in the Forms Framework imo.

    Anyway, does the job for me :smile:

    Yes obviously this must be a part of Forms Framework.
    Anyways, it does the job for others also :)

  • AegletesAegletes Member

    Thanks a lot for sharing this, I am surprised they haven't implemented anything to do with orientation in the framework.

  • NMackayNMackay GBInsider, University mod

    @DeveshMishra @Aegletes

    There's some new features in the pipeline that will make this easier going forwards.

    https://github.com/xamarin/Xamarin.Forms/labels/a/gestures
    https://github.com/xamarin/Xamarin.Forms/issues/3504

  • CaptainXamtasticCaptainXamtastic GBUniversity ✭✭✭
    edited March 1

    @NMackay Very interesting, I've implemented it with a sensible change:

    I have a base view model that parallels the page base (indeed I have various base layers for different contexts for both page and view models) and instead of putting the OnOrientationChanged event in the page base and firing that, I pass it to the corresponding base view model and it fires it - that way the business rules bubble up the view model layers instead of bubbling up the page layers and then having to be passed into each individual view model at the top page's binding context.

    But thanks for the core code!

  • CaptainXamtasticCaptainXamtastic GBUniversity ✭✭✭

    @ClintStLaurent said:
    But viewmodels are supposed to be ignorant and agnostic of views.

    What - a criticism without a solution!

    So there's a whole scope such as binding a menu to 'IsVisible' where it can be argued that the business rule is 'give the user a menu tool' as opposed to the viewmodel knowing about the view, and I take orientation as being of the same ilk, in effect being agnostic to the view by accepting it as a business rule 'this is the orientation of the user's hardware' (ie there's no room to give a user a certain tool here).

    And trying to cater for so many platforms, such as presenting Menus in Desktop and Tablet and 'not on iPhone except in Landscape', it's much easier to use a converter bound to a viewmodel than to try OnPlatform in Xaml. And this current project is for 8 platform/idiom combos.

    And what really talks is when the client is spending a 1/4 million plus on the app and says to do it the quickest way, so I'll bind to orientation in the view model, and fire off orientation changes in the vms every time, this is just saying 'don't present the user with a tool.

    And Btw, so what if a view does not subscribe to an event!

    Come up with a solution that allows this to happen without the vm being involved I'm all ears but until you do, I'm too busy making money.

Sign In or Register to comment.