FMX navigation bars

I have found that the android navigation bar, which, on phones, lives at the bottom of the screen (and on the same phone area in either portrait or landscape), gets changed on various tablets.

Does anyone know how to determine which phone border the navigation bar lives on? Or does one have to find a different thing, eg “Taskbar” which may or may not exist on a particular device?

Do you specifically need to know where it is located, or do you just want to know the bounds of your running app?

Note that is some circumstances there is no navigation bar (e.g. on my phone I have enabled gestures for nav which frees up the screen real estate normally taken up by the nav bar).

Thanks for replying, Jarrod! Yes, I need to know the area of the screen that I can use, free from the status bar, the navigation bar, and maybe even a taskbar. The problem is that while the status bar is always at the top of the screen, in landscape mode the navigation bar may be on the left or the right, or, with tablets with bigger screens, always at the bottom.

Ian suggested putting everything into a layout (? layabout !) and adjusting its position with padding, but I still need to know the size and position of the various margins, or things get written over and it looks ugly.

Different phone and tablet manufacturers seem to have done various things in different ways. I was looking at reducing the navigation bar so it is replaced by gestures (this was some help), only to get a message on my samsung phone today saying that that has been discontinued, and minimising the navigation bar in this way only wipes out your programme now! NOT the desired effect.

Couple this with ClientHeight and ClientWidth returning different values at different parts of the programme, and confusion gets a little rife!

So that’s the problem from my vantage point!

I can have a look at some code that we use to get the app bounds on Monday. Are you only targeting Android, or do you need to support iOS as well?

Only targeting android.. I can cope with the positioning of the bars on phones, but at what point they change from having the navigation bar in landscape mode at the side versus having it on the bottom I would dearly love to know! Otherwise I have to make it an option that the user has to set when he first runs the programme.

TBH you’re working too hard on this. There are a load of ready-made FireMonkey events and properties you can use which are ready to go out of the box.

To detect the device rotation (on indeed any change like a toolbar being hidden/shown or a user using the new multi-tasking features of Android) use the OnSafeAreaChanged event of the form.

The OnSafeAreaChanged event in Delphi FireMonkey (FMX) triggers when the safe area dimensions of a form change. It is primarily used to adjust UI padding, preventing controls from being obscured by system UI elements like notches, camera cutouts, status bars, or navigation bars on mobile devices.

Why use OnSafeAreaChanged

In modern mobile operating systems (like Android’s edge-to-edge enforcement or iOS/iPadOS), the client area extends to the very edges of the screen. Without adjustments, important UI elements or buttons may be overlapped by system overlays.

How to use it

Assign an event handler to your form’s OnSafeAreaChanged to dynamically adjust the form or layout’s Padding to match the safe area bounds.

Because OnSafeAreaChanged doesn’t pass a TRectF parameter directly in its event signature, you must query the form’s SafeArea property inside the handler.

Example Implementation:

procedure TForm1.FormSafeAreaChanged(Sender: TObject);
var
  LSafeRect: TRectF;
begin
  // Retrieve the current safe area rectangle
  LSafeRect := Self.SafeArea;

  // Adjust your layout or form's padding to avoid system UI overlays
  Self.Padding.Left := LSafeRect.Left;
  Self.Padding.Top := LSafeRect.Top;
  Self.Padding.Right := LSafeRect.Right;
  Self.Padding.Bottom := LSafeRect.Bottom;
end;

Common Scenarios

  • Device Rotation: The safe area dimensions change when a user rotates their device from portrait to landscape (e.g., notches move to the side), triggering this event.

  • OS Updates: Operating systems enforcing full-screen “edge-to-edge” layouts require UI elements to be properly inset.

  • Virtual Keyboards: Showing or hiding certain system overlays can occasionally shift the visible screen boundaries.

Thanks, Ian - I will play with it. Is there something that needs to go into the USES for this?

Self.SafeArea (or FormName.SafeArea) come up unknown.

The only suggestion I can get from the net is

uses
  FMX.Platform, FMX.Platform.iOS, iOSapi.UIKit, FMX.Types;

// Example to get safe area insets
procedure TForm1.FormCreate(Sender: TObject);
var
  SafeAreaService: IFMXSafeAreaService;
  Insets: TRectF;
begin
  if TPlatformServices.Current.SupportsPlatformService(IFMXSafeAreaService, SafeAreaService) then
  begin
    Insets := SafeAreaService.GetSafeAreaInsets(Self);
    // Apply insets to a panel or layout
    Layout1.Padding.Top := Insets.Top;
    Layout1.Padding.Bottom := Insets.Bottom;
  end;
end;

which is pretty messy.

Is not actually correct. It can be implemented like this when you use OnSafeAreaChanged:

procedure TForm1.FormSafeAreaChanged(Sender: TObject; const AInsets: TRectF);
begin
  Padding.Rect := AInsets;
end;

i.e. one line of code.

Much prettier! Thanks.

I’ve found that the AInsets properties are not correct on my Android 16 phone. Logging the values in the FormSafeAreaChanged gives me these results are startup.

T:33.90 B:48.00 L:0.00 R:0.00
T:33.90 B:0.00 L:0.00 R:0.00
T:33.90 B:48.00 L:0.00 R:0.00
T:33.90 B:0.00 L:0.00 R:0.00

So the top is giving me the status bar height, but the bottom is giving me the nav bar height only sometimes with the last time being zero.

If I rotate the phone to landscape, the L value changes to 39.90 and all other values are zero.

If I rotate the phone back to portrait, the T value changes back to 39.90 and all other values are zero.

The only way I can see the B (nav bar) value appear non zero again to to restart the app.

On an older Android 11 phone, with the exact same app at startup I get:

T:31.60 B:0.00 L:0.00 R:0.00
T:0.00 B:0.00 L:0.00 R:0.00

Changing the orientation has no effect (FormSafeAreaChanged never fires again).

Are there issues with FormSafeAreaChanged or could something in my code be interfering?

EDIT: Using D13.1 and all of my controls are on tabs of a TTabControl if that makes any difference.

Oops, I pasted it from an example (didn’t try to compile it). My bad. Even I get it wrong :sob:

Sorry!!!

Yes with 12 and higher, the AInsets gives you the safe area rectangle.

That is odd. It fires every time on my Android 16 device.

I guess it’s possible.

  1. Are you able to reproduce this in a test app?
  2. Are you using OnSafeAreaChanged like I have in the example?

I just tried with a bare bones project (just a memo set to client align) and the results are quite different.

The status and nav bars are now always shown, whereas on my real app they are hidden (blank) until I swipe. What setting affects this behaviour?

On Android 16 the FormSafeAreaChanged does report the correct values for each orientation and using AInsets to change the form padding works correctly.

On Android 11 FormSafeAreaChanged reports zero for all AInsets values. It also never fires after the first time.

All of my real projects use a set of tabs to host the various views. Maybe this is the root cause of the FormSafeAreaChanged issues. I’ll play with the test app until it breaks to try and work it out.

This is what I stripped the code down to for the FormSafeAreaChanged event in both real and test apps:

procedure TfmMain.FormSafeAreaChanged(Sender: TObject; const AInsets: TRectF);
begin
LogAdd(‘T:%f B:%f L:%f R:%f’,[AInsets.Top, AInsets.Bottom, AInsets.Left, AInsets.Right]);
Padding.Rect := AInsets;
end;

OK, my mystery is partly solved. I had FullScreen set which hides the status and navigation bars until they are swiped. If I turn that off FormSafeAreaChanged gives me the correct AInsets and the padding works.

The reason I set FullScreen was to maximise the user space and minimise the distractions.

I did just try and add some code to hide the bars, but no success with that on newer Android versions as it seems that WindowInsetsController is required now. It does work on Android 11.

procedure HideSystemBars;
var
  DecorView: JView;
begin
  DecorView := TAndroidHelper.Activity.getWindow.getDecorView;
  if DecorView <> nil then
  begin
    // Flags for Immersive Sticky mode
    DecorView.setSystemUiVisibility(
      TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_STABLE or
      TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
      TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
      TJView.JavaClass.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
      TJView.JavaClass.SYSTEM_UI_FLAG_FULLSCREEN or
      TJView.JavaClass.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
    );
  end;
end;

David, I have been sweating tears of blood over coping with status and navigation bars, finding out things like which version of android does what with them, and getting programmes to run on androids from 10 to 16. There is some quite inconsistent behaviour, some of which I have managed to get around, and some I just have to live with.

Here are the tools I have put together. Where you are trying to utilise the space behind the status and navigation bars, I have been trying to put them back in, as having them obliterate buttions and squiggle over images I find unpleasant.

You may find some useful ideas, so here it is, though rather long!

Routines to allow status and navigation bars back into android programmes, after the developers made them part of the scene (dam* them!) in android 15.

Need to have in the main unit:

  StatusBarHeight, NavigationBarHeight : integer;  //Heights of these bars
  OriginalClientHeight, OriginalClientWidth : integer;  //Original client height and width - almost!
  OrientationMargins : tRect;   //Gets margins around "safe area" of screen

Need to have in each unit:

  ImageStatus, ImageNavigation : tImage;           //Images for the status and navigation bars



In Form.Create in the main programme:

   GetStatusBarHeights;   // Get the heights of the status and the navigation bars.
                          // Note that these are zero in earlier androids.
   
   // The Client height and Client Width available here are short by the status bar height,
   // depending on the orientation of the device.

   if ClientHeight > ClientWidth then
   begin
      OriginalClientHeight := max(ClientHeight, ClientWidth) + round(StatusBarHeight);
      OriginalClientWidth := min(ClientHeight, ClientWidth);
   end
   else
   begin
      OriginalClientHeight := max(ClientHeight, ClientWidth);
      OriginalClientWidth := min(ClientHeight, ClientWidth) + round(StatusBarHeight);
   end;




In Form.OnSafeAreaChanged in the main programme:

procedure Form.FormSafeAreaChanged(Sender: TObject; const AInsets: TRectF);
begin
   OrientationMargins.Left := round(aInsets.Left);
   OrientationMargins.Top := round(aInsets.Top);
   OrientationMargins.Right := round(aInsets.Right);
   OrientationMargins.Bottom := round(aInsets.Bottom);
end;







Need to put this at the start of each Form.Resize:

var
   CH, CW : single;  // Adjusted ClientHeight and ClientWidth
   LeftOffset : single; // For when the left has to be shifted due to the buttons
   TopOffset : single;
   Orientation : string;
   rectStatus, rectNavigation : tRectF;
begin
   Memo1.Lines.Add('Resize: ' + inttostr(ClientHeight) + ', ' + inttostr(ClientWidth));

   if ClientWidth = 0 then exit;    // Get zero in the first call, I think from the FormCreate calling.

   // Set up the parameters for the usable screen area - ie without the status and navigation bars, and
   // the top and left margins for different orientations..
   // The ClientHeight and ClientWidth arguments are no longer required (but left in so I can use this
   // routine in other programmes without having to change them all!).

   // The usable rectangle is CH x CW, with left and top margins of LeftOffset and TopOffset.
   // The rectangles are for the images used to cover the status and navigation bar areas,
   // though the navigation bar one is a bit limited as it just sits under the opacity of
   // the navigation bar rather than taking over its colour.

   SetBarDetails(F1, ImageStatus, ImageNavigation, ClientHeight, ClientWidth, CH, CW, LeftOffset, TopOffset, rectStatus, rectNavigation, Orientation);

   // The SetSizes cause the images to be written on the form on which they are called,
   // hence they have to be done here, not in SetBarDetails.  I think this is because
   // SetSize is responsible for the construction of the canvas for the image.

   ImageStatus.Bitmap.SetSize(round(rectStatus.Right), round(rectStatus.Bottom));
   ImageNavigation.Bitmap.SetSize(round(rectNavigation.Right), round(rectNavigation.Bottom));

   // Actually write the status and navigation images (only necessary in androids
   // from 15 on).

   WriteBars(ImageStatus, ImageNavigation, rectStatus, rectNavigation, Orientation);

   //Adjust some of the stuff on the screen.





And you need the following routines in the main unit:


procedure Form.GetStatusBarHeights;
var
  LID: Integer;
  LResources: JResources;
begin
  StatusBarHeight := 0;
  LResources := TAndroidHelper.Context.getResources;
  LID := LResources.getIdentifier(StringToJString('status_bar_height'), StringToJString('dimen'), StringToJString('android'));

  if LID > 0 then
    StatusBarHeight := round(LResources.getDimensionPixelSize(LID) / TAndroidHelper.DisplayMetrics.density);


  NavigationBarHeight := 0;
  LResources := TAndroidHelper.Context.getResources;
  LID := LResources.getIdentifier(StringToJString('navigation_bar_height'), StringToJString('dimen'), StringToJString('android'));

  if LID > 0 then
    NavigationBarHeight := round(LResources.getDimensionPixelSize(LID) / TAndroidHelper.DisplayMetrics.density);

    //Earlier androids may still return a nav and status bar height, though the only
    // effect they have on the "safe" area is to decrease the available height.
    // The OnSafeAreaChanges is not called in the earlier androids (<= 14)
end;





procedure tPeteAMainForm.SetBarDetails (Form : tForm; var xImageStatus : tImage; var xImageNavigation : tImage;
   FormClientHeight, FormClientWidth : integer; var CH : single; var CW : single;
   var LeftOffset : single; var TopOffset : single; var RectStatus : tRectF; var RectNavigation : tRectf;
   var Orientation : string);
// FormClient Height, FormClientWidth used to be provided by the calling form using the ClientHeight and ClientWidth parameters
// Note that they are zero on the first call, so if they are zero, do nothing.
// They are no longer used!
// CH and CW are the returned clientheight and clientwidth for where we will put our components
// LeftOffset and TopOffset are the offsets from the top and the side for where the status and navigation bars go
// RectStatus, RectNavigation contain the positions and sizes for the status bar rectangles
// - left, top for the positions, width and height for the sizes.
var
   LService: IFMXScreenService;
   tso : tScreenOrientation;
begin
   OriginalClientHeight := max(ClientHeight, OriginalClientHeight); //Sometimes get variation in this.
   FormClientHeight := OriginalClientHeight;
   FormClientWidth := OriginalCLientWidth;

   CH := FormClientHeight;
   CW := FormClientWidth;
   LeftOffset := 0;
   TopOffset := StatusBarHeight;

   if StatusBarHeight <= 0 then GetStatusBarHeights;

   //Make the status bar and navigation bar images (OK to make them here, but the
   // bitmap.setsizes must be done on the form on which the image is to be drawn.

   if xImageStatus = nil then xImageStatus := tImage.Create(Form);
   if xImageStatus.Bitmap = nil then xImageStatus.Bitmap := tBitmap.Create;
   xImageStatus.Parent := Form;

   if xImageNavigation = nil then xImageNavigation := tImage.Create(Form);
   if xImageNavigation.Bitmap = nil then xImageNavigation.Bitmap := tBitmap.Create;
   xImageNavigation.Parent := Form;

   if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService, LService) then
   begin
      tso := LService.GetScreenOrientation; //This is more accurate than using ClientHeight and CLientWidth

      case tso of     // These were more for debugging that usefulne3ss, but here they are anyway.
         tScreenOrientation.Portrait:           Orientation := 'Portrait';
         tScreenOrientation.Landscape:          Orientation := 'Landscape';
         tScreenOrientation.InvertedPortrait:   Orientation := 'InvertedPortrait';
         tScreenOrientation.InvertedLandscape:  Orientation := 'InvertedLandscape';
      end;



   if not TOSVersion.Check(15) then
   begin
      //Here, StatusBarHeight and NavigationBarHeight are both zero!

      if (tso = tScreenOrientation.Landscape) or (tso = tScreenOrientation.InvertedLandscape) then
      begin
         CH := min(ClientHeight, ClientWidth);      //Different versions of android give different values!
         CW := max(ClientHeight, ClientWidth);
      end
      else
      begin
         CH := max(ClientHeight, ClientWidth);
         CW := min(ClientHeight, ClientWidth);
      end;

      CH := CH - NavigationBarHeight - StatusBarHeight;

      LeftOffset := 0;
      TopOffset := 0;

      RectStatus := tRectF.Create(100, 100, 200, 200);      // These were necessary for debugging, and have been left in
      RectNavigation := tRectF.Create(100, 100, 200, 200);  // as no doubt there will be further debugging needed!

      exit;
   end;



      if (tso = tScreenOrientation.Portrait) or (tso = tScreenOrientation.InvertedPortrait) or
         ( (OrientationMargins.Left = 0) and (OrientationMargins.Right = 0) ) then
      begin
      //Here, the bars are top and bottom, with nothing on either side.

         if (tso = tScreenOrientation.Portrait) or (tso = tScreenOrientation.InvertedPortrait) then
         begin

         // PORTRAIT - status bar on top, clientwidth, nav bat on bottom, ibid.

            CH := FormClientHeight - StatusBarHeight - NavigationBarHeight;

            RectStatus.Left := 0;
            RectStatus.Top := 0;
            RectStatus.Right := CW;
            RectStatus.Bottom := StatusBarHeight;

            RectNavigation.Left := 0;
            RectNavigation.Top := FormClientHeight - NavigationBarHeight;
            RectNavigation.Right := CW;
            RectNavigation.Bottom := NavigationBarHeight;
         end
         else
         begin

         // LANDSCAPE - but still with top and bottom bars.

            CH := FormClientWidth - StatusBarHeight - NavigationBarHeight;
            CW := FormCLientHeight;

            RectStatus.Left := 0;
            RectStatus.Top := 0;
            RectStatus.Right := CW;
            RectStatus.Bottom := StatusBarHeight;

            RectNavigation.Left := 0;
            RectNavigation.Top := FormClientWidth - NavigationBarHeight;
            RectNavigation.Right := CW;
            RectNavigation.Bottom := NavigationBarHeight;
         end;
      end
      else
      begin

      //Landscape or InvertedLandscape, with navigation bar to one side.

         if tso = tScreenOrientation.Landscape then  //Landscape - rotated to the left
         begin
            Orientation := 'Landscape';

            FormClientHeight := OriginalClientWidth;
            FormClientWidth := OriginalCLientHeight;

            // Status bar at the top; navigation bar on the right.

            CH := FormClientHeight - StatusBarHeight;
            CW := FormClientWidth - NavigationBarHeight;

            //Status bar image position X, Y:  0,0
            //Status bar image size  StatusBarHeight, ClientHeight

            RectStatus.Left := 0;
            RectStatus.Top := 0;
            RectStatus.Right := FormClientWidth;
            RectStatus.Bottom := StatusBarHeight;;

            //Navigation bar image position X, Y: 0, ClientHeight - NavigationBarHeight
            //Navigation bar image size ClientWidth NavigationBarHeight

            RectNavigation.Left := FormClientWidth - NavigationBarHeight;
            RectNavigation.Top := 0;
            RectNavigation.Right := NavigationBarHeight;
            RectNavigation.Bottom := FormClientWidth;
         end;

         if tso = tScreenOrientation.InvertedLandscape then //Landscape - rotated to the right
         begin
            Orientation := 'InvertedLandscape';

            //Status bar at the top; Navigation bar on the left.

            FormClientHeight := OriginalClientWidth;
            FormClientWidth := OriginalCLientHeight;

            CW := FormClientWidth - NavigationBarHeight;
            CH := FormClientHeight - StatusBarHeight;
            LeftOffset := NavigationBarHeight;

            RectStatus.Left := 0;
            RectStatus.Top := 0;
            RectStatus.Right := FormClientWidth;
            RectStatus.Bottom := StatusBarHeight;

            RectNavigation.Left := 0;
            RectNavigation.Top := 0;
            RectNavigation.Right := NavigationBarHeight;
            RectNavigation.Bottom := FormClientWidth;
         end;
      end;
   end;

   //Set the positions and the sizes of the images.

   xImageStatus.Position.X := RectStatus.Left;
   xImageStatus.Position.Y := RectStatus.Top;
   xImageStatus.Width := RectStatus.Right;
   xImageStatus.Height := RectStatus.Bottom;

   xImageNavigation.Position.X := RectNavigation.Left;
   xImageNavigation.Position.Y := RectNavigation.Top;
   xImageNavigation.Width := RectNavigation.Right;
   xImageNavigation.Height := RectNavigation.Bottom;
end;








procedure Form.WriteBars (var ImageStatus : tImage; var ImageNavigation : tImage;
   var RectStatus : tRectF; var RectNavigation : tRectf; var Orientation : string);

//I don't know if this idea is valid, but it comes from an earlier version using CLientHeight and ClientWidth:
// The ClientHeight and ClientWidth appear to be defined for each form - hence they have to be passed as
// arguments to this routine or otherwise you get the values for this form, which are just held over from when
// this form was last active - and may have nothing to do with the values for the current form!

var
   rect : tRectF;
   brush : tBrush;
   s : string;
begin
   if not tOsVersion.Check(15) then exit;   //Not needed if android < 15

   brush := tBrush.Create(tBrushKind.solid, tAlphaColorRec.Red);///.Black);

   // Draw the status bar

   rect := tRectF.Create(0, 0, RectStatus.Right, RectStatus.Bottom);
   ImageStatus.Bitmap.Canvas.BeginScene;
   ImageStatus.Bitmap.Canvas.FillRect(rect, 1, brush);
   ImageStatus.Bitmap.Canvas.EndScene;

   //Now for the navigation bar

   brush.Color := tAlphaColorRec.Springgreen;

///   rect := tRectF.Create(0, 0, 100, 100);
   rect := tRectF.Create(0, 0, RectNavigation.Right, RectNavigation.Bottom);
   ImageNavigation.Bitmap.Canvas.BeginScene;
   ImageNavigation.Bitmap.Canvas.FillRect(rect, 1, brush);
   ImageNavigation.Bitmap.Canvas.EndScene;

   brush.Destroy;
end;



Are you running your app with the form’s Full Screen set true? That was the cause of most of my issues.

Now I’ve turned that off the padding works well. Even having the padding adjust to suit the virtual keyboard works now, so I can adjust the viewport of the TVerticalScroll I use on the tabs and keep my edit controls visible while the keyboard is showing.

The only thing I’d like to do is hide the status and navigation bars, but still reserve the space for them and have them only appear when the user swipes.

Interesting, David. I thought that from android 15 on it was always on no matter what the settings said! I have always stuck with the default “off”. The help text is not very helpful!