Custom iOS Device Rotation

Device orientation change support is something Delphi (or rather iOS) does for you. And you generally don’t question how it’s done since all apps look the same.

I am here to suggest you something different. Since we are talking about animation, video will explain better:

Now, if you like how it looks, this article will explain how to do that.

 

The Wrong Way

First of all let me point out how NOT to do this. Your first instinct might be to lock the device in portrait (for example) orientation and detect device rotation with a sensor, then compensating for rotation.

While you can do this, you will encounter at least two issues with this approach:

  • If the device is flat on the table when the app starts, you don’t know which layout to choose.
  • If your rotation is not synchronized with what iOS thinks your rotation is, any interstitial ads will open the wrong way (it will think that you are locked in portrait). By the way, this is the best spot to remind you that my JVEsoft Components Suite has out of the box support for Chartboost, RevMob, AppFlood and AdMob interstitials, for you to best monetize your app.

 

The Right Way

Generally what you need to do are two simple steps: disable default animation and implement your own animation.

To do that you would need to copy the FMX.Platform.iOS.pas file from the Delphi’s source\fmx folder to your project folder and add it to your project. Upfront, if you are uncomfortable with changing Embarcadero code, you shouldn’t start: this single file has about 5,200 line of code and you are going to change less than 10 of them.

 

1. Find the definitions of the FMXViewController interface and the TFMXViewController class and add this declaration to both:

procedure willRotateToInterfaceOrientation(toInterfaceOrientation:
  UIInterfaceOrientation; duration: NSTimeInterval); cdecl;

Delphi doesn’t do anything before rotation, so it does not override it. But we have to.

 

2. Find the TPlatformCocoaTouch.CalculateStatusBarHeight procedure and change it as follows:

FStatusBarHeight := 0;

Everything will work much cleaner, if Delphi does not take the status bar height into account. And if you want it visible, you would need to compensate for it differently in anyway.

 

3. Find the TFMXViewController.BeforeOrientationChange procedure and remove both calls to TUIView.OCClass.setAnimationsEnabled.

During the rotation Delphi replaces the portrait drawing with a landscape drawing (or vice versa). Nothing wrong with that, but the way Delphi disables animation to do that interferes with our code.

 

4. Add the body for the function, we have declared above:

procedure TFMXViewController.willRotateToInterfaceOrientation(
  toInterfaceOrientation: UIInterfaceOrientation;
  duration: NSTimeInterval);
begin
  TUIView.OCClass.setAnimationsEnabled(False);
end;

This will disable the usual animation, just before it would have started. This code is for pre iOS 8 support.

 

5. Find the TFMXViewController.didRotateFromInterfaceOrientation procedure and add this line at the top:

TUIView.OCClass.setAnimationsEnabled(True);

This will re-enable the animation after the rotation.

 

6. Find the TFMXViewController.viewWillTransitionToSize procedure and add this line at the top:

TUIView.OCClass.setAnimationsEnabled(False);

This is for iOS 8 support.

 

At this point we have disabled the default animation and all we have to do is create our own animation.

 

7. Add your main form (for example) to the uses clause of FMX.Platform.iOS.pas (you should add it to the implementation side, not the interface side).

 

8. Find the TFMXViewController.OrientationChanged procedure and add a call to your own animation function at the end of the procedure:

MainWindow.Rotated;

 

9. The last step is implementing your own animation. The attached example. gives you the exact code, but here is the main extract:

procedure TMainWindow.Rotated;
begin
  // This is the new orientation
  case TUIApplication.wrap(TUIApplication.OCClass.
      SharedApplication).statusBarOrientation of
    UIDeviceOrientationPortrait: Angle := 0;
    UIDeviceOrientationLandscapeLeft: Angle := 1;
    UIDeviceOrientationLandscapeRight: Angle := 3;
    UIDeviceOrientationPortraitUpsideDown: Angle := 2;
  end;

  // Here we keep the main layout on the full screen
  // (Align does not work with rotation)
  if Angle in [1, 3] then
    Layout1.SetBounds((FLastWidth - FLastHeight) / 2,
      (FLastHeight - FLastWidth) / 2, FLastHeight, FLastWidth)
  else
    Layout1.SetBounds(0, 0, FLastWidth, FLastHeight);

  // First rotate the main layout, then animate children rotation
  Layout1.RotationAngle := 360 - Angle * 90;
  FloatAnimation1.StopValue := Angle * 90;
  FloatAnimation1.Start;
end;

Here the Layout1 is the top-level layout (it’s the parent of all visual entities on the screen). FloatAnimation1 is defined with StartFromCurrent set to True and is responsible for rotating all the child controls.

To setup the initial state call this Rotated function from your OnCreate event handler.

 

Working Example

Attached is a working demo code. The GUI was built for iPad, but it would be pretty easy to update it for iPhone.

Notice, due to the bug in Delphi, this code will not work correctly in iOS 8 Simulator. You can run it on the actual device or in an earlier simulator.

You can also look at the some of my apps, which use this approach, for example, here: https://itunes.apple.com/us/app/id592960107

 

The Question

By the way, can anyone tell me how to do this for Android? (Serious question)

 

PS

To make sure your users get a consistent feel from the start, include all 4 orientations of the splash screen, instead of the regular portrait and landscape.

For example, the app I’ve given a link to uses these 4:
down   up   left   right

Leave a Reply

Please use your real name instead of you company name or keyword spam.