kazuser
kazuser

Reputation: 316

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.

Upvotes: 2

Views: 674

Answers (2)

Fernando Rizzato
Fernando Rizzato

Reputation: 112

You can subscribe to the Message TOrientationChangedMessage and use the IFMXScreenService to determine the current orientation:

Subscribe to the specific message on the form create:

  FId := TMessageManager.DefaultManager.SubscribeToMessage
    (TOrientationChangedMessage, DoOrientationChanged);

Declare the message handler:

procedure YourForm.DoOrientationChanged(const Sender: TObject;
  const M: TMessage);
var
  s: IFMXScreenService;
begin
  if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService, s)
  then
  begin
    case s.GetScreenOrientation of
      TScreenOrientation.Portrait:
        lblTitle1.Text := 'Portrait';
      TScreenOrientation.Landscape:
        lblTitle1.Text := 'Landscape';
      TScreenOrientation.InvertedPortrait:
        lblTitle1.Text := 'InvertedPortrait';
      TScreenOrientation.InvertedLandscape:
        lblTitle1.Text := 'InvertedLandscape';
    end;
  end;
end;

Unsubscribe on the form destroy:

  TMessageManager.DefaultManager.Unsubscribe(TOrientationChangedMessage, FId);

Upvotes: 0

Dave Nottage
Dave Nottage

Reputation: 3602

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

Upvotes: 4

Related Questions