Xamarin - Notes on Handling Rotations across Platforms

Xamarin Cross-Platform Rotations Image

 

 *Bluetube was acquired in October 2016. To learn more, please click here.*

I was recently working on a mobile app for iOS and Android in Xamarin that had originally been designed for use in portrait mode only. My task was to modify the application to work in landscape mode in addition to portrait, and for the new landscape views to match the new comps provided. Even the simplest of tasks turns out to have "gotchas" sometimes.

So what were the challenges?

For iOS, my initial approach was to simply swap layout views in the UIViewController during the WillAnimateRotation event, which is one of the rotation lifecycle events. As its name implies, the WillAnimateRotation is called just before the rotation is performed and is where logic should be added to support view rotation.

 

public override void WillAnimateRotation (UIInterfaceOrientation toInterfaceOrientation, double duration)
{

base.WillAnimateRotation (toInterfaceOrientation, duration);

switch (toInterfaceOrientation) {

case UIInterfaceOrientation.LandscapeLeft:

case UIInterfaceOrientation.LandscapeRight:

NSBundle.MainBundle.LoadNib ("LandscapeView", this, null);

break;

case UIInterfaceOrientation.Portrait:

case UIInterfaceOrientation.PortraitUpsideDown:

NSBundle.MainBundle.LoadNib ("PortraitView", this, null);

break;

}

}

When testing this approach, I found that although the views swapped, there was a screen flicker during the transition which would have been a bad user experience. I was unable to determine a cause for the flicker.

So not wanting to lose sight of the forest for the trees nor waste any more time, I changed my approach to use only one view file and reposition the view elements programmatically.

Two hurdles that had to be handled with this approach were:

1. Resizing elements

Calculating the new dimension sizes for the landscape view elements was becoming time               consuming, so I wrote a method for getting the new dimensions on based on the original               dimensions and what the percent modification should be. This saved me a good bit of time from having to perform this task for all the view elements manually.

 

protected Dimensions RecalculateDimensions(float originalWidth, float originalHeight, float percentModifcation)

{

Dimensions d = new Dimensions ();

d.Height = originalHeight * percentModifcation;

d.Width = (originalWidth / originalHeight) * d.Height;

return d;

}

public struct Dimensions

{

public float Width, Height;

public Dimensions(float width, float height)

{

Width = width;

Height = height;

}

}

2. Finding the new X coordinate(s) for centering an element or elements in a series to their parent view:

Finding the X coordinate for a single element was simple. It was just a matter of finding the center X and negative offset for the element based on its frame width:

 

protected float GetCenterXOffset(float elementFrameWidth)

{

return ((this.View.Bounds.Width / 2) - (elementFrameWidth / 2));

}

Elements in a series were a little trickier, because they have to be treated as a whole in relation to the parent view. The formula for determining the X coordinates differs depending on:

(a) Whether there is an odd or even count of elements

(b) How many elements there should be per side

          (c) The total width and spacing between the elements                 

The code for this method is a little too large to post here but I hope to have an example project in the near future for viewing.

For Android, I started again with the approach of additional layouts for landscape mode rather than programmatically adjusting the layouts. This was accomplished by creating a folder called "layout-land" in the Resources directory, where the new landscape layouts would reside. Once this folder is added and a new layout with the same name is added to the layout-land folder, Android will automatically use the new landscape when the device rotated.

There were no major issues with adding landscape layouts in Android. However, there were a few minor issues worth mentioning:

1. The WYSIWYG editor for Android layouts:

Creating and editing Android layouts in the WYSIWYG was frustrating because it is not friendliest of tools. I found editing the layouts directly in the source XML to be less frustrating and quicker for accomplishing my goals.

 

2. Maintaining state across layout swaps:

This was needed when the device was rotated and elements contained data relevant to what was occurring at the time, such as forms where users may have already entered data. There were two events that had to be addressed in the Activity associated with the view. These two events utilize the Bundle object to pass simple data as key/value pairs.

 

protected override void OnSaveInstanceState (Bundle outState)

{

if(!string.IsNullOrEmpty(username.Text))

outState.PutString ("username", username.Text);

if (!string.IsNullOrEmpty (password.Text))

outState.PutString ("password", password.Text);

base.OnSaveInstanceState (outState);

}

protected override void OnRestoreInstanceState(Bundle outstate)

{

username.Text = outstate.GetString ("username");

password.Text = outstate.GetString ("password")

outstate.Clear ();

}

3. The OnDestroy event when swapping layouts:

 

An activity is usually destroyed and recreated when a layout reorientation occurs. Since the original application was only written for Portrait mode, no check was necessary to see if a rotation was occurring. If the OnDestroy method was being called, then the user was exiting the screen for other functionality.

In our OnDestroy method for this activity, a media player service was stopped, so now when the rotation occurred, the media player service stopped. I added a check of a property called IsChangingConfigurations to determine whether the user was exiting for other functionality or simply rotating the layout. That worked fine initially, but during testing we had problems with the application failing on an older Android device with this layout reorientation. The problem was caused by accessing the IsChangingConfigurations property which was introduced in Android 3.X.

The solution I applied was to set a flag on the OnRetainNonConfigurationInstance event which is used by the pre 3.X API's. Then in the OnDestroy I checked the flags according to the build used by the device.

 

private bool hasConfigChanged { get; set; }

...

[Obsolete]

public override Java.Lang.Object OnRetainNonConfigurationInstance()

{

this.hasConfigChanged = true;

return new Java.Lang.Object ();

}

protected override void OnDestroy()

{

bool stopService = false;

if (Build.VERSION.SdkInt < BuildVersionCodes.Honeycomb)

stopService = !this.hasConfigChanged;

else

stopService = !this.IsChangingConfigurations;

if (stopService) {

Intent mp = new Intent (Constants.MediaPlayerService);

MainApplication.Instance.StopService(mp);



base.OnDestroy();

}

In retrospect, I believe the most time consuming problems were the programmatic rearranging of view elements in iOS. It would have been much easier to have simply switched view files on orientation. However, I now have methods for coordinate calculations, which will help for next time.

Here are some resources I referenced while resolving these issues. Perhaps you'll find them of help as well:

Android:

 - Rotation Demo

 - Xamarin Guide to Handling Rotation for Android

iOS

 - Rotation Demo

 - View Controller Programming Guide for iOS 

 

-Wade Renn

Posted in: Mobile and Tablet Xamarin | Permalink

Posted by Wade Renn