androiddelphifiremonkey

FMX + Android + Rotation


Trying to determine a "180° flip": all of the above methods are useless when the device is quickly flipped (for example from "Landscape" to "Reverse landscape") 😕

But there is a "native way" — through the OrientationEventListener. Can anyone help make it work?

unit android.view.OrientationEventListener;

interface

uses
  AndroidAPI.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.JNI.GraphicsContentViewText;

type
  JOrientationEventListener = interface;

  JOrientationEventListenerClass = interface(JObjectClass)
    ['{6C04CBB1-63B1-45E1-9CBE-AE3F41A60064}']
    function _GetORIENTATION_UNKNOWN: Integer; cdecl;
    function canDetectOrientation : boolean; cdecl;
    function init(context: JContext): JOrientationEventListener; cdecl; overload;
    function init(context: JContext; rate: Integer): JOrientationEventListener; cdecl; overload;
    procedure disable; cdecl;
    procedure enable; cdecl;
    procedure onOrientationChanged(Integerparam0: integer); cdecl;
    property ORIENTATION_UNKNOWN : Integer read _GetORIENTATION_UNKNOWN;
  end;

  [JavaSignature('android/view/OrientationEventListener')]
  JOrientationEventListener = interface(JObject)
    ['{ED4F435E-E48F-420E-A26E-75BFB8FCCB94}']
    function canDetectOrientation: boolean; cdecl;
    procedure disable; cdecl;
    procedure enable; cdecl;
    procedure onOrientationChanged(Integerparam0: integer); cdecl;
  end;

  TJOrientationEventListener = class(TJavaGenericImport<JOrientationEventListenerClass, JOrientationEventListener>)
  end;

const
  TJOrientationEventListenerORIENTATION_UNKNOWN = -1;

implementation

end.

Solution

  • I started looking into creating a descendant of OrientationEventListener (which presently can be done only in Java code), however that came with its own set of problems, namely that the canDetectOrientation function would always return false!

    Next up I looked into why the TOrientationChangedMessage was not being sent when changing immediately from "normal" to "inverted" orientation - that appears to be because the onConfigurationChanged method is not being called on the activity (i.e. no way Delphi can help this)

    I figured then that perhaps the easiest way is to use a timer (ugh) to check when the screen rotation changes, so I came up with the following unit which is now in the Kastri repo:

    unit DW.OrientationMonitor;
    
    {*******************************************************}
    {                                                       }
    {                      Kastri                           }
    {                                                       }
    {         Delphi Worlds Cross-Platform Library          }
    {                                                       }
    {  Copyright 2020-2021 Dave Nottage under MIT license   }
    {  which is located in the root folder of this library  }
    {                                                       }
    {*******************************************************}
    
    {$I DW.GlobalDefines.inc}
    
    interface
    
    uses
      // FMX
      FMX.Types;
    
    type
      TOrientationChangedEvent = procedure(Sender: TObject; const Orientation: TScreenOrientation) of object;
    
      TOrientationMonitor = class(TObject)
      private
        FHasOrientationChanged: Boolean;
        FOrientation: TScreenOrientation;
        FRotation: Integer;
        FTimer: TTimer;
        FOnOrientationChanged: TOrientationChangedEvent;
        procedure DoOrientationChange;
        procedure SetIsActive(const Value: Boolean);
        procedure TimerHandler(Sender: TObject);
        function GetIsActive: Boolean;
      public
        constructor Create;
        destructor Destroy; override;
        property IsActive: Boolean read GetIsActive write SetIsActive;
        property Orientation: TScreenOrientation read FOrientation;
        property OnOrientationChanged: TOrientationChangedEvent read FOnOrientationChanged write FOnOrientationChanged;
      end;
    
    implementation
    
    uses
      // DW
      DW.UIHelper;
    
    { TOrientationMonitor }
    
    constructor TOrientationMonitor.Create;
    begin
      inherited;
      FOrientation := TUIHelper.GetScreenOrientation;
      FTimer := TTimer.Create(nil);
      FTimer.Interval := 100;
      FTimer.OnTimer := TimerHandler;
    end;
    
    destructor TOrientationMonitor.Destroy;
    begin
      FTimer.Free;
      inherited;
    end;
    
    procedure TOrientationMonitor.DoOrientationChange;
    begin
      if Assigned(FOnOrientationChanged) then
        FOnOrientationChanged(Self, FOrientation);
    end;
    
    function TOrientationMonitor.GetIsActive: Boolean;
    begin
      Result := FTimer.Enabled;
    end;
    
    procedure TOrientationMonitor.SetIsActive(const Value: Boolean);
    begin
      FTimer.Enabled := Value;
    end;
    
    procedure TOrientationMonitor.TimerHandler(Sender: TObject);
    var
      LOrientation: TScreenOrientation;
    begin
      LOrientation := TUIHelper.GetScreenOrientation;
      if LOrientation <> FOrientation then
      begin
        FOrientation := LOrientation;
        DoOrientationChange;
      end;
    end;
    
    end.
    

    NOTE The unit relies on other units in the Kastri repo, but you could just as well extract the GetScreenOrientation method

    An example of using TOrientationMonitor:

    unit Unit1;
    
    interface
    
    uses
      System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
      FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.StdCtrls,
      DW.OrientationMonitor;
    
    type
      TForm1 = class(TForm)
        Label1: TLabel;
      private
        FOrientation: TScreenOrientation;
        FOrientationMonitor: TOrientationMonitor;
        procedure OrientationChangedHandler(Sender: TObject; const AOrientation: TScreenOrientation);
      public
        constructor Create(AOwner: TComponent); override;
        destructor Destroy; override;
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.fmx}
    
    uses
      System.TypInfo;
    
    { TForm1 }
    
    constructor TForm1.Create(AOwner: TComponent);
    begin
      inherited;
      FOrientationMonitor := TOrientationMonitor.Create;
      FOrientationMonitor.OnOrientationChanged := OrientationChangedHandler;
      FOrientation := FOrientationMonitor.Orientation;
      Label1.Text := Format('Orientation: %s', [GetEnumName(TypeInfo(TScreenOrientation), Ord(FOrientation))]);
      FOrientationMonitor.IsActive := True;
    end;
    
    destructor TForm1.Destroy;
    begin
      FOrientationMonitor.Free;
      inherited;
    end;
    
    procedure TForm1.OrientationChangedHandler(Sender: TObject; const AOrientation: TScreenOrientation);
    var
      LInfo: PTypeInfo;
    begin
      LInfo := TypeInfo(TScreenOrientation);
      Label1.Text := Format('Old: %s, New: %s', [GetEnumName(LInfo, Ord(FOrientation)), GetEnumName(LInfo, Ord(AOrientation))]);
      FOrientation := AOrientation;
    end;
    
    end.
    

    During this investigation I discovered that the test app would not rotate into the inverted portrait position. The trick to making it so that it does is by updating the manifest to include the android:screenOrientation attribute with a value of fullSensor into the activity node, like this:

    <activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
            android:label="%activityLabel%"
            android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
            android:screenOrientation="fullSensor"
            android:launchMode="singleTask">
    

    I know this doesn't exactly answer your question about OrientationEventListener, but I hope it'll achieve what you desire