├── .gitignore ├── README.md ├── delphi_google_geocoding ├── README.md ├── demo │ ├── PGeolocation.dpr │ ├── PGeolocation.dproj │ ├── Unit1.fmx │ └── Unit1.pas ├── iPub.Rtl.Geolocation.Google.pas └── iPub.Rtl.Geolocation.pas ├── delphi_ios_exploring_app_crashes ├── README.md ├── ss1.png ├── ss2.png ├── ss3.png ├── ss4.png ├── ss5.png ├── ss6.png └── ss7.png ├── delphi_ios_handle_incoming_url └── README.md ├── delphi_ios_universal_links └── README.md └── delphi_system_bars ├── README.md ├── iPub.FMX.SystemBars.Android.pas ├── iPub.FMX.SystemBars.iOS.pas ├── iPub.FMX.SystemBars.pas └── screenshots ├── calculator.png ├── gallery.png └── nubank.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Uncomment these types if you want even more clean repository. But be careful. 2 | # It can make harm to an existing project source. Read explanations below. 3 | # 4 | # Resource files are binaries containing manifest, project icon and version info. 5 | # They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files. 6 | # 7 | # Type library file (binary). In old Delphi versions it should be stored. 8 | # Since Delphi 2009 it is produced from .ridl file and can safely be ignored. 9 | #*.tlb 10 | # 11 | # Diagram Portfolio file. Used by the diagram editor up to Delphi 7. 12 | # Uncomment this if you are not using diagrams or use newer Delphi version. 13 | #*.ddp 14 | # 15 | # Visual LiveBindings file. Added in Delphi XE2. 16 | # Uncomment this if you are not using LiveBindings Designer. 17 | #*.vlb 18 | # 19 | # Deployment Manager configuration file for your project. Added in Delphi XE2. 20 | # Uncomment this if it is not mobile development and you do not use remote debug feature. 21 | #*.deployproj 22 | # 23 | 24 | # Delphi compiler-generated binaries (safe to delete) 25 | #*.exe 26 | *.dll 27 | *.bpl 28 | *.bpi 29 | *.dcp 30 | *.so 31 | *.apk 32 | *.drc 33 | *.map 34 | *.dres 35 | *.rsm 36 | *.tds 37 | *.dcu 38 | *.lib 39 | *.jdbg 40 | *.plist 41 | 42 | # Delphi autogenerated files (duplicated info) 43 | *.cfg 44 | *Resource.rc 45 | 46 | # Delphi local files (user-specific info) 47 | *.local 48 | *.identcache 49 | *.projdata 50 | *.tvsconfig 51 | *.skincfg 52 | *.cbk 53 | *.dsk 54 | 55 | # Delphi history and backups 56 | __history/ 57 | __recovery/ 58 | *.~* 59 | 60 | # Castalia statistics file 61 | *.stat 62 | 63 | # Rtf 64 | ~$*.rtf 65 | ~$*.doc 66 | ~$*.docx 67 | 68 | # iPub Changes 69 | bin/ 70 | !bin/**/*.dll 71 | *.dex 72 | *.a 73 | *.o 74 | *.vrc 75 | *.res 76 | *.log 77 | !app/*.res 78 | tests/ 79 | tmp*.tmp 80 | !**/redist/** 81 | !**/resources/** 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firemonkey Tutorials 2 | 1) ### [Delphi iOS - Exploring app crashes] 3 | > Exploring the .crash files generated by the iOS symbolizing it to identify the call stack 4 | 5 | 2) ### [Delphi iOS - Universal Links] 6 | > Adding a new Universal Link to your Delphi Firemonkey iOS app 7 | 8 | 3) ### [Delphi iOS - Handle incoming url] 9 | > Find out which url opened your app (custom schemes and universal links) 10 | 11 | 4) ### [Delphi All - Google Geocoding] 12 | > Getting the address of an geographic coordinates, and the reverse, getting the geographic coordinates of an address 13 | 14 | 5) ### [Delphi All - System Bars] 15 | > Setting the status bar and navigation bar background colors and visibility, getting the bounds and an extra tip to get the system bars full transparent in android's splash screen 16 | 17 | [Delphi iOS - Exploring app crashes]: 18 | [Delphi iOS - Universal Links]: 19 | [Delphi iOS - Handle incoming url]: 20 | [Delphi All - Google Geocoding]: 21 | [Delphi All - System Bars]: 22 | -------------------------------------------------------------------------------- /delphi_google_geocoding/README.md: -------------------------------------------------------------------------------- 1 | # Google Geocoding 2 | This is a tutorial of how to use the google maps geocoding API to get the address of an geographic coordinates, and the reverse, to get the geographic coordinates of an address with delphi. This code is full cross platform. 3 | 4 | ##### Get the google maps geocoding api key 5 | First of all, you need to get your own google maps geocoding api key. This is an easy step, and you will find this searching in the google. 6 | 7 | ### Geographic coordinates to address 8 | 9 | ```delphi 10 | procedure TForm1.btnGeolocationToAddressClick(Sender: TObject); 11 | var 12 | LGeolocation: TipSimpleGeolocation; 13 | LGoogleGeocoding: IipGoogleGeocoding; 14 | LStreetNumber: string; 15 | LRoute: string; 16 | LSublocality: string; 17 | LLocality: string; 18 | LAdministrativeArea1: string; 19 | LCountry: string; 20 | LPostalCode: string; 21 | LFormattedAddress: string; 22 | begin 23 | LGeolocation.Latitude := 40.7581389; 24 | LGeolocation.Longitude := -73.9773581; 25 | 26 | LGoogleGeocoding := TipGoogleGeocoding.Create; 27 | LGoogleGeocoding.ApiKey := 'okokokokokokok'; // Your google maps API key here <<<<<<<<<<<<<<<<<<<< 28 | if LGoogleGeocoding.TryGeolocationToAddress(LGeolocation, 'pt-BR', LStreetNumber, 29 | LRoute, LSublocality, LLocality, LAdministrativeArea1, LCountry, LPostalCode, 30 | LFormattedAddress) then 31 | begin 32 | showmessage('Successful!' + #13#10 + #13#10 + LFormattedAddress) 33 | end 34 | else 35 | showmessage('Failed!'); 36 | end; 37 | ``` 38 | Replace the ```okokokokokokok``` with your api key. 39 | 40 | ### Address to geographic coordinates 41 | 42 | ```delphi 43 | procedure TForm1.btnAddressToGeolocationClick(Sender: TObject); 44 | var 45 | LGeolocation: TipSimpleGeolocation; 46 | LGoogleGeocoding: IipGoogleGeocoding; 47 | begin 48 | LGoogleGeocoding := TipGoogleGeocoding.Create; 49 | LGoogleGeocoding.ApiKey := 'okokokokokokok'; // Your google maps API key here <<<<<<<<<<<<<<<<<<<< 50 | if LGoogleGeocoding.TryAddressToGeolocation('625 5th Ave, New York, NY 10022, EUA', 'pt-BR', LGeolocation) then 51 | begin 52 | showmessage(Format('Successful!' + #13#10 + #13#10 + 'Latitude: %g' + #13#10 + 'Longitude: %g', 53 | [LGeolocation.Latitude, LGeolocation.Longitude])); 54 | end 55 | else 56 | showmessage('Failed!'); 57 | end; 58 | ``` 59 | 60 | Replace the ```okokokokokokok``` with your api key. -------------------------------------------------------------------------------- /delphi_google_geocoding/demo/PGeolocation.dpr: -------------------------------------------------------------------------------- 1 | program PGeolocation; 2 | 3 | uses 4 | System.StartUpCopy, 5 | FMX.Forms, 6 | Unit1 in 'Unit1.pas' {Form1}, 7 | iPub.Rtl.Geolocation.Google in '..\iPub.Rtl.Geolocation.Google.pas', 8 | iPub.Rtl.Geolocation in '..\iPub.Rtl.Geolocation.pas'; 9 | 10 | {$R *.res} 11 | 12 | begin 13 | Application.Initialize; 14 | Application.CreateForm(TForm1, Form1); 15 | Application.Run; 16 | end. 17 | -------------------------------------------------------------------------------- /delphi_google_geocoding/demo/Unit1.fmx: -------------------------------------------------------------------------------- 1 | object Form1: TForm1 2 | Left = 0 3 | Top = 0 4 | Caption = 'Form1' 5 | ClientHeight = 254 6 | ClientWidth = 298 7 | FormFactor.Width = 320 8 | FormFactor.Height = 480 9 | FormFactor.Devices = [Desktop] 10 | DesignerMasterStyle = 0 11 | object btnGeolocationToAddress: TButton 12 | Position.X = 56.000000000000000000 13 | Position.Y = 64.000000000000000000 14 | Size.Width = 153.000000000000000000 15 | Size.Height = 33.000000000000000000 16 | Size.PlatformDefault = False 17 | TabOrder = 0 18 | Text = 'Geolocation to Address' 19 | OnClick = btnGeolocationToAddressClick 20 | end 21 | object btnAddressToGeolocation: TButton 22 | Position.X = 56.000000000000000000 23 | Position.Y = 112.000000000000000000 24 | Size.Width = 153.000000000000000000 25 | Size.Height = 33.000000000000000000 26 | Size.PlatformDefault = False 27 | TabOrder = 1 28 | Text = 'Address to Geolocation' 29 | OnClick = btnAddressToGeolocationClick 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /delphi_google_geocoding/demo/Unit1.pas: -------------------------------------------------------------------------------- 1 | unit Unit1; 2 | 3 | interface 4 | 5 | uses 6 | System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, 7 | FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, 8 | FMX.Controls.Presentation, FMX.StdCtrls; 9 | 10 | type 11 | TForm1 = class(TForm) 12 | btnGeolocationToAddress: TButton; 13 | btnAddressToGeolocation: TButton; 14 | procedure btnGeolocationToAddressClick(Sender: TObject); 15 | procedure btnAddressToGeolocationClick(Sender: TObject); 16 | private 17 | { Private declarations } 18 | public 19 | { Public declarations } 20 | end; 21 | 22 | var 23 | Form1: TForm1; 24 | 25 | implementation 26 | 27 | {$R *.fmx} 28 | 29 | uses 30 | iPub.Rtl.Geolocation, 31 | iPub.Rtl.Geolocation.Google; 32 | 33 | procedure TForm1.btnGeolocationToAddressClick(Sender: TObject); 34 | var 35 | LGeolocation: TipSimpleGeolocation; 36 | LGoogleGeocoding: IipGoogleGeocoding; 37 | LStreetNumber: string; 38 | LRoute: string; 39 | LSublocality: string; 40 | LLocality: string; 41 | LAdministrativeArea1: string; 42 | LCountry: string; 43 | LPostalCode: string; 44 | LFormattedAddress: string; 45 | begin 46 | LGeolocation.Latitude := 40.7581389; 47 | LGeolocation.Longitude := -73.9773581; 48 | 49 | LGoogleGeocoding := TipGoogleGeocoding.Create; 50 | LGoogleGeocoding.ApiKey := 'okokokokokokok'; // Your google maps API key here <<<<<<<<<<<<<<<<<<<< 51 | if LGoogleGeocoding.TryGeolocationToAddress(LGeolocation, 'pt-BR', LStreetNumber, 52 | LRoute, LSublocality, LLocality, LAdministrativeArea1, LCountry, LPostalCode, 53 | LFormattedAddress) then 54 | begin 55 | showmessage('Successful!' + #13#10 + #13#10 + LFormattedAddress) 56 | end 57 | else 58 | showmessage('Failed!'); 59 | end; 60 | 61 | procedure TForm1.btnAddressToGeolocationClick(Sender: TObject); 62 | var 63 | LGeolocation: TipSimpleGeolocation; 64 | LGoogleGeocoding: IipGoogleGeocoding; 65 | begin 66 | LGoogleGeocoding := TipGoogleGeocoding.Create; 67 | LGoogleGeocoding.ApiKey := 'okokokokokokok'; // Your google maps API key here <<<<<<<<<<<<<<<<<<<< 68 | if LGoogleGeocoding.TryAddressToGeolocation('625 5th Ave, New York, NY 10022, EUA', 'pt-BR', LGeolocation) then 69 | begin 70 | showmessage(Format('Successful!' + #13#10 + #13#10 + 'Latitude: %g' + #13#10 + 'Longitude: %g', 71 | [LGeolocation.Latitude, LGeolocation.Longitude])); 72 | end 73 | else 74 | showmessage('Failed!'); 75 | end; 76 | 77 | end. 78 | -------------------------------------------------------------------------------- /delphi_google_geocoding/iPub.Rtl.Geolocation.Google.pas: -------------------------------------------------------------------------------- 1 | unit iPub.Rtl.Geolocation.Google; 2 | 3 | interface 4 | 5 | {$SCOPEDENUMS ON} 6 | 7 | uses 8 | { Delphi } 9 | System.Net.HttpClient, 10 | 11 | { iPub } 12 | iPub.Rtl.Geolocation; 13 | 14 | type 15 | { IipGoogleGeocoding } 16 | 17 | IipGoogleGeocoding = interface(IUnknown) 18 | ['{35162A15-67AD-4381-8161-92D59DA962A7}'] 19 | function GetApiKey: string; 20 | procedure SetApiKey(const AValue: string); 21 | function TryAddressToGeolocation(const AAddress, ALanguage: string; out AGeolocation: TipSimpleGeolocation): Boolean; 22 | function TryGeolocationToAddress(const AGeolocation: TipSimpleGeolocation; const ALanguage: string; var AStreetNumber, ARoute, ASublocality, ALocality, AAdministrativeArea1, ACountry, APostalCode, AFormattedAddress: string): Boolean; 23 | property ApiKey: string read GetApiKey write SetApiKey; 24 | end; 25 | 26 | { TipGoogleGeocoding } 27 | 28 | TipGoogleGeocoding = class(TInterfacedObject, IipGoogleGeocoding) 29 | private 30 | FApiKey: string; 31 | FHttpClient: THTTPClient; 32 | function GetApiKey: string; 33 | procedure SetApiKey(const AValue: string); 34 | public 35 | constructor Create(const AApiKey: string = ''); 36 | function TryAddressToGeolocation(const AAddress, ALanguage: string; out AGeolocation: TipSimpleGeolocation): Boolean; 37 | function TryGeolocationToAddress(const AGeolocation: TipSimpleGeolocation; const ALanguage: string; var AStreetNumber, ARoute, ASublocality, ALocality, AAdministrativeArea1, ACountry, APostalCode, AFormattedAddress: string): Boolean; 38 | property ApiKey: string read GetApiKey write SetApiKey; 39 | end; 40 | 41 | implementation 42 | 43 | uses 44 | { Delphi } 45 | System.Generics.Collections, 46 | System.JSON, 47 | System.Net.URLClient, 48 | System.NetEncoding, 49 | System.NetConsts, 50 | System.SysUtils; 51 | 52 | { TipGoogleGeocoding } 53 | 54 | constructor TipGoogleGeocoding.Create(const AApiKey: string); 55 | begin 56 | inherited Create; 57 | FApiKey := AApiKey; 58 | FHttpClient := THTTPClient.Create; 59 | FHttpClient.Accept := 'application/json'; 60 | FHttpClient.AcceptCharSet := 'UTF-8'; 61 | FHttpClient.AllowCookies := False; 62 | FHttpClient.ConnectionTimeout := 10000; 63 | FHttpClient.HandleRedirects := False; 64 | FHttpClient.ResponseTimeout := 10000; 65 | FHttpClient.SecureProtocols := [THttpSecureProtocol.TLS12]; 66 | end; 67 | 68 | function TipGoogleGeocoding.GetApiKey: string; 69 | begin 70 | Result := FApiKey; 71 | end; 72 | 73 | procedure TipGoogleGeocoding.SetApiKey(const AValue: string); 74 | begin 75 | FApiKey := AValue; 76 | end; 77 | 78 | function TipGoogleGeocoding.TryAddressToGeolocation(const AAddress, ALanguage: string; 79 | out AGeolocation: TipSimpleGeolocation): Boolean; 80 | const 81 | GOOGLE_ADDRESS_TO_GEOLOCATION_URI = 'https://maps.googleapis.com/maps/api/geocode/json?address=%s&key=%s'; 82 | var 83 | LLatitude: Double; 84 | LLongitude: Double; 85 | LResponse: IHTTPResponse; 86 | LResultsArray: TJSONArray; 87 | LValue: TJSONValue; 88 | begin 89 | AGeolocation := TipSimpleGeolocation.Create; 90 | if FApiKey.IsEmpty then 91 | Exit(False); 92 | FHttpClient.AcceptLanguage := 'en-US;q=0.5'; 93 | if not ALanguage.Trim.IsEmpty then 94 | FHttpClient.AcceptLanguage := ALanguage + ',' + FHttpClient.AcceptLanguage; 95 | try 96 | LResponse := FHttpClient.Get(Format(GOOGLE_ADDRESS_TO_GEOLOCATION_URI, [TNetEncoding.URL.EncodeQuery(AAddress), FApiKey])); 97 | except 98 | on E: ENetException do 99 | Exit(False); 100 | end; 101 | LValue := TJSONObject.ParseJSONValue(LResponse.ContentAsString); 102 | try 103 | if (not Assigned(LValue)) or (LValue.GetValue('status', '') <> 'OK') then 104 | Exit(False); 105 | LResultsArray := LValue.P['results'] as TJSONArray; 106 | if LResultsArray.Count <= 0 then 107 | Exit(False); 108 | Result := (Assigned(LResultsArray.Items[0].FindValue('geometry'))) and (Assigned(LResultsArray.Items[0].P['geometry'].FindValue('location'))); 109 | if Result then 110 | begin 111 | Result := (LResultsArray.Items[0].P['geometry'].P['location'].TryGetValue('lat', LLatitude)) and (LResultsArray.Items[0].P['geometry'].P['location'].TryGetValue('lng', LLongitude)); 112 | if Result then 113 | begin 114 | AGeolocation.Latitude := LLatitude; 115 | AGeolocation.Longitude := LLongitude; 116 | end; 117 | end; 118 | finally 119 | LValue.Free; 120 | end; 121 | end; 122 | 123 | function TipGoogleGeocoding.TryGeolocationToAddress( 124 | const AGeolocation: TipSimpleGeolocation; const ALanguage: string; var AStreetNumber, 125 | ARoute, ASublocality, ALocality, AAdministrativeArea1, ACountry, 126 | APostalCode, AFormattedAddress: string): Boolean; 127 | 128 | function ProcessAddressComponent(AItem: TJSONObject): Boolean; 129 | var 130 | LValue: TJSONValue; 131 | LTypesArray: TJSONArray; 132 | I: Integer; 133 | begin 134 | LValue := AItem.GetValue('types'); 135 | if (not Assigned(LValue)) or not (LValue is TJSONArray) then 136 | Exit(False); 137 | Result := True; 138 | LTypesArray := TJSONArray(LValue); 139 | for I := 0 to LTypesArray.Count-1 do 140 | begin 141 | if LTypesArray.Items[I].Value = 'street_number' then 142 | begin 143 | if not AItem.TryGetValue('long_name', AStreetNumber) then 144 | Exit(False); 145 | end 146 | else if LTypesArray.Items[I].Value = 'route' then 147 | begin 148 | if not AItem.TryGetValue('long_name', ARoute) then 149 | Exit(False); 150 | end 151 | else if LTypesArray.Items[I].Value = 'sublocality' then 152 | begin 153 | if not AItem.TryGetValue('long_name', ASublocality) then 154 | Exit(False); 155 | end 156 | else if (LTypesArray.Items[I].Value = 'locality') and (ALocality = '') then 157 | begin 158 | if not AItem.TryGetValue('long_name', ALocality) then 159 | Exit(False); 160 | end 161 | else if (LTypesArray.Items[I].Value = 'administrative_area_level_2') and (ALocality = '') then 162 | begin 163 | if not AItem.TryGetValue('long_name', ALocality) then 164 | Exit(False); 165 | end 166 | else if LTypesArray.Items[I].Value = 'administrative_area_level_1' then 167 | begin 168 | if not AItem.TryGetValue('short_name', AAdministrativeArea1) then 169 | Exit(False); 170 | end 171 | else if LTypesArray.Items[I].Value = 'country' then 172 | begin 173 | if not AItem.TryGetValue('long_name', ACountry) then 174 | Exit(False); 175 | end 176 | else if LTypesArray.Items[I].Value = 'postal_code' then 177 | begin 178 | if not AItem.TryGetValue('long_name', APostalCode) then 179 | Exit(False); 180 | end; 181 | end; 182 | end; 183 | 184 | const 185 | GOOGLE_GEOLOCATION_TO_ADDRESS_URI = 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%s,%s&key=%s'; 186 | var 187 | I: Integer; 188 | LResponse: IHTTPResponse; 189 | LStatus: string; 190 | LJSON: TJSONValue; 191 | LJSONResultsValue: TJSONValue; 192 | begin 193 | Result := False; 194 | FHttpClient.AcceptLanguage := 'en-US;q=0.5'; 195 | if not ALanguage.Trim.IsEmpty then 196 | FHttpClient.AcceptLanguage := ALanguage + ',' + FHttpClient.AcceptLanguage; 197 | try 198 | if FApiKey.IsEmpty then 199 | Exit(False); 200 | try 201 | LResponse := FHttpClient.Get(Format(GOOGLE_GEOLOCATION_TO_ADDRESS_URI, [AGeolocation.Latitude.ToString.Replace(',', '.'), AGeolocation.Longitude.ToString.Replace(',', '.'), FApiKey])); 202 | except 203 | on E: ENetException do 204 | Exit(False); 205 | end; 206 | LJSON := TJSONObject.ParseJSONValue(LResponse.ContentAsString); 207 | if not Assigned(LJSON) then 208 | Exit(False); 209 | try 210 | if (not LJSON.TryGetValue('status', LStatus)) or (LStatus <> 'OK') or not (LJSON is TJSONObject) then 211 | Exit(False); 212 | LJSONResultsValue := TJSONObject(LJSON).GetValue('results'); 213 | if (not Assigned(LJSONResultsValue)) or (not (LJSONResultsValue is TJSONArray)) or (TJSONArray(LJSONResultsValue).Count <= 0) then 214 | Exit(False); 215 | LJSONResultsValue := TJSONObject(TJSONArray(LJSONResultsValue).Items[0]).GetValue('address_components'); 216 | if (not Assigned(LJSONResultsValue)) or (not (LJSONResultsValue is TJSONArray)) or (TJSONArray(LJSONResultsValue).Count <= 0) then 217 | Exit(False); 218 | for I := 0 to TJSONArray(LJSONResultsValue).Count-1 do 219 | if (not (TJSONArray(LJSONResultsValue).Items[I] is TJSONObject)) or 220 | (not ProcessAddressComponent(TJSONObject(TJSONArray(LJSONResultsValue).Items[I]))) then 221 | Exit(False); 222 | Result := LJSON.TryGetValue('results[0].formatted_address', AFormattedAddress) and 223 | (AFormattedAddress <> '') and (ACountry <> '') and (AAdministrativeArea1 <> '') and 224 | (ALocality <> '') and (ASublocality <> ''); 225 | finally 226 | LJSON.Free; 227 | end; 228 | finally 229 | if not Result then 230 | begin 231 | AStreetNumber := ''; 232 | ARoute := ''; 233 | ASublocality := ''; 234 | ALocality := ''; 235 | AAdministrativeArea1 := ''; 236 | ACountry := ''; 237 | APostalCode := ''; 238 | AFormattedAddress := ''; 239 | end; 240 | end; 241 | end; 242 | 243 | end. 244 | -------------------------------------------------------------------------------- /delphi_google_geocoding/iPub.Rtl.Geolocation.pas: -------------------------------------------------------------------------------- 1 | unit iPub.Rtl.Geolocation; 2 | 3 | interface 4 | 5 | {$SCOPEDENUMS ON} 6 | 7 | uses 8 | { Delphi } 9 | System.Types, 10 | System.Math; 11 | 12 | type 13 | { TipSimpleGeolocation } 14 | 15 | TipSimpleGeolocation = packed record 16 | private 17 | FLatitude: Double; 18 | FLongitude: Double; 19 | procedure SetLatitude(AValue: Double); 20 | procedure SetLongitude(AValue: Double); 21 | public 22 | class function Create: TipSimpleGeolocation; overload; static; 23 | class function Create(const ALatitude, ALongitude: Double): TipSimpleGeolocation; overload; static; 24 | function DistanceInKmTo(const AGeolocation: TipSimpleGeolocation): Double; 25 | function DistanceInMetersTo(const AGeolocation: TipSimpleGeolocation): Double; 26 | function IsEmpty: Boolean; 27 | function IsSame(const AGeolocation: TipSimpleGeolocation): Boolean; overload; 28 | function IsSame(const ALatitude, ALongitude: Double): Boolean; overload; 29 | property Latitude: Double read FLatitude write SetLatitude; 30 | property Longitude: Double read FLongitude write SetLongitude; 31 | end; 32 | 33 | implementation 34 | 35 | uses 36 | { Delphi } 37 | FMX.Types; 38 | 39 | type 40 | { TipEpsilon } 41 | 42 | TipEpsilon = record 43 | const 44 | Geolocation = 1E-6; 45 | end; 46 | 47 | { TipRoundTo } 48 | 49 | TipRoundTo = record 50 | const 51 | Geolocation = -6; 52 | end; 53 | 54 | { TipSimpleGeolocation } 55 | 56 | class function TipSimpleGeolocation.Create(const ALatitude, ALongitude: Double): TipSimpleGeolocation; 57 | begin 58 | Result.Latitude := ALatitude; 59 | Result.Longitude := ALongitude; 60 | end; 61 | 62 | class function TipSimpleGeolocation.Create: TipSimpleGeolocation; 63 | begin 64 | Result.Latitude := 0.0; 65 | Result.Longitude := 0.0; 66 | end; 67 | 68 | function TipSimpleGeolocation.DistanceInKmTo( 69 | const AGeolocation: TipSimpleGeolocation): Double; 70 | begin 71 | Result := DistanceInMetersTo(AGeolocation) / 1000; 72 | end; 73 | 74 | function TipSimpleGeolocation.DistanceInMetersTo( 75 | const AGeolocation: TipSimpleGeolocation): Double; 76 | const 77 | EARTHS_RADIUS_IN_METERS = 6378137; 78 | var 79 | LDeltaLat, LDeltaLong, LA: Double; 80 | begin 81 | LDeltaLat := DegToRad(AGeolocation.Latitude - Latitude); 82 | LDeltaLong := DegToRad(AGeolocation.Longitude - Longitude); 83 | LA := Sin(LDeltaLat / 2) * Sin(LDeltaLat / 2) + Cos(DegToRad(Latitude)) * Cos(DegToRad(AGeolocation.Latitude)) * Sin(LDeltaLong / 2) * Sin(LDeltaLong / 2); 84 | Result := Abs(EARTHS_RADIUS_IN_METERS * 2 * ArcTan2(Sqrt(LA), Sqrt(1 - LA))); 85 | end; 86 | 87 | function TipSimpleGeolocation.IsEmpty: Boolean; 88 | begin 89 | Result := SameValue(Latitude, 0.0, TipEpsilon.Geolocation) and 90 | SameValue(Longitude, 0.0, TipEpsilon.Geolocation); 91 | end; 92 | 93 | function TipSimpleGeolocation.IsSame( 94 | const AGeolocation: TipSimpleGeolocation): Boolean; 95 | begin 96 | Result := IsSame(AGeolocation.Latitude, AGeolocation.Longitude); 97 | end; 98 | 99 | function TipSimpleGeolocation.IsSame(const ALatitude, ALongitude: Double): Boolean; 100 | begin 101 | Result := SameValue(Latitude, ALatitude, TipEpsilon.Geolocation) and 102 | SameValue(Longitude, ALongitude, TipEpsilon.Geolocation); 103 | end; 104 | 105 | procedure TipSimpleGeolocation.SetLatitude(AValue: Double); 106 | begin 107 | FLatitude := RoundTo(EnsureRange(AValue, -90, 90), TipRoundTo.Geolocation); 108 | end; 109 | 110 | procedure TipSimpleGeolocation.SetLongitude(AValue: Double); 111 | begin 112 | FLongitude := RoundTo(EnsureRange(AValue, -180, 180), TipRoundTo.Geolocation); 113 | end; 114 | 115 | end. 116 | -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/README.md: -------------------------------------------------------------------------------- 1 | # Delphi iOS - Exploring app crashes 2 | First of all, as most of you already know, there is a code from Erik Van Bilsen (blog Grijjy), which is spectacular to make an error report on firemonkey iOS apps to return the call stack of exceptions, so you can send it via email , or show using Log.d, for example, that I recommend to everyone: https://github.com/grijjy/JustAddCode/tree/master/ErrorReporting 3 | 4 | However, as Erik himself warns, 5 | > “the app may still crash… before we get a chance to handle the exception” 6 | 7 | This means that in some cases you will not be able to catch the error either with a try except, or with the error report of Erik, because the app will simply crash (close unexpectedly), and here I will explain how we can better investigate these crashes. 8 | 9 | ## Step 1 - iOS crash report file 10 | Every time your app crashes, the ios automatically generate a crash report file, even if you are not debugging, or even if your device is not connected to your Mac, this file is always generated. To access this file you need to: 11 | 1) Connect your device to your Mac 12 | 2) Open the **Xcode**, go to **Window > Devices and Simulators >** 13 | 3) In the Devices tab, select your device, and click in button "**View Device Logs**" 14 | ![(ss1.png](ss1.png) 15 | 4) You will see all crash reports of the iOS in your device, you can find your process and the last crash of it, and select it, as below: 16 | ![(ss2.png](ss2.png) 17 | See that this crash log does not display the call stack correctly, it only shows the addresses of the methods, but not their name, this is called unsymbolicated crash file, and that is what we will solve from now. 18 | 5) **Right click** > **Export Log**, as bellow: 19 | ![(ss3.png](ss3.png) 20 | 6) Save the file to 21 | ```/Users//PAServer/scratch-dir/-macOS/.crash``` 22 | - Example the path in my mac: 23 | ```/Users/vinicius/PAServer/scratch-dir/Vinícius-macOS/iPub4.crash``` 24 | - To be sure which way is right, you can also open a **Terminal**, and open the **Finder** in **Home Folder**, access the "**PAServer**" folder, access the “**scratch-dir**” folder and drag and drop the “**-macOS**” folder inside the **Terminal**, then will show the full path of the folder 25 | - Keep in mind this path, we will use it in the next steps. 26 | 7) Close the **Xcode** 27 | 28 | ## Step 2 - Check the app symbols file 29 | Every time delphi compiles for iOS, in the folder where we saved the crash file, in the Mac, the PAServer save your app file and your app symbols file, that is, the files **.app** and **.app.dSYM**. So in this folder make sure that there will be these two files and the crash file, as bellow: 30 | ![(ss4.png](ss4.png) 31 | 32 | **Note:** Is very important to compile the app with the debug mode to generate a complete dSYM file, even if the app has crashed with closed delphi debugging, this dSYM file that will contain the symbols we need (unit and method names) 33 | 34 | ## Step 3 - Finding the symbolicatecrash app in your Mac 35 | We need to have the path of the symbolicatecrash app in your Mac to do the last step. To find it: 36 | 1) Open the **Terminal** and type the command 37 | ```sudo find /Applications -name symbolicatecrash``` 38 | 2) Type the password and wait the result, as bellow: 39 | ![(ss5.png](ss5.png) 40 | 3) We will prefer the symbolicatecrash that contains “SharedFrameworks” in the path, because it is compatible with all Apple platforms 41 | In my case, the path is: 42 | ```/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash``` 43 | 44 | ## Step 4 - Symbolizing the crash file 45 | Now that we have: 46 | - the path inside the PAServer folder where our app files are located, and now also our crash file; 47 | - the path of the app symbolicatecrash in your mac; 48 | 49 | We will go to symbolize the crash file: 50 | 1) Open a new Terminal 51 | 2) Access the path inside the PAServer folder, where our file apps is inside, in my case, doing the command: 52 | ```cd /Users/vinicius/PAServer/scratch-dir/Vinícius-macOS``` 53 | 3) Set the DEVELOPER_DIR with the command: 54 | ```export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer``` 55 | 4) Use the app symbolicatecrash (the path that we get in previous step) to symbolize the crash file, with the command: 56 | ``` .crash .app > Symbolicate.crash``` 57 | - In my case: 58 | ```/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash iPub4.crash iPub4.app > Symbolicate.crash``` 59 | 5) Now you will check the new file “**Symbolicate.crash**” together with the file “**.crash**”, as bellow: 60 | ![(ss6.png](ss6.png) 61 | 6) Now just open the "**Symbolicate.crash**" file with the **Text Editor**, and you will see the crash file with complete informations of call stacks: 62 | ![(ss7.png](ss7.png) 63 | -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_ios_exploring_app_crashes/ss1.png -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_ios_exploring_app_crashes/ss2.png -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_ios_exploring_app_crashes/ss3.png -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/ss4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_ios_exploring_app_crashes/ss4.png -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/ss5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_ios_exploring_app_crashes/ss5.png -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/ss6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_ios_exploring_app_crashes/ss6.png -------------------------------------------------------------------------------- /delphi_ios_exploring_app_crashes/ss7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_ios_exploring_app_crashes/ss7.png -------------------------------------------------------------------------------- /delphi_ios_handle_incoming_url/README.md: -------------------------------------------------------------------------------- 1 | # Delphi iOS - Handle incoming url 2 | This tutorial will explain how to find out which url opened your Delphi Firemonkey iOS app. This work for Custom Schemes and for Universal Links. Let's go. 3 | In the file **iOSapi.Foundation.pas** put the code: 4 | 5 | // Dave Nottage code (https://www.delphiworlds.com/) 6 | NSUserActivityPersistentIdentifier = NSString; 7 | 8 | TNSUserActivityBlockMethod1 = procedure(inputStream: NSInputStream; outputStream: NSOutputStream; error: NSError) of object; 9 | TNSUserActivityBlockMethod2 = procedure of object; 10 | 11 | NSUserActivityClass = interface(NSObjectClass) 12 | ['{412EAEBF-5927-4D01-B83F-69D3B5DFE7B5}'] 13 | {class} procedure deleteAllSavedUserActivitiesWithCompletionHandler(handler: TNSUserActivityBlockMethod2); cdecl; 14 | [MethodName('deleteSavedUserActivitiesWithPersistentIdentifiers:completionHandler:')] 15 | {class} procedure deleteSavedUserActivitiesWithPersistentIdentifiers(persistentIdentifiers: NSArray; handler: TNSUserActivityBlockMethod2); cdecl; 16 | end; 17 | 18 | NSUserActivity = interface(NSObject) 19 | ['{B8C2F6C9-31FE-4282-B7CA-98C96E163033}'] 20 | function activityType: NSString; cdecl; 21 | procedure addUserInfoEntriesFromDictionary(otherDictionary: NSDictionary); cdecl; 22 | procedure becomeCurrent; cdecl; 23 | function delegate: Pointer; cdecl; 24 | function expirationDate: NSDate; cdecl; 25 | procedure getContinuationStreamsWithCompletionHandler(completionHandler: TNSUserActivityBlockMethod1); cdecl; 26 | function initWithActivityType(activityType: NSString): Pointer; cdecl; 27 | procedure invalidate; cdecl; 28 | function isEligibleForHandoff: Boolean; cdecl; 29 | function isEligibleForPrediction: Boolean; cdecl; 30 | function isEligibleForPublicIndexing: Boolean; cdecl; 31 | function isEligibleForSearch: Boolean; cdecl; 32 | function keywords: NSSet; cdecl; 33 | function needsSave: Boolean; cdecl; 34 | function persistentIdentifier: NSUserActivityPersistentIdentifier; cdecl; 35 | function referrerURL: NSURL; cdecl; 36 | function requiredUserInfoKeys: NSSet; cdecl; 37 | procedure resignCurrent; cdecl; 38 | procedure setDelegate(delegate: Pointer); cdecl; 39 | procedure setEligibleForHandoff(eligibleForHandoff: Boolean); cdecl; 40 | procedure setEligibleForPrediction(eligibleForPrediction: Boolean); cdecl; 41 | procedure setEligibleForPublicIndexing(eligibleForPublicIndexing: Boolean); cdecl; 42 | procedure setEligibleForSearch(eligibleForSearch: Boolean); cdecl; 43 | procedure setExpirationDate(expirationDate: NSDate); cdecl; 44 | procedure setKeywords(keywords: NSSet); cdecl; 45 | procedure setNeedsSave(needsSave: Boolean); cdecl; 46 | procedure setPersistentIdentifier(persistentIdentifier: NSUserActivityPersistentIdentifier); cdecl; 47 | procedure setReferrerURL(referrerURL: NSURL); cdecl; 48 | procedure setRequiredUserInfoKeys(requiredUserInfoKeys: NSSet); cdecl; 49 | procedure setSupportsContinuationStreams(supportsContinuationStreams: Boolean); cdecl; 50 | procedure setTargetContentIdentifier(targetContentIdentifier: NSString); cdecl; 51 | procedure setTitle(title: NSString); cdecl; 52 | procedure setUserInfo(userInfo: NSDictionary); cdecl; 53 | procedure setWebpageURL(webpageURL: NSURL); cdecl; 54 | function supportsContinuationStreams: Boolean; cdecl; 55 | function targetContentIdentifier: NSString; cdecl; 56 | function title: NSString; cdecl; 57 | function userInfo: NSDictionary; cdecl; 58 | function webpageURL: NSURL; cdecl; 59 | end; 60 | TNSUserActivity = class(TOCGenericImport) end; 61 | 62 | ... 63 | function NSUserActivityTypeBrowsingWeb: NSString; 64 | ... 65 | implementation 66 | ... 67 | function NSUserActivityTypeBrowsingWeb: NSString; 68 | begin 69 | result := CocoaNSStringConst(FoundationFwk, 'NSUserActivityTypeBrowsingWeb'); 70 | end; 71 | 72 | In the file **FMX.Platform.iOS.pas**, in the TApplicationDelegate class, in the private section, put the code: 73 | 74 | class function applicationContinueUserActivityRestorationHandler(self: id; _cmd: SEL; application: PUIApplication; 75 | userActivity: Pointer; restorationHandler: Pointer; restorableObjects: Pointer): Boolean; cdecl; static; 76 | 77 | In the file **FMX.Platform.iOS.pas**, in the implementation of the method TApplicationDelegate.CreateDelegateMetaClass, before the line "objc_registerClassPair(DelegateClass);", put the code: 78 | 79 | class_addMethod(DelegateClass, sel_getUid('application:continueUserActivity:restorationHandler:'), 80 | @applicationContinueUserActivityRestorationHandler, 'B@:@@@@'); 81 | 82 | In the file **FMX.Platform.iOS.pas**, in the TApplicationDelegate implementation, put the code: 83 | 84 | class function TApplicationDelegate.applicationContinueUserActivityRestorationHandler( 85 | self: id; _cmd: SEL; application: PUIApplication; userActivity, 86 | restorationHandler, restorableObjects: Pointer): Boolean; 87 | var 88 | LUserActivity: NSUserActivity; 89 | LURLString: string; 90 | begin 91 | Result := False; 92 | if Assigned(userActivity) then 93 | begin 94 | LUserActivity := TNSUserActivity.Wrap(userActivity); 95 | if NSStrToStr(LUserActivity.activityType) = NSStrToStr(NSUserActivityTypeBrowsingWeb) then 96 | begin 97 | if Assigned(LUserActivity.webpageURL) then 98 | LURLString := NSStrToStr(LUserActivity.webpageURL.absoluteString) 99 | else 100 | LURLString := string.Empty; 101 | 102 | Result := PlatformCocoaTouch.HandleApplicationEvent(TApplicationEvent.OpenURL, 103 | TiOSOpenApplicationContext.Create(string.Empty, LURLString, nil)); 104 | end; 105 | end; 106 | end; 107 | 108 | ## Usage 109 | 110 | uses 111 | System.Messaging, 112 | FMX.Platform, 113 | FMX.Platform.iOS, 114 | FMX.Dialogs; 115 | 116 | constructor TipUrlHandler.Create; 117 | begin 118 | inherited Create; 119 | TMessageManager.DefaultManager.SubscribeToMessage(TApplicationEventMessage, ApplicationEventMessageHandler); 120 | end; 121 | 122 | destructor TipUrlHandler.Destroy; 123 | begin 124 | TMessageManager.DefaultManager.Unsubscribe(TApplicationEventMessage, ApplicationEventMessageHandler, True); 125 | inherited; 126 | end; 127 | 128 | procedure TipUrlHandler.ApplicationEventMessageHandler(const ASender: TObject; 129 | const AMessage: TMessage); 130 | begin 131 | case TApplicationEventData(TApplicationEventMessage(AMessage).Value).Event of 132 | TApplicationEvent.OpenUrl: 133 | begin 134 | Showmessage(TiOSOpenApplicationContext(TApplicationEventData(TApplicationEventMessage(AMessage).Value).Context).URL); 135 | end; 136 | else 137 | end; 138 | end; 139 | -------------------------------------------------------------------------------- /delphi_ios_universal_links/README.md: -------------------------------------------------------------------------------- 1 | 2 | # iOS Universal Links 3 | This is a tutorial of how to add a new Universal Link to your Delphi Firemonkey iOS app, to open your app by a link. In this tutorial we will add the Universal Link: https://yoursite.com/app/ 4 | 5 | ##### Why Universal Links and not Custom Schemes? 6 | For two reasons: 7 | 1) Because the custom scheme is not unique, if you configure the Custom Scheme ```yourapp://```, nothing will prevent other developers from configuring this same Custom Scheme to open their application and not yours. Universal Link is unique, it will only be linked to your App, and it cannot be linked to the third party app. 8 | 2) Universal Link and Custom Schemes works only for installed applications. Even with Universal Link, you can send the link to a user that if he doesn't have your app installed, the link won't open your app, much like the AppStore, but it will open your website's page, just like any link does . But we can make a redirect to the AppStore so that the user installs the app (I will also teach this throughout this tutorial), that is, when you send a Universal Link to someone and they click, if they have your app installed, yours app will open, but if it doesn't have your app installed, it will open the AppStore. This is impossible to do with Custom Schemes. 9 | 10 | ### 1) Configure your app to register approved domains 11 | 1) Enter in your apple developer account at [developer.apple.com](https://developer.apple.com/). 12 | 2) Enable **Associated Domains** on your app identifier. 13 | 3) Generate a new provision profile, and set it in the XCode. 14 | 15 | ### 2) Editing the file "Entitlement.TemplateiOS.xml" 16 | You will find this file in your project folder. Unfortunately, I don't know a correct way to edit this model file, due to some limitations of Delphi itself. The only way that worked for me was to take the final entitlement file generated by compiling your iOS program and editing it. (I'm open to suggestions that work) 17 | 1) Save one copy of the file ```\Entitlement.TemplateiOS.xml``` for backup. 18 | 2) Compile for iOS. 19 | 3) Go to the iOS ouput directory of your project. Usually ```\iOSDevice64\Release\``` 20 | 4) Open in the NotePad the file ```.entitlements``` and copy the content. 21 | 5) Go back to the project directory, and open in the NotePad the file ```\Entitlement.TemplateiOS.xml```, delete all content and paste the new content. 22 | 6) Search for the lines 23 | 24 | com.apple.developer.associated-domains 25 | * 26 | 27 | 7) Replace with these new lines: 28 | 29 | com.apple.developer.associated-domains 30 | 31 | applinks:yoursite.com 32 | 33 | 34 | Remarks: Replace the ```yoursite.com``` with your domain. Although your Universal Link is for example https://yoursite.com/app or https://yoursite.com/teste/ios, you must only put the domain, as this is where we will put our ```.well-known/apple-app-site-association``` becoming accessible at this URL "https://yoursite.com/.well-known/apple-app-site-association" (I'll explain more about this file in the next step) 35 | 36 | 8) Save the new content of the file ```\Entitlement.TemplateiOS.xml```. 37 | 38 | ### 3) Creating the file ".well-known/apple-app-site-association" in your website 39 | Now you must create the file that will be on your website to confirm that you are the owner of the app and inform which URLs are linked to your app. You can link multiple Universal Links to your app, and also Universal Links from multiple apps, as I can have multiple apps and a single domain, and everything is done just in that file. But to be more simplistic, I'll explain how to add just one link to just one app. 40 | 41 | 1) Open again in the NotePad the file ```\Entitlement.TemplateiOS.xml```, find the line ```application-identifier```. Below it will have your appID, it will be something like ```A51GDSSDSJ.com.yourapp```. So, copy your appID. 42 | 2) Open the NotePad, paste the following content: 43 | 44 | { 45 | "applinks": { 46 | "apps": [], 47 | "details": [ 48 | { 49 | "appID": "A51GDSSDSJ.com.yourapp", 50 | "paths": [ "/app/" ] 51 | } 52 | ] 53 | } 54 | } 55 | 56 | Replace the ```A51GDSSDSJ.com.yourapp``` with your appID that we copied in the last step, and replace the ```/app/``` by the path you want. I used the path ```/app/``` because I want my Universal Link to be "https://yoursite.com/app/". You will be able to use wildcards on your Universal Link, which is mainly used when we want Universal Link to not only open our app, but also open it by passing some type of information. For example, if I put the path ```/app/*```, I could send to the user the link "https://yoursite.com/app/sale_form", then my app could analyze the link that was used to open it and then open it with the sale form open, for example. 57 | 3) Save the content, which we edited in NotePad in the previous step, on your computer with the file name ```apple-app-site-association```. Note: This file has no extension. 58 | 4) On your website host, in the root directory create a folder called ```.well-known``` and place the file we created in the previous step in this folder, so that the file is accessible via the url "https://yoursite.com/.well-known/apple-app-site-association". 59 | 5) This file, ```apple-app-site-association```, must be configured on your web server as ```application/json``` MIME type. As I use the IIS web server, and for those who use it, just create a file called ```web.config``` with the following content: 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Save this file in the same folder of your ```apple-app-site-association``` file. 72 | 6) Test the URL "https://yoursite.com/.well-known/apple-app-site-association" in your browser. If you were unable to access, you have missed one of the previous steps. 73 | 74 | ### 4) Testing 75 | 1) First, you should unistall your app, since iOS only checks and saves the Universal Links for each application at the installation (I don't know if when updating an app iOS will also do this verification, you will have to test this). 76 | It works as follows: On installation, iOS will check the key value ```com.apple.developer.associated-domains``` that is on your ```.entitlements``` file, which is our domain, and then iOS will try to access the Apple file from our domain "https://yoursite.com/.well-known/apple-app-site-association". The iOS will then try to find the paths (our Universal Links) by the appID and then save internally on the system, and will be saved until the user uninstalls the app. 77 | 2) Send the link on WhatsApp, GMail, by SMS or any other application and click on the link. If the iOS opened your app directly, it worked. If the iOS opened Safari or any other browser, it didn't work and you missed a previous step. 78 | 79 | ### 5) Opening the AppStore when the user don't have the app installed 80 | 81 | As I explained at the beginning, Universal Links only works for applications already installed. That is, when the user who does not have the app installed clicks on the link, it will open the page of your website in the browser, the URL that is linked, like any link. Then we will make a code to redirect from our website to the AppStore on the page of our app. Following the following steps: 82 | 1) There are several ways to redirect, but I will do it here using a simple php code: 83 | 84 | 93 | 94 | If your app is already on the AppStore, put its id in place of ```id00000000```. In this code I put a android redirection, this is because you can set the same link in your app in ios and android. Save this file with the name ```index.php``` and put it in the folder of your universal link on the host, to stay like this "https://yoursite.com/app/index.php". 95 | 2) I particularly use wildcard on my Universal Links, so I have to make the above php code run on any subpath, for example "https://yoursite.com/app/lahahaghgdhgsdhgfdhaasdf". For that, we have to configure a Friendly URL on our web server. How I use the IIS web server, and for those who use it, just create a file called ```web.config``` with the following content: 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Save this file in the root path of your website. 125 | 126 | ### 6) Recommendation: Set the Android App Links too 127 | Although this tutorial is only for setting up Universal Links for your app on iOS, you should do the same for Android, which works in much the same way, but under another name: Android App Links. You can (should) configure Android App Links with the same Universal Link as iOS, so you can send the same link to your users, which will work to open your app on both Android and iOS. It will work identically. 128 | 129 | ### 7) Identify the URL that opened your app 130 | If you use more than 1 Universal Link, or your Universal Link has a wildcard, you will probably want to know which link opened your app. As this task is a little more complex to be done with Delphi, I made a tutorial just for that which works for Universal Links and Custom Schemes: https://github.com/viniciusfbb/fmx_tutorials/tree/master/delphi_ios_handle_incoming_url/README.md 131 | 132 | ### 8) Utilities 133 | The sky is the limit, so there are endless uses, but I will highlight special one: QRCode. For example, if your app is for customers of restaurants, you can print the QRCode containing your app's Universal Link, to paste in restaurants. Thus, when the user points the camera, he will open his app, if installed, or open the AppStore to install his app. -------------------------------------------------------------------------------- /delphi_system_bars/README.md: -------------------------------------------------------------------------------- 1 | # Firemonkey system bars 2 | 3 | One of the difficulties that we have in firemonkey is to have more control of the system bars and this is so important for the design of the app that it should be done in a simple way via form properties, but as this is not yet possible I will teach here how to better configure the system bars (status bar and navigation bar), changing the background color or making them background full transparent, including on the splash screen, allowing a more beautiful app: 4 | 5 |

6 | 7 |

8 | 9 | ## System bars 10 | 11 | After added the units of this repository in your project, in your forms you can simple: 12 | 13 | ```delphi 14 | uses 15 | iPub.FMX.SystemBars; 16 | 17 | ... 18 | 19 | Form1.SystemBars.StatusBarBackgroundColor := TAlphaColors.Black; 20 | Form1.SystemBars.NavigationBarBackgroundColor := TAlphaColors.Black; 21 | ``` 22 | 23 | But if you want to draw something underneath, like having an image in the background, you can change the visibility: 24 | 25 | ```delphi 26 | uses 27 | iPub.FMX.SystemBars; 28 | 29 | ... 30 | 31 | Form1.SystemBars.StatusBarBackgroundColor := $20000000; 32 | Form1.SystemBars.NavigationBarBackgroundColor := $20000000; 33 | Form1.SystemBars.Visibility := TipFormSystemBars.TVisibilityMode.VisibleAndOverlap; 34 | ``` 35 | 36 | When TVisibilityMode.VisibleAndOverlap is being used, you can see the size of the system bars through the code: 37 | 38 | ```delphi 39 | uses 40 | iPub.FMX.SystemBars; 41 | 42 | ... 43 | 44 | Form1.SystemBars.Visibility := TipFormSystemBars.TVisibilityMode.VisibleAndOverlap; 45 | Padding.Rect := Form1.SystemBars.Insets; 46 | ``` 47 | 48 | There is also the TappableInsets property, which are the distances in which the system bars, without gesture bar, are overlapping the sides of the form (top, left, right and bottom). 49 | 50 | You can also capture changes to Insets. This can be useful when the app is rotated. See: 51 | 52 | ```delphi 53 | uses 54 | iPub.FMX.SystemBars; 55 | 56 | ... 57 | 58 | procedure TForm1.SystemBarsInsetsChange(Sender: TObject); 59 | begin 60 | end; 61 | 62 | Form1.SystemBars.OnInsetsChange := SystemBarsInsetsChange; 63 | ``` 64 | 65 | ## Android Splash screen - System bars and background 66 | 67 | The class TAndroidSystemBars is just to use in runtime, after the application full launch. But if you don't change the default "styles.xml" and "splash_image_def.xml" provided by Delphi, you will have an ugly system bars or simply not predictable (some devices may be white, others black or even gray), and the splash screen background will be always black. To fix the system bars colors and background color of splash screen you should change the "styles.xml" and "splash_image_def.xml" files. 68 | 69 | ### Creating "styles.xml" 70 | 71 | Create one file called "styles.xml" with the following content: 72 | 73 | ```xml 74 | 75 | 81 | 82 | ``` 83 | 84 | In this content above I used the white color example (#ffffff) but you can change it to your color but note that in navigationBarColor the color need to start with "#01" to set the alpha value to 1 to solve problems found in LG devices 85 | 86 | Save the file in one subdirectory of your project, like "resources" subdirectory, for example. 87 | 88 | ### Creating "splash_image_def.xml" 89 | 90 | Create one file called "splash_image_def.xml" with the following content: 91 | 92 | ```xml 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | ``` 105 | 106 | In this content above I used one white color example (#ffffff) but you can change it to your color. 107 | 108 | Save the file in one subdirectory of your project, like "resources" subdirectory, for example. 109 | 110 | ### Deploying the "styles.xml" and "splash_image_def.xml" 111 | 112 | 1) Open your Project > Deployment, then you need to apply the following steps below for Android 32/64 bits in Release and Debug mode: 113 | 2) Look in the "Local Name" column and disable these 3 default files: "styles.xml", "styles-v21.xml" and "splash_image_def.xml" 114 | 3) Add your own "styles.xml" file and set the Remote Path field to "res\values" 115 | 4) Add your own "splash_image_def.xml" file and set the Remote Path field to "res\drawable" 116 | 117 | Now you can uninstall your app, compile and run to see the difference ;) 118 | 119 | ## Compatibility 120 | 121 | I made some tests with Delphi 11 Alexandria, Delphi 10.4.2 Sydney and Delphi 10.3.3 Rio running the app in some devices: 122 | 123 | GOOGLE - G020A Android64 11.0.0 (API level 30) 124 | LG - LM-X430 Android32 10.0.0 (API level 29) 125 | SAMSUNG - SM-A013M Android32 10.0.0 (API level 29) 126 | SAMSUNG - SM-G955F Android64 9.0.0 (API level 28) 127 | SAMSUNG - SM-G935F Android64 8.0.0 (API level 26) 128 | MOTOROLA - MotoG3 Android32 6.0.0 (API level 23) 129 | 130 | All worked perfectly. But these codes should work well on all android versions supported by delphi (Android 6 to Android 11), although it is likely to work on older Android devices as well. 131 | -------------------------------------------------------------------------------- /delphi_system_bars/iPub.FMX.SystemBars.Android.pas: -------------------------------------------------------------------------------- 1 | {************************************************************************} 2 | { } 3 | { iPub.FMX.SystemBars } 4 | { } 5 | { Copyright (c) 2021-2022 iPub } 6 | { https://github.com/viniciusfbb/fmx_tutorials } 7 | { } 8 | { Use of this source code is governed by a MIT license that can be found } 9 | { at https://opensource.org/licenses/MIT } 10 | { } 11 | {************************************************************************} 12 | unit iPub.FMX.SystemBars.Android; 13 | 14 | interface 15 | 16 | {$SCOPEDENUMS ON} 17 | {$IFDEF ANDROID} 18 | 19 | implementation 20 | 21 | uses 22 | { Delphi } 23 | System.Classes, 24 | System.SysUtils, 25 | System.Types, 26 | System.UITypes, 27 | System.Messaging, 28 | System.Math, 29 | System.Math.Vectors, 30 | System.Generics.Collections, 31 | FMX.Types, 32 | FMX.Utils, 33 | FMX.Forms, 34 | FMX.Platform, 35 | FMX.Platform.Android, 36 | FMX.Platform.UI.Android, 37 | FMX.Platform.Screen.Android, 38 | Androidapi.JNIBridge, 39 | Androidapi.JNI.GraphicsContentViewText, 40 | Androidapi.Helpers, 41 | Androidapi.Jni, 42 | Androidapi.JNI.Embarcadero, 43 | Androidapi.JNI.JavaTypes, 44 | Androidapi.JNI.Os, 45 | Androidapi.JNI.App, 46 | Androidapi.JNI.Widget, 47 | 48 | { iPub } 49 | iPub.FMX.SystemBars; 50 | 51 | type 52 | { IipSystemBarsServiceAndroid } 53 | 54 | IipSystemBarsServiceAndroid = interface(TipFormSystemBars.IFMXWindowSystemBarsService) 55 | ['{77586947-BF49-4938-9A34-51588E8BD915}'] 56 | procedure CheckInsetsChanges(const AForm: TCommonCustomForm); 57 | function HasGestureNavigationBar(const AForm: TCommonCustomForm): Boolean; 58 | procedure TryFixInvisibleMode; 59 | end; 60 | 61 | { TipSystemBarsServiceAndroid } 62 | 63 | TipSystemBarsServiceAndroid = class(TInterfacedObject, IipSystemBarsServiceAndroid, 64 | TipFormSystemBars.IFMXWindowSystemBarsService, IFMXWindowSystemStatusBarService, 65 | IFMXFullScreenWindowService) 66 | strict private 67 | type 68 | TOnApplyWindowInsetsListener = class(TJavaLocal, JView_OnApplyWindowInsetsListener) 69 | strict private 70 | FChangeChecksEnabled: Boolean; 71 | public 72 | constructor Create; 73 | function onApplyWindowInsets(v: JView; insets: JWindowInsets): JWindowInsets; cdecl; 74 | property ChangeChecksEnabled: Boolean read FChangeChecksEnabled write FChangeChecksEnabled; 75 | end; 76 | 77 | TOnAttachStateChangeListener = class(TJavaLocal, JView_OnAttachStateChangeListener) 78 | public 79 | procedure onViewAttachedToWindow(v: JView); cdecl; 80 | procedure onViewDetachedFromWindow(v: JView); cdecl; 81 | end; 82 | 83 | TOnWindowFocusChangeListener = class(TJavaLocal, JViewTreeObserver_OnWindowFocusChangeListener) 84 | public 85 | procedure onWindowFocusChanged(hasFocus: Boolean); cdecl; 86 | end; 87 | 88 | TOnTouchListener = class(TJavaLocal, JView_OnTouchListener) 89 | public 90 | function onTouch(v: JView; event: JMotionEvent): Boolean; cdecl; 91 | end; 92 | 93 | TWindowServiceFix = class(TInterfacedObject, IFMXWindowService) 94 | strict private 95 | FDefaultWindowService: IFMXWindowService; 96 | public 97 | constructor Create; 98 | destructor Destroy; override; 99 | { IFMXWindowService } 100 | function FindForm(const AHandle: TWindowHandle): TCommonCustomForm; 101 | function CreateWindow(const AForm: TCommonCustomForm): TWindowHandle; 102 | procedure DestroyWindow(const AForm: TCommonCustomForm); 103 | procedure ReleaseWindow(const AForm: TCommonCustomForm); 104 | procedure SetWindowState(const AForm: TCommonCustomForm; const AState: TWindowState); 105 | procedure ShowWindow(const AForm: TCommonCustomForm); 106 | procedure HideWindow(const AForm: TCommonCustomForm); 107 | function ShowWindowModal(const AForm: TCommonCustomForm): TModalResult; 108 | procedure InvalidateWindowRect(const AForm: TCommonCustomForm; R: TRectF); 109 | procedure InvalidateImmediately(const AForm: TCommonCustomForm); 110 | procedure SetWindowRect(const AForm: TCommonCustomForm; ARect: TRectF); 111 | function GetWindowRect(const AForm: TCommonCustomForm): TRectF; 112 | function GetClientSize(const AForm: TCommonCustomForm): TPointF; 113 | procedure SetClientSize(const AForm: TCommonCustomForm; const ASize: TPointF); 114 | procedure SetWindowCaption(const AForm: TCommonCustomForm; const ACaption: string); 115 | procedure SetCapture(const AForm: TCommonCustomForm); 116 | procedure ReleaseCapture(const AForm: TCommonCustomForm); 117 | function ClientToScreen(const AForm: TCommonCustomForm; const Point: TPointF): TPointF; 118 | function ScreenToClient(const AForm: TCommonCustomForm; const Point: TPointF): TPointF; 119 | procedure BringToFront(const AForm: TCommonCustomForm); 120 | procedure SendToBack(const AForm: TCommonCustomForm); 121 | procedure Activate(const AForm: TCommonCustomForm); 122 | function GetWindowScale(const AForm: TCommonCustomForm): Single; 123 | function CanShowModal: Boolean; 124 | end; 125 | strict private 126 | FAfterCreateFormHandleMessageId: Integer; 127 | FBeforeDestroyFormHandleMessageId: Integer; 128 | FDefaultFullScreenService: IFMXFullScreenWindowService; 129 | FFormActivateMessageId: Integer; 130 | FFormReleasedMessageId: Integer; 131 | FDefaultFormVisibility: TDictionary; 132 | FOnApplyWindowInsetsListener: TOnApplyWindowInsetsListener; 133 | FOnAttachStateChangeListener: TOnAttachStateChangeListener; 134 | FOnTouchListener: TOnTouchListener; 135 | FOnWindowFocusChangeListener: TOnWindowFocusChangeListener; 136 | FRegisteredBarsService: Boolean; 137 | FRegisteredStatusBarService: Boolean; 138 | FVirtualKeyboardBounds: TRect; 139 | FVirtualKeyboardMessageId: Integer; 140 | FWindowServiceFix: IFMXWindowService; 141 | class function AbsoluteRectToScreenScaled(const AAbsoluteRect: TRect): TRectF; static; 142 | procedure AfterCreateFormHandle(const ASender: TObject; const AMessage: TMessage); 143 | procedure ApplicationEventHandler(const ASender: TObject; const AMessage: TMessage); 144 | procedure BeforeDestroyFormHandle(const ASender: TObject; const AMessage: TMessage); 145 | function CanFormChangeSystemBars(const AForm: TCommonCustomForm): Boolean; 146 | procedure CheckInsetsChanges(const AForm: TCommonCustomForm); 147 | function DoGetAbsoluteInsets(const AForm: TCommonCustomForm): TRect; 148 | function DoGetAbsoluteTappableInsets(const AForm: TCommonCustomForm): TRect; 149 | procedure DoSetVisibility(const AForm: TCommonCustomForm; const AMode: TipFormSystemBars.TVisibilityMode); 150 | procedure FormActivate(const ASender: TObject; const AMessage: TMessage); 151 | procedure FormReleased(const ASender: TObject; const AMessage: TMessage); 152 | function GetAbsoluteInsets(const AForm: TCommonCustomForm): TRect; 153 | function GetAbsoluteTappableInsets(const AForm: TCommonCustomForm): TRect; 154 | class function GetMainActivityContentView: JViewGroup; static; 155 | class function GetWindowDecorView: JView; static; 156 | function HasGestureNavigationBar(const AForm: TCommonCustomForm): Boolean; 157 | function HasSystemBars(const AForm: TCommonCustomForm): Boolean; 158 | function IsDarkTheme: Boolean; 159 | procedure RefreshView(const AView: JView); 160 | function RemoveKeyboardOverlappedBars(const AInsets: TRectF; const AForm: TCommonCustomForm): TRectF; 161 | procedure TryFixInvisibleMode; 162 | procedure VirtualKeyboardChangeHandler(const ASender: TObject; const AMessage: System.Messaging.TMessage); 163 | public 164 | constructor Create; 165 | destructor Destroy; override; 166 | { IFMXWindowSystemBarsService } 167 | function GetInsets(const AForm: TCommonCustomForm): TRectF; 168 | function GetTappableInsets(const AForm: TCommonCustomForm): TRectF; 169 | procedure SetNavigationBarBackgroundColor(const AForm: TCommonCustomForm; const AColor: TAlphaColor); 170 | { IFMXWindowSystemStatusBarService / IFMXWindowSystemBarsService } 171 | procedure IFMXWindowSystemStatusBarService.SetBackgroundColor = SetStatusBarBackgroundColor; 172 | procedure SetStatusBarBackgroundColor(const AForm: TCommonCustomForm; const AColor: TAlphaColor); 173 | procedure SetVisibility(const AForm: TCommonCustomForm; const AMode: TipFormSystemBars.TVisibilityMode); 174 | { IFMXFullScreenWindowService } 175 | function GetFullScreen(const AForm: TCommonCustomForm): Boolean; 176 | procedure SetFullScreen(const AForm: TCommonCustomForm; const AValue: Boolean); 177 | procedure SetShowFullScreenIcon(const AForm: TCommonCustomForm; const AValue: Boolean); 178 | end; 179 | 180 | { TAndroid9 } 181 | 182 | TAndroid9 = class 183 | public type 184 | { WindowManager.LayoutParams } 185 | 186 | JWindowManager_LayoutParamsClass = interface(Androidapi.JNI.GraphicsContentViewText.JWindowManager_LayoutParamsClass) 187 | ['{D657960B-0CEA-4A56-9FC1-EB165CCB4891}'] 188 | {class} function _GetLAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT: Integer; cdecl; 189 | {class} function _GetLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES: Integer; cdecl; 190 | {class} property LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT: Integer read _GetLAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; 191 | {class} property LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES: Integer read _GetLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; 192 | end; 193 | 194 | [JavaSignature('android/view/WindowManager$LayoutParams')] 195 | JWindowManager_LayoutParams = interface(Androidapi.JNI.GraphicsContentViewText.JWindowManager_LayoutParams) 196 | ['{C849F24F-5CAC-425E-B5B0-0EE72E71966B}'] 197 | function _GetlayoutInDisplayCutoutMode: Integer; cdecl; 198 | procedure _SetlayoutInDisplayCutoutMode(Value: Integer); cdecl; 199 | property layoutInDisplayCutoutMode: Integer read _GetlayoutInDisplayCutoutMode write _SetlayoutInDisplayCutoutMode; 200 | end; 201 | TJWindowManager_LayoutParams = class(TJavaGenericImport) end; 202 | end; 203 | 204 | { TAndroid10 } 205 | 206 | TAndroid10 = class 207 | public type 208 | { Insets } 209 | 210 | JInsetsClass = interface(JObjectClass) 211 | ['{BDB53B96-47AA-4A43-A08F-7648EE48A7D9}'] 212 | end; 213 | 214 | [JavaSignature('android/graphics/Insets')] 215 | JInsets = interface(JObject) 216 | ['{A0703F81-D34D-4D59-8D3F-5E1D7DD3192D}'] 217 | function _Getbottom: Integer; cdecl; 218 | function _Getleft: Integer; cdecl; 219 | function _Getright: Integer; cdecl; 220 | function _Gettop: Integer; cdecl; 221 | property bottom: Integer read _Getbottom; 222 | property left: Integer read _Getleft; 223 | property right: Integer read _Getright; 224 | property top: Integer read _Gettop; 225 | end; 226 | TJInsets = class(TJavaGenericImport) end; 227 | 228 | 229 | { WindowInsets } 230 | 231 | JWindowInsetsClass = interface(Androidapi.JNI.GraphicsContentViewText.JWindowInsetsClass) 232 | ['{6BCCFCCA-A7F0-4740-B4D6-D03286DAD89C}'] 233 | end; 234 | 235 | [JavaSignature('android/view/WindowInsets')] 236 | JWindowInsets = interface(Androidapi.JNI.GraphicsContentViewText.JWindowInsets) 237 | ['{05AFC51B-3683-4DD1-BB1A-22799899142B}'] 238 | function getMandatorySystemGestureInsets: JInsets; cdecl; 239 | function getTappableElementInsets: JInsets; cdecl; 240 | end; 241 | TJWindowInsets = class(TJavaGenericImport) end; 242 | 243 | 244 | { Window } 245 | 246 | JWindowClass = interface(Androidapi.JNI.GraphicsContentViewText.JWindowClass) 247 | ['{FE27644B-35A7-48EA-90A3-42CC8CB5E1B9}'] 248 | end; 249 | 250 | [JavaSignature('android/view/Window')] 251 | JWindow = interface(Androidapi.JNI.GraphicsContentViewText.JWindow) 252 | ['{25E670D9-5AFB-4C61-9703-D5E5CD89F66F}'] 253 | procedure setNavigationBarContrastEnforced(enforceContrast: Boolean); cdecl; 254 | procedure setStatusBarContrastEnforced(enforceContrast: Boolean); cdecl; 255 | end; 256 | TJWindow = class(TJavaGenericImport) end; 257 | end; 258 | 259 | { TAndroid11 } 260 | 261 | TAndroid11 = class 262 | public type 263 | { WindowInsets.Type } 264 | 265 | JWindowInsets_TypeClass = interface(JObjectClass) 266 | ['{847139A7-6187-4D24-86FE-44BB6017F246}'] 267 | {class} function captionBar: Integer; cdecl; 268 | {class} function displayCutout: Integer; cdecl; 269 | {class} function ime: Integer; cdecl; 270 | {class} function mandatorySystemGestures: Integer; cdecl; 271 | {class} function navigationBars: Integer; cdecl; 272 | {class} function statusBars: Integer; cdecl; 273 | {class} function systemBars: Integer; cdecl; 274 | {class} function systemGestures: Integer; cdecl; 275 | {class} function tappableElement: Integer; cdecl; 276 | end; 277 | 278 | [JavaSignature('android/view/WindowInsets$Type')] 279 | JWindowInsets_Type = interface(JObject) 280 | ['{380D2B14-3C81-4F30-888E-1465CF5D2BCF}'] 281 | end; 282 | TJWindowInsets_Type = class(TJavaGenericImport) end; 283 | 284 | 285 | { WindowInsetsController } 286 | 287 | JWindowInsetsControllerClass = interface(JObjectClass) 288 | ['{D7FCB1D1-65AD-4A41-B873-6F8CD3B1E6F4}'] 289 | {class} function _GetAPPEARANCE_LIGHT_NAVIGATION_BARS: Integer; cdecl; 290 | {class} function _GetAPPEARANCE_LIGHT_STATUS_BARS: Integer; cdecl; 291 | {class} function _GetBEHAVIOR_SHOW_BARS_BY_SWIPE: Integer; cdecl; 292 | {class} function _GetBEHAVIOR_SHOW_BARS_BY_TOUCH: Integer; cdecl; 293 | {class} function _GetBEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE: Integer; cdecl; 294 | {class} property APPEARANCE_LIGHT_NAVIGATION_BARS: Integer read _GetAPPEARANCE_LIGHT_NAVIGATION_BARS; 295 | {class} property APPEARANCE_LIGHT_STATUS_BARS: Integer read _GetAPPEARANCE_LIGHT_STATUS_BARS; 296 | {class} property BEHAVIOR_SHOW_BARS_BY_SWIPE: Integer read _GetBEHAVIOR_SHOW_BARS_BY_SWIPE; 297 | {class} property BEHAVIOR_SHOW_BARS_BY_TOUCH: Integer read _GetBEHAVIOR_SHOW_BARS_BY_TOUCH; 298 | {class} property BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE: Integer read _GetBEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; 299 | end; 300 | 301 | [JavaSignature('android/view/WindowInsetsController')] 302 | JWindowInsetsController = interface(JObject) 303 | ['{895DBAB1-0A52-4240-AC13-ED40474E6850}'] 304 | function getSystemBarsAppearance: Integer; cdecl; 305 | function getSystemBarsBehavior: Integer; cdecl; 306 | procedure hide(types: Integer); cdecl; 307 | procedure setSystemBarsAppearance(appearance, mask: Integer); cdecl; 308 | procedure setSystemBarsBehavior(behavior: Integer); cdecl; 309 | procedure show(types: Integer); cdecl; 310 | end; 311 | TJWindowInsetsController = class(TJavaGenericImport) end; 312 | 313 | 314 | { WindowInsets } 315 | 316 | JWindowInsetsClass = interface(Androidapi.JNI.GraphicsContentViewText.JWindowInsetsClass) 317 | ['{6BCCFCCA-A7F0-4740-B4D6-D03286DAD89C}'] 318 | end; 319 | 320 | [JavaSignature('android/view/WindowInsets')] 321 | JWindowInsets = interface(Androidapi.JNI.GraphicsContentViewText.JWindowInsets) 322 | ['{05AFC51B-3683-4DD1-BB1A-22799899142B}'] 323 | function getInsets(typeMask: Integer): TAndroid10.JInsets; cdecl; 324 | function isVisible(typeMask: Integer): Boolean; cdecl; 325 | end; 326 | TJWindowInsets = class(TJavaGenericImport) end; 327 | 328 | 329 | { Window } 330 | 331 | JWindowClass = interface(Androidapi.JNI.GraphicsContentViewText.JWindowClass) 332 | ['{6A055684-AA34-432C-8B18-12B7D721C599}'] 333 | end; 334 | 335 | [JavaSignature('android/view/Window')] 336 | JWindow = interface(Androidapi.JNI.GraphicsContentViewText.JWindow) 337 | ['{BC863876-71E3-4292-8B2C-860FADB8066C}'] 338 | procedure setDecorFitsSystemWindows(decorFitsSystemWindows: Boolean); cdecl; 339 | end; 340 | TJWindowEx = class(TJavaGenericImport) end; 341 | 342 | 343 | { View } 344 | 345 | JViewClass = interface(Androidapi.JNI.GraphicsContentViewText.JViewClass) 346 | ['{44C48716-DA19-42A7-9141-E137DC92598F}'] 347 | end; 348 | 349 | [JavaSignature('android/view/View')] 350 | JView = interface(Androidapi.JNI.GraphicsContentViewText.JView) 351 | ['{150FC57B-6421-4F25-A626-D0B51AA9E4FF}'] 352 | function getWindowInsetsController: JWindowInsetsController; cdecl; 353 | end; 354 | TJView = class(TJavaGenericImport) end; 355 | end; 356 | 357 | {$REGION 'Backwards compatibility'} 358 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 359 | { TAndroidBeforeMarshmallow } 360 | 361 | TAndroidBeforeMarshmallow = class 362 | strict private 363 | class function GetAbsoluteNavigationBarInsets: TRect; static; 364 | class function GetAbsoluteStatusBarHeight: Integer; static; 365 | public 366 | class function AbsoluteSystemInsets: TRect; static; 367 | end; 368 | 369 | { TAndroidBeforeMarshmallow } 370 | 371 | class function TAndroidBeforeMarshmallow.GetAbsoluteNavigationBarInsets: TRect; 372 | 373 | function IsTablet: Boolean; 374 | begin 375 | Result := (TAndroidHelper.Context.getResources().getConfiguration().screenLayout and 376 | TJConfiguration.JavaClass.SCREENLAYOUT_SIZE_MASK) >= TJConfiguration.JavaClass.SCREENLAYOUT_SIZE_LARGE; 377 | end; 378 | 379 | type 380 | TNavigationBarLocation = (Right, Bottom); 381 | var 382 | LResources: JResources; 383 | LNavigationBarExists: Boolean; 384 | LOrientation: Integer; 385 | LResourceID: Integer; 386 | LLocation: TNavigationBarLocation; 387 | begin 388 | Result := TRect.Empty; 389 | try 390 | LLocation := TNavigationBarLocation.Bottom; 391 | LResources := TAndroidHelper.Context.getResources(); 392 | try 393 | LResourceID := LResources.getIdentifier(TAndroidHelper.StringToJString('config_showNavigationBar'), TAndroidHelper.StringToJString('bool'), TAndroidHelper.StringToJString('android')); 394 | LNavigationBarExists := (LResourceID > 0) and LResources.getBoolean(LResourceID); 395 | except 396 | LNavigationBarExists := False; 397 | end; 398 | if LNavigationBarExists then 399 | begin 400 | LOrientation := LResources.getConfiguration().orientation; 401 | if IsTablet then 402 | begin 403 | if LOrientation = TJConfiguration.JavaClass.ORIENTATION_PORTRAIT then 404 | LResourceID := LResources.getIdentifier(TAndroidHelper.StringToJString('navigation_bar_height'), TAndroidHelper.StringToJString('dimen'), TAndroidHelper.StringToJString('android')) 405 | else 406 | LResourceID := LResources.getIdentifier(TAndroidHelper.StringToJString('navigation_bar_height_landscape'), TAndroidHelper.StringToJString('dimen'), TAndroidHelper.StringToJString('android')); 407 | end 408 | else 409 | begin 410 | if LOrientation = TJConfiguration.JavaClass.ORIENTATION_PORTRAIT then 411 | LResourceID := LResources.getIdentifier(TAndroidHelper.StringToJString('navigation_bar_height'), TAndroidHelper.StringToJString('dimen'), TAndroidHelper.StringToJString('android')) 412 | else 413 | begin 414 | LResourceID := LResources.getIdentifier(TAndroidHelper.StringToJString('navigation_bar_width'), TAndroidHelper.StringToJString('dimen'), TAndroidHelper.StringToJString('android')); 415 | LLocation := TNavigationBarLocation.Right; 416 | end; 417 | end; 418 | if LResourceID > 0 then 419 | begin 420 | case LLocation of 421 | TNavigationBarLocation.Right: Result.Right := LResources.getDimensionPixelSize(LResourceID); 422 | TNavigationBarLocation.Bottom: Result.Bottom := LResources.getDimensionPixelSize(LResourceID); 423 | end; 424 | end; 425 | end; 426 | except 427 | Result := TRect.Empty; 428 | end; 429 | end; 430 | 431 | class function TAndroidBeforeMarshmallow.GetAbsoluteStatusBarHeight: Integer; 432 | var 433 | resourceID: Integer; 434 | sAbis: string; 435 | arrAbis: TJavaObjectArray; 436 | I: Integer; 437 | needCheckStatusBarHeight: boolean; 438 | begin 439 | Result := 0; 440 | if TOSVersion.Major >= 5 then 441 | begin 442 | sAbis := ''; 443 | arrAbis := TJBuild.JavaClass.SUPPORTED_ABIS; 444 | for I := 0 to arrAbis.Length - 1 do 445 | sAbis := sAbis + ',' + JStringToString(arrAbis.Items[I]); 446 | sAbis := sAbis.trim([',']); 447 | end 448 | else 449 | sAbis := JStringToString(TJBuild.JavaClass.CPU_ABI) + ',' + JStringToString(TJBuild.JavaClass.CPU_ABI2); 450 | 451 | needCheckStatusBarHeight := (sAbis.Contains('x86') or JStringToString(TJBuild.JavaClass.FINGERPRINT).Contains('intel') 452 | or sAbis.Contains('arm64-v8a')) and (TOSVersion.Major >= 4); 453 | 454 | if (TOSVersion.Major >= 5) or (needCheckStatusBarHeight) then 455 | begin 456 | resourceID := TAndroidHelper.Activity.getResources.getIdentifier(StringToJString('status_bar_height'), 457 | StringToJString('dimen'), StringToJString('android')); 458 | if resourceID > 0 then 459 | Result := TAndroidHelper.Activity.getResources.getDimensionPixelSize(resourceID); 460 | end; 461 | end; 462 | 463 | class function TAndroidBeforeMarshmallow.AbsoluteSystemInsets: TRect; 464 | begin 465 | Result := GetAbsoluteNavigationBarInsets; 466 | Result.Top := GetAbsoluteStatusBarHeight; 467 | end; 468 | {$ENDIF} 469 | {$ENDREGION} 470 | 471 | var 472 | FSystemBarsServiceAndroid: IipSystemBarsServiceAndroid; 473 | 474 | { TipSystemBarsServiceAndroid.TOnApplyWindowInsetsListener } 475 | 476 | constructor TipSystemBarsServiceAndroid.TOnApplyWindowInsetsListener.Create; 477 | begin 478 | inherited Create; 479 | FChangeChecksEnabled := True; 480 | end; 481 | 482 | function TipSystemBarsServiceAndroid.TOnApplyWindowInsetsListener.onApplyWindowInsets(v: JView; 483 | insets: JWindowInsets): JWindowInsets; 484 | var 485 | LForm: TCommonCustomForm; 486 | begin 487 | Result := v.OnApplyWindowInsets(insets); 488 | if FChangeChecksEnabled and Assigned(FSystemBarsServiceAndroid) and Assigned(Screen) then 489 | begin 490 | LForm := Screen.ActiveForm; 491 | if Assigned(LForm) then 492 | FSystemBarsServiceAndroid.CheckInsetsChanges(LForm); 493 | end; 494 | end; 495 | 496 | { TipSystemBarsServiceAndroid.TOnAttachStateChangeListener } 497 | 498 | procedure TipSystemBarsServiceAndroid.TOnAttachStateChangeListener.onViewAttachedToWindow( 499 | v: JView); 500 | var 501 | LForm: TCommonCustomForm; 502 | LFormSystemBars: TipFormSystemBars; 503 | begin 504 | // In first FormActive, the MainActivity.getContentView is not attached to window, so we 505 | // need to set the configs again to fix any problem in first SetVisibility (like insets calculations) 506 | if Assigned(FSystemBarsServiceAndroid) and Assigned(Screen) then 507 | begin 508 | LForm := Screen.ActiveForm; 509 | if Assigned(LForm) then 510 | begin 511 | LFormSystemBars := LForm.SystemBars; 512 | if Assigned(LFormSystemBars) then 513 | FSystemBarsServiceAndroid.SetVisibility(LForm, LFormSystemBars.Visibility); 514 | end; 515 | end; 516 | end; 517 | 518 | procedure TipSystemBarsServiceAndroid.TOnAttachStateChangeListener.onViewDetachedFromWindow( 519 | v: JView); 520 | begin 521 | end; 522 | 523 | { TipSystemBarsServiceAndroid.TOnWindowFocusChangeListener } 524 | 525 | procedure TipSystemBarsServiceAndroid.TOnWindowFocusChangeListener.onWindowFocusChanged( 526 | hasFocus: Boolean); 527 | begin 528 | if hasFocus and Assigned(FSystemBarsServiceAndroid) then 529 | FSystemBarsServiceAndroid.TryFixInvisibleMode; 530 | end; 531 | 532 | { TipSystemBarsServiceAndroid.TOnTouchListener } 533 | 534 | function TipSystemBarsServiceAndroid.TOnTouchListener.onTouch(v: JView; 535 | event: JMotionEvent): Boolean; 536 | var 537 | LForm: TCommonCustomForm; 538 | LFormSystemBars: TipFormSystemBars; 539 | LTouchPoint: TPointF; 540 | LBottomInsets: Single; 541 | begin 542 | Result := False; 543 | if TOSVersion.Check(10) and // Android 10 (api level 29) or later 544 | Assigned(event) and (event.getAction = TJMotionEvent.JavaClass.ACTION_DOWN) then 545 | begin 546 | // Remove touch events from system gesture bar area, for touch processing in this area 547 | // be exclusive to the operating system, as in iOS. It is better this way because the double 548 | // processing generates some inconveniences like scrolling along with the form close gesture 549 | if Assigned(FSystemBarsServiceAndroid) and Assigned(Screen) then 550 | begin 551 | LForm := Screen.ActiveForm; 552 | if Assigned(LForm) then 553 | begin 554 | LFormSystemBars := LForm.SystemBars; 555 | if Assigned(LFormSystemBars) and (LFormSystemBars.Visibility <> TipFormSystemBars.TVisibilityMode.Invisible) then 556 | begin 557 | LBottomInsets := LFormSystemBars.Insets.Bottom; 558 | if (LBottomInsets > 0) and FSystemBarsServiceAndroid.HasGestureNavigationBar(LForm) then 559 | begin 560 | LTouchPoint := ConvertPixelToPoint(TPointF.Create(event.getRawX, event.getRawY)); 561 | LTouchPoint := LForm.ScreenToClient(LTouchPoint); 562 | Result := LTouchPoint.Y > LForm.Height - LBottomInsets; 563 | end; 564 | end; 565 | end; 566 | end; 567 | end; 568 | end; 569 | 570 | { TipSystemBarsServiceAndroid.TWindowServiceFix } 571 | 572 | // This class is just to fix the ClientToScreen and ScreenToClient in VisibleAndOverlap mode 573 | 574 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.Activate( 575 | const AForm: TCommonCustomForm); 576 | begin 577 | FDefaultWindowService.Activate(AForm); 578 | end; 579 | 580 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.BringToFront( 581 | const AForm: TCommonCustomForm); 582 | begin 583 | FDefaultWindowService.BringToFront(AForm); 584 | end; 585 | 586 | function TipSystemBarsServiceAndroid.TWindowServiceFix.CanShowModal: Boolean; 587 | begin 588 | Result := FDefaultWindowService.CanShowModal; 589 | end; 590 | 591 | function TipSystemBarsServiceAndroid.TWindowServiceFix.ClientToScreen( 592 | const AForm: TCommonCustomForm; const Point: TPointF): TPointF; 593 | var 594 | LSystemBars: TipFormSystemBars; 595 | begin 596 | Result := FDefaultWindowService.ClientToScreen(AForm, Point); 597 | LSystemBars := AForm.SystemBars; 598 | if Assigned(LSystemBars) and (LSystemBars.Visibility = TipFormSystemBars.TVisibilityMode.VisibleAndOverlap) then 599 | Result.Y := Result.Y - LSystemBars.TappableInsets.Top; 600 | end; 601 | 602 | constructor TipSystemBarsServiceAndroid.TWindowServiceFix.Create; 603 | begin 604 | inherited Create; 605 | { IFMXFullScreenWindowService } 606 | if TPlatformServices.Current.SupportsPlatformService(IFMXWindowService, FDefaultWindowService) then 607 | begin 608 | TPlatformServices.Current.RemovePlatformService(IFMXWindowService); 609 | TPlatformServices.Current.AddPlatformService(IFMXWindowService, Self); 610 | end; 611 | end; 612 | 613 | function TipSystemBarsServiceAndroid.TWindowServiceFix.CreateWindow( 614 | const AForm: TCommonCustomForm): TWindowHandle; 615 | begin 616 | Result := FDefaultWindowService.CreateWindow(AForm); 617 | end; 618 | 619 | destructor TipSystemBarsServiceAndroid.TWindowServiceFix.Destroy; 620 | begin 621 | { IFMXFullScreenWindowService } 622 | if TPlatformServices.Current <> nil then 623 | begin 624 | TPlatformServices.Current.RemovePlatformService(IFMXWindowService); 625 | if Assigned(FDefaultWindowService) then 626 | TPlatformServices.Current.AddPlatformService(IFMXWindowService, FDefaultWindowService); 627 | end; 628 | inherited; 629 | end; 630 | 631 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.DestroyWindow( 632 | const AForm: TCommonCustomForm); 633 | begin 634 | FDefaultWindowService.DestroyWindow(AForm); 635 | end; 636 | 637 | function TipSystemBarsServiceAndroid.TWindowServiceFix.FindForm( 638 | const AHandle: TWindowHandle): TCommonCustomForm; 639 | begin 640 | Result := FDefaultWindowService.FindForm(AHandle); 641 | end; 642 | 643 | function TipSystemBarsServiceAndroid.TWindowServiceFix.GetClientSize( 644 | const AForm: TCommonCustomForm): TPointF; 645 | begin 646 | Result := FDefaultWindowService.GetClientSize(AForm); 647 | end; 648 | 649 | function TipSystemBarsServiceAndroid.TWindowServiceFix.GetWindowRect( 650 | const AForm: TCommonCustomForm): TRectF; 651 | begin 652 | Result := FDefaultWindowService.GetWindowRect(AForm); 653 | end; 654 | 655 | function TipSystemBarsServiceAndroid.TWindowServiceFix.GetWindowScale( 656 | const AForm: TCommonCustomForm): Single; 657 | begin 658 | Result := TWindowServiceAndroid(TObject(FDefaultWindowService)).GetWindowScale(AForm); 659 | end; 660 | 661 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.HideWindow( 662 | const AForm: TCommonCustomForm); 663 | begin 664 | FDefaultWindowService.HideWindow(AForm); 665 | end; 666 | 667 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.InvalidateImmediately( 668 | const AForm: TCommonCustomForm); 669 | begin 670 | FDefaultWindowService.InvalidateImmediately(AForm); 671 | end; 672 | 673 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.InvalidateWindowRect( 674 | const AForm: TCommonCustomForm; R: TRectF); 675 | begin 676 | FDefaultWindowService.InvalidateWindowRect(AForm, R); 677 | end; 678 | 679 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.ReleaseCapture( 680 | const AForm: TCommonCustomForm); 681 | begin 682 | FDefaultWindowService.ReleaseCapture(AForm); 683 | end; 684 | 685 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.ReleaseWindow( 686 | const AForm: TCommonCustomForm); 687 | begin 688 | FDefaultWindowService.ReleaseWindow(AForm); 689 | end; 690 | 691 | function TipSystemBarsServiceAndroid.TWindowServiceFix.ScreenToClient( 692 | const AForm: TCommonCustomForm; const Point: TPointF): TPointF; 693 | var 694 | LSystemBars: TipFormSystemBars; 695 | begin 696 | Result := FDefaultWindowService.ScreenToClient(AForm, Point); 697 | LSystemBars := AForm.SystemBars; 698 | if Assigned(LSystemBars) and (LSystemBars.Visibility = TipFormSystemBars.TVisibilityMode.VisibleAndOverlap) then 699 | Result.Y := Result.Y + LSystemBars.TappableInsets.Top; 700 | end; 701 | 702 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.SendToBack( 703 | const AForm: TCommonCustomForm); 704 | begin 705 | FDefaultWindowService.SendToBack(AForm); 706 | end; 707 | 708 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.SetCapture( 709 | const AForm: TCommonCustomForm); 710 | begin 711 | FDefaultWindowService.SetCapture(AForm); 712 | end; 713 | 714 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.SetClientSize( 715 | const AForm: TCommonCustomForm; const ASize: TPointF); 716 | begin 717 | FDefaultWindowService.SetClientSize(AForm, ASize); 718 | end; 719 | 720 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.SetWindowCaption( 721 | const AForm: TCommonCustomForm; const ACaption: string); 722 | begin 723 | FDefaultWindowService.SetWindowCaption(AForm, ACaption); 724 | end; 725 | 726 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.SetWindowRect( 727 | const AForm: TCommonCustomForm; ARect: TRectF); 728 | begin 729 | FDefaultWindowService.SetWindowRect(AForm, ARect); 730 | end; 731 | 732 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.SetWindowState( 733 | const AForm: TCommonCustomForm; const AState: TWindowState); 734 | begin 735 | FDefaultWindowService.SetWindowState(AForm, AState); 736 | end; 737 | 738 | procedure TipSystemBarsServiceAndroid.TWindowServiceFix.ShowWindow( 739 | const AForm: TCommonCustomForm); 740 | begin 741 | FDefaultWindowService.ShowWindow(AForm); 742 | end; 743 | 744 | function TipSystemBarsServiceAndroid.TWindowServiceFix.ShowWindowModal( 745 | const AForm: TCommonCustomForm): TModalResult; 746 | begin 747 | Result := ShowWindowModal(AForm); 748 | end; 749 | 750 | { TipSystemBarsServiceAndroid } 751 | 752 | class function TipSystemBarsServiceAndroid.AbsoluteRectToScreenScaled( 753 | const AAbsoluteRect: TRect): TRectF; 754 | var 755 | LEpsilonPositionRange: Integer; 756 | begin 757 | if AAbsoluteRect = TRect.Empty then 758 | Exit(TRectF.Empty); 759 | Result := TRectF.Create(ConvertPixelToPoint(AAbsoluteRect.TopLeft), ConvertPixelToPoint(AAbsoluteRect.BottomRight)); 760 | // Round to position epsilon 761 | LEpsilonPositionRange := -3; 762 | Assert(LEpsilonPositionRange = Round(Log10(TEpsilon.Position))); 763 | Result := TRectF.Create(RoundTo(Result.Left, LEpsilonPositionRange), 764 | RoundTo(Result.Top, LEpsilonPositionRange), 765 | RoundTo(Result.Right, LEpsilonPositionRange), 766 | RoundTo(Result.Bottom, LEpsilonPositionRange)); 767 | end; 768 | 769 | procedure TipSystemBarsServiceAndroid.AfterCreateFormHandle( 770 | const ASender: TObject; const AMessage: TMessage); 771 | 772 | procedure TryApplyInsetsListener; 773 | var 774 | LViewGroup: JViewGroup; 775 | begin 776 | LViewGroup := GetMainActivityContentView; 777 | if Assigned(LViewGroup) then 778 | begin 779 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 780 | if TJBuild_VERSION.JavaClass.SDK_INT >= 20 then // Android 4.4W (Kitkat Watch / api level 20) or later 781 | {$ENDIF} 782 | begin 783 | FOnApplyWindowInsetsListener := TOnApplyWindowInsetsListener.Create; 784 | LViewGroup.setOnApplyWindowInsetsListener(FOnApplyWindowInsetsListener); 785 | end; 786 | FOnAttachStateChangeListener := TOnAttachStateChangeListener.Create; 787 | LViewGroup.addOnAttachStateChangeListener(FOnAttachStateChangeListener); 788 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 789 | if TOSVersion.Check(4, 3) then // Android 4.3 (Jelly Bean / api level 18) or later 790 | {$ENDIF} 791 | begin 792 | FOnWindowFocusChangeListener := TOnWindowFocusChangeListener.Create; 793 | LViewGroup.getViewTreeObserver.addOnWindowFocusChangeListener(FOnWindowFocusChangeListener); 794 | end; 795 | end; 796 | if not Assigned(FOnTouchListener) then 797 | FOnTouchListener := TOnTouchListener.Create; 798 | end; 799 | 800 | var 801 | LForm: TCommonCustomForm; 802 | begin 803 | if not Assigned(FOnAttachStateChangeListener) then 804 | TryApplyInsetsListener; 805 | if ASender is TCommonCustomForm then 806 | begin 807 | LForm := TCommonCustomForm(ASender); 808 | if LForm.IsHandleAllocated then 809 | TAndroidWindowHandle(LForm.Handle).View.setOnTouchListener(FOnTouchListener); 810 | CheckInsetsChanges(LForm); 811 | end; 812 | end; 813 | 814 | procedure TipSystemBarsServiceAndroid.ApplicationEventHandler( 815 | const ASender: TObject; const AMessage: TMessage); 816 | begin 817 | if (AMessage is TApplicationEventMessage) and (TApplicationEventMessage(AMessage).Value.Event = TApplicationEvent.BecameActive) then 818 | begin 819 | TThread.CreateAnonymousThread( 820 | procedure 821 | begin 822 | Sleep(1000); 823 | TThread.CurrentThread.ForceQueue(nil, 824 | procedure 825 | begin 826 | if Assigned(FSystemBarsServiceAndroid) then 827 | FSystemBarsServiceAndroid.TryFixInvisibleMode; 828 | end); 829 | end).Start; 830 | end; 831 | end; 832 | 833 | procedure TipSystemBarsServiceAndroid.BeforeDestroyFormHandle( 834 | const ASender: TObject; const AMessage: TMessage); 835 | var 836 | LForm: TCommonCustomForm; 837 | begin 838 | if ASender is TCommonCustomForm then 839 | begin 840 | LForm := TCommonCustomForm(ASender); 841 | if LForm.IsHandleAllocated then 842 | TAndroidWindowHandle(LForm.Handle).View.setOnTouchListener(nil); 843 | end; 844 | end; 845 | 846 | function TipSystemBarsServiceAndroid.CanFormChangeSystemBars( 847 | const AForm: TCommonCustomForm): Boolean; 848 | begin 849 | Result := Assigned(AForm) and AForm.IsHandleAllocated and AForm.Active; 850 | end; 851 | 852 | procedure TipSystemBarsServiceAndroid.CheckInsetsChanges( 853 | const AForm: TCommonCustomForm); 854 | var 855 | LNewInsets: TRectF; 856 | LNewTappableInsets: TRectF; 857 | LFormSystemBars: TipFormSystemBars; 858 | begin 859 | if Assigned(AForm) and AForm.Active then 860 | begin 861 | LFormSystemBars := AForm.SystemBars; 862 | if Assigned(LFormSystemBars) then 863 | begin 864 | LNewInsets := GetInsets(AForm); 865 | LNewTappableInsets := GetTappableInsets(AForm); 866 | if (not LNewInsets.EqualsTo(LFormSystemBars.Insets, TEpsilon.Position)) or 867 | (not LNewTappableInsets.EqualsTo(LFormSystemBars.TappableInsets, TEpsilon.Position)) then 868 | begin 869 | TMessageManager.DefaultManager.SendMessage(AForm, TipFormSystemBars.TInsetsChangeMessage.Create(LNewInsets, LNewTappableInsets)); 870 | TThread.ForceQueue(nil, 871 | procedure() 872 | begin 873 | if Assigned(FSystemBarsServiceAndroid) and Assigned(Screen) and (Screen.ActiveForm = AForm) then 874 | FSystemBarsServiceAndroid.SetVisibility(AForm, AForm.SystemBars.Visibility); 875 | end); 876 | end; 877 | end; 878 | end; 879 | end; 880 | 881 | constructor TipSystemBarsServiceAndroid.Create; 882 | begin 883 | inherited; 884 | FWindowServiceFix := TWindowServiceFix.Create; 885 | { IFMXWindowSystemBarsService } 886 | if not TPlatformServices.Current.SupportsPlatformService(TipFormSystemBars.IFMXWindowSystemBarsService) then 887 | begin 888 | TPlatformServices.Current.AddPlatformService(TipFormSystemBars.IFMXWindowSystemBarsService, Self); 889 | FRegisteredBarsService := True; 890 | end; 891 | { IFMXWindowSystemStatusBarService } 892 | if not TPlatformServices.Current.SupportsPlatformService(IFMXWindowSystemStatusBarService) then 893 | begin 894 | TPlatformServices.Current.AddPlatformService(IFMXWindowSystemStatusBarService, Self); 895 | FRegisteredStatusBarService := True; 896 | end; 897 | { IFMXFullScreenWindowService } 898 | if TPlatformServices.Current.SupportsPlatformService(IFMXFullScreenWindowService, FDefaultFullScreenService) then 899 | TPlatformServices.Current.RemovePlatformService(IFMXFullScreenWindowService); 900 | TPlatformServices.Current.AddPlatformService(IFMXFullScreenWindowService, Self); 901 | 902 | FDefaultFormVisibility := TDictionary.Create; 903 | FAfterCreateFormHandleMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TAfterCreateFormHandle, AfterCreateFormHandle); 904 | FBeforeDestroyFormHandleMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TBeforeDestroyFormHandle, BeforeDestroyFormHandle); 905 | FFormActivateMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TFormActivateMessage, FormActivate); 906 | FFormReleasedMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TFormReleasedMessage, FormReleased); 907 | FVirtualKeyboardMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TVKStateChangeMessage, VirtualKeyboardChangeHandler); 908 | TMessageManager.DefaultManager.SubscribeToMessage(TApplicationEventMessage, ApplicationEventHandler); 909 | end; 910 | 911 | destructor TipSystemBarsServiceAndroid.Destroy; 912 | var 913 | LViewGroup: JViewGroup; 914 | begin 915 | TMessageManager.DefaultManager.Unsubscribe(TApplicationEventMessage, ApplicationEventHandler); 916 | TMessageManager.DefaultManager.Unsubscribe(TVKStateChangeMessage, FVirtualKeyboardMessageId); 917 | TMessageManager.DefaultManager.Unsubscribe(TAfterCreateFormHandle, FAfterCreateFormHandleMessageId); 918 | TMessageManager.DefaultManager.Unsubscribe(TBeforeDestroyFormHandle, FBeforeDestroyFormHandleMessageId); 919 | TMessageManager.DefaultManager.Unsubscribe(TFormReleasedMessage, FFormReleasedMessageId); 920 | TMessageManager.DefaultManager.Unsubscribe(TFormActivateMessage, FFormActivateMessageId); 921 | if TPlatformServices.Current <> nil then 922 | begin 923 | { IFMXFullScreenWindowService } 924 | TPlatformServices.Current.RemovePlatformService(IFMXFullScreenWindowService); 925 | if Assigned(FDefaultFullScreenService) then 926 | TPlatformServices.Current.AddPlatformService(IFMXFullScreenWindowService, FDefaultFullScreenService); 927 | { IFMXWindowSystemBarsService } 928 | if FRegisteredBarsService then 929 | TPlatformServices.Current.RemovePlatformService(TipFormSystemBars.IFMXWindowSystemBarsService); 930 | { IFMXWindowSystemStatusBarService } 931 | if FRegisteredStatusBarService then 932 | TPlatformServices.Current.RemovePlatformService(IFMXWindowSystemStatusBarService); 933 | end; 934 | 935 | if Assigned(FOnAttachStateChangeListener) then 936 | begin 937 | LViewGroup := GetMainActivityContentView; 938 | if Assigned(LViewGroup) then 939 | begin 940 | if Assigned(FOnApplyWindowInsetsListener) {and (TJBuild_VERSION.JavaClass.SDK_INT >= 20)} then // Android 4.4W (Kitkat Watch / api level 20) or later 941 | LViewGroup.setOnApplyWindowInsetsListener(nil); 942 | LViewGroup.removeOnAttachStateChangeListener(FOnAttachStateChangeListener); 943 | if Assigned(FOnWindowFocusChangeListener) {and TOSVersion.Check(4, 3)} then // Android 4.3 (Jelly Bean / api level 18) or later 944 | LViewGroup.getViewTreeObserver.removeOnWindowFocusChangeListener(FOnWindowFocusChangeListener); 945 | end; 946 | FreeAndNil(FOnApplyWindowInsetsListener); 947 | FreeAndNil(FOnAttachStateChangeListener); 948 | FreeAndNil(FOnWindowFocusChangeListener); 949 | end; 950 | FreeAndNil(FOnTouchListener); 951 | FDefaultFormVisibility.Free; 952 | inherited; 953 | end; 954 | 955 | // StatusBar + NavigationBar + GestureBar 956 | function TipSystemBarsServiceAndroid.DoGetAbsoluteInsets( 957 | const AForm: TCommonCustomForm): TRect; 958 | var 959 | LView: JView; 960 | LWindowInsets: JWindowInsets; 961 | LWindowInsetsAndroid11: TAndroid11.JWindowInsets; 962 | LInsets: TAndroid10.JInsets; 963 | begin 964 | Result := TRect.Empty; 965 | if TOSVersion.Check(6) then // Android 6 (Marshmallow / api level 23) or later 966 | begin 967 | LView := GetWindowDecorView; 968 | if Assigned(LView) then 969 | begin 970 | LWindowInsets := LView.getRootWindowInsets; 971 | if Assigned(LWindowInsets) then 972 | begin 973 | if TOSVersion.Check(11) then // Android 11 (api level 30) or later 974 | begin 975 | LWindowInsetsAndroid11 := TAndroid11.TJWindowInsets.Wrap(TAndroidHelper.JObjectToID(LWindowInsets)); 976 | if Assigned(LWindowInsetsAndroid11) then 977 | begin 978 | LInsets := LWindowInsetsAndroid11.getInsets(TAndroid11.TJWindowInsets_Type.JavaClass.mandatorySystemGestures); 979 | if Assigned(LInsets) then 980 | Result := TRect.Create(LInsets.left, LInsets.top, LInsets.right, LInsets.bottom); 981 | LInsets := LWindowInsetsAndroid11.getInsets(TAndroid11.TJWindowInsets_Type.JavaClass.systemBars); 982 | if Assigned(LInsets) then 983 | Result := TRect.Create(Max(Result.Left, LInsets.left), Max(Result.Top, LInsets.top), 984 | Max(Result.Right, LInsets.right), Max(Result.Bottom, LInsets.bottom)); 985 | end; 986 | end 987 | else 988 | begin 989 | if TOSVersion.Check(10) then // Android 10 (api level 29) 990 | begin 991 | LInsets := TAndroid10.TJWindowInsets.Wrap(TAndroidHelper.JObjectToID(LWindowInsets)).getMandatorySystemGestureInsets; 992 | if Assigned(LInsets) then 993 | Result := TRect.Create(LInsets.left, LInsets.top, LInsets.right, LInsets.bottom); 994 | end; 995 | Result := TRect.Create(Max(Result.Left, LWindowInsets.getSystemWindowInsetLeft), 996 | Max(Result.Top, LWindowInsets.getSystemWindowInsetTop), 997 | Max(Result.Right, LWindowInsets.getSystemWindowInsetRight), 998 | Max(Result.Bottom, LWindowInsets.getSystemWindowInsetBottom)); 999 | end; 1000 | end; 1001 | end; 1002 | end 1003 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1004 | else if TOSVersion.Check(4, 4) then // Android 4.4 (Kitkat / api level 19) to Android 5.1 (Lollipop MR1 / api level 22) 1005 | Result := TAndroidBeforeMarshmallow.AbsoluteSystemInsets; 1006 | {$ENDIF} 1007 | end; 1008 | 1009 | function TipSystemBarsServiceAndroid.DoGetAbsoluteTappableInsets( 1010 | const AForm: TCommonCustomForm): TRect; 1011 | var 1012 | LView: JView; 1013 | LWindowInsets: JWindowInsets; 1014 | LInsets: TAndroid10.JInsets; 1015 | begin 1016 | Result := TRect.Empty; 1017 | if TOSVersion.Check(10) then // Android 10 (api level 29) or later 1018 | begin 1019 | LView := GetWindowDecorView; 1020 | if Assigned(LView) then 1021 | begin 1022 | LWindowInsets := LView.getRootWindowInsets; 1023 | if Assigned(LWindowInsets) then 1024 | begin 1025 | if TOSVersion.Check(11) then // Android 11 (api level 30) or later 1026 | LInsets := TAndroid11.TJWindowInsets.Wrap(TAndroidHelper.JObjectToID(LWindowInsets)).getInsets( 1027 | TAndroid11.TJWindowInsets_Type.JavaClass.tappableElement) 1028 | else 1029 | LInsets := TAndroid10.TJWindowInsets.Wrap(TAndroidHelper.JObjectToID(LWindowInsets)).getTappableElementInsets; 1030 | if Assigned(LInsets) then 1031 | Result := TRect.Create(LInsets.left, LInsets.top, LInsets.right, LInsets.bottom); 1032 | end; 1033 | end; 1034 | end 1035 | else 1036 | Result := DoGetAbsoluteInsets(AForm); 1037 | end; 1038 | 1039 | procedure TipSystemBarsServiceAndroid.DoSetVisibility( 1040 | const AForm: TCommonCustomForm; 1041 | const AMode: TipFormSystemBars.TVisibilityMode); 1042 | var 1043 | LActivity: JActivity; 1044 | LWindow: JWindow; 1045 | LWindowAndroid11: TAndroid11.JWindow; 1046 | LView: JFormView; 1047 | LMainActivityContentView: JViewGroup; 1048 | LViewAndroid11: TAndroid11.JView; 1049 | LWindowInsetsController: TAndroid11.JWindowInsetsController; 1050 | LSystemUiVisibility: Integer; 1051 | LSystemUiVisibilityMask: Integer; 1052 | LWinParams: JWindowManager_LayoutParams; 1053 | LWinParamsAndroid9: TAndroid9.JWindowManager_LayoutParams; 1054 | LHasGestureNavigationBar: Boolean; 1055 | LRect: TRect; 1056 | begin 1057 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1058 | if not TOSVersion.Check(4, 4) then // Supported only Android 4.4 (Kitkat / api level 19) or later 1059 | Exit; 1060 | {$ENDIF} 1061 | if not CanFormChangeSystemBars(AForm) then 1062 | Exit; 1063 | 1064 | LWindow := nil; 1065 | LActivity := TAndroidHelper.Activity; 1066 | if Assigned(LActivity) then 1067 | LWindow := LActivity.getWindow; 1068 | if AForm.IsHandleAllocated then 1069 | LView := TAndroidWindowHandle(AForm.Handle).View 1070 | else 1071 | LView := nil; 1072 | LHasGestureNavigationBar := HasGestureNavigationBar(AForm); 1073 | 1074 | SetStatusBarBackgroundColor(AForm, AForm.SystemStatusBar.BackgroundColor); 1075 | SetNavigationBarBackgroundColor(AForm, AForm.SystemBars.NavigationBarBackgroundColor); 1076 | if TOSVersion.Check(11) then // Android 11 (api level 30) or later 1077 | begin 1078 | if Assigned(LWindow) then 1079 | begin 1080 | LWindowAndroid11 := TAndroid11.TJWindowEx.Wrap((LWindow as ILocalObject).GetObjectID); 1081 | if Assigned(LWindowAndroid11) then 1082 | LWindowAndroid11.setDecorFitsSystemWindows((AMode = TipFormSystemBars.TVisibilityMode.Visible) and not LHasGestureNavigationBar); 1083 | LWinParams := LWindow.getAttributes; 1084 | if Assigned(LWinParams) then 1085 | begin 1086 | LWinParamsAndroid9 := TAndroid9.TJWindowManager_LayoutParams.Wrap(TAndroidHelper.JObjectToID(LWinParams)); 1087 | if Assigned(LWinParamsAndroid9) then 1088 | begin 1089 | if AMode = TipFormSystemBars.TVisibilityMode.Invisible then 1090 | LWinParamsAndroid9.layoutInDisplayCutoutMode := TAndroid9.TJWindowManager_LayoutParams.JavaClass.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 1091 | else 1092 | LWinParamsAndroid9.layoutInDisplayCutoutMode := TAndroid9.TJWindowManager_LayoutParams.JavaClass.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; 1093 | end; 1094 | LWindow.setAttributes(LWinParams); 1095 | end; 1096 | end; 1097 | 1098 | if Assigned(LView) then 1099 | begin 1100 | LViewAndroid11 := TAndroid11.TJView.Wrap(TAndroidHelper.JObjectToID(LView)); 1101 | if Assigned(LViewAndroid11) then 1102 | begin 1103 | LWindowInsetsController := LViewAndroid11.getWindowInsetsController; 1104 | if Assigned(LWindowInsetsController) then 1105 | begin 1106 | case AMode of 1107 | TipFormSystemBars.TVisibilityMode.Visible, 1108 | TipFormSystemBars.TVisibilityMode.VisibleAndOverlap: 1109 | begin 1110 | LWindowInsetsController.show(TAndroid11.TJWindowInsets_Type.JavaClass.statusBars or 1111 | TAndroid11.TJWindowInsets_Type.JavaClass.navigationBars); 1112 | end; 1113 | TipFormSystemBars.TVisibilityMode.Invisible: 1114 | begin 1115 | LWindowInsetsController.setSystemBarsBehavior(TAndroid11.TJWindowInsetsController.JavaClass.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 1116 | LWindowInsetsController.hide(TAndroid11.TJWindowInsets_Type.JavaClass.statusBars or 1117 | TAndroid11.TJWindowInsets_Type.JavaClass.navigationBars); 1118 | end; 1119 | end; 1120 | end; 1121 | end; 1122 | end; 1123 | SetStatusBarBackgroundColor(AForm, AForm.SystemStatusBar.BackgroundColor); 1124 | SetNavigationBarBackgroundColor(AForm, AForm.SystemBars.NavigationBarBackgroundColor); 1125 | end 1126 | else // BEFORE Android 11 (api level 30) 1127 | begin 1128 | LSystemUiVisibilityMask := 0; 1129 | if Assigned(LWindow) then 1130 | LWinParams := LWindow.getAttributes 1131 | else 1132 | LWinParams := nil; 1133 | 1134 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1135 | if TOSVersion.Check(4, 4) and not TOSVersion.Check(5) and // Android 4.4 (Kitkat / api level 19) and Android 4.4W (Kitkat Watch / api level 20) 1136 | Assigned(LWinParams) then 1137 | begin 1138 | case AMode of 1139 | TipFormSystemBars.TVisibilityMode.Visible: 1140 | begin 1141 | LWinParams.flags := LWinParams.flags and not 1142 | (TJWindowManager_LayoutParams.JavaClass.FLAG_TRANSLUCENT_STATUS or 1143 | TJWindowManager_LayoutParams.JavaClass.FLAG_TRANSLUCENT_NAVIGATION); 1144 | end; 1145 | TipFormSystemBars.TVisibilityMode.VisibleAndOverlap: 1146 | begin 1147 | LWinParams.flags := LWinParams.flags or 1148 | TJWindowManager_LayoutParams.JavaClass.FLAG_TRANSLUCENT_STATUS or 1149 | TJWindowManager_LayoutParams.JavaClass.FLAG_TRANSLUCENT_NAVIGATION; 1150 | end; 1151 | TipFormSystemBars.TVisibilityMode.Invisible: ; 1152 | end; 1153 | end; 1154 | {$ENDIF} 1155 | if Assigned(LWinParams) 1156 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1157 | and TOSVersion.Check(5) 1158 | {$ENDIF} then // Android 5.0 (Lollipop / api level 21) or later 1159 | begin 1160 | LWinParams.flags := LWinParams.flags and not 1161 | (TJWindowManager_LayoutParams.JavaClass.FLAG_TRANSLUCENT_STATUS or 1162 | TJWindowManager_LayoutParams.JavaClass.FLAG_TRANSLUCENT_NAVIGATION); 1163 | end; 1164 | if Assigned(LWinParams) then 1165 | begin 1166 | if TOSVersion.Check(9) then // Android 9 (Pie / api level 28) or later 1167 | begin 1168 | LWinParamsAndroid9 := TAndroid9.TJWindowManager_LayoutParams.Wrap(TAndroidHelper.JObjectToID(LWinParams)); 1169 | if Assigned(LWinParamsAndroid9) then 1170 | begin 1171 | if AMode = TipFormSystemBars.TVisibilityMode.Invisible then 1172 | LWinParamsAndroid9.layoutInDisplayCutoutMode := TAndroid9.TJWindowManager_LayoutParams.JavaClass.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 1173 | else 1174 | LWinParamsAndroid9.layoutInDisplayCutoutMode := TAndroid9.TJWindowManager_LayoutParams.JavaClass.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; 1175 | end; 1176 | end; 1177 | if AMode = TipFormSystemBars.TVisibilityMode.Invisible then 1178 | LWinParams.flags := (LWinParams.flags or TJWindowManager_LayoutParams.JavaClass.FLAG_FULLSCREEN) 1179 | and not TJWindowManager_LayoutParams.JavaClass.FLAG_FORCE_NOT_FULLSCREEN 1180 | else 1181 | LWinParams.flags := (LWinParams.flags or TJWindowManager_LayoutParams.JavaClass.FLAG_FORCE_NOT_FULLSCREEN) 1182 | and not TJWindowManager_LayoutParams.JavaClass.FLAG_FULLSCREEN; 1183 | LWindow.setAttributes(LWinParams); 1184 | end; 1185 | 1186 | if TOSVersion.Check(6) then // Android 6 (Marshmallow / api level 23) or later 1187 | LSystemUiVisibilityMask := LSystemUiVisibilityMask or TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 1188 | if TOSVersion.Check(8) then // Android 8.0 (Oreo / api level 26) or later 1189 | LSystemUiVisibilityMask := LSystemUiVisibilityMask or TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; 1190 | if Assigned(LView) 1191 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1192 | and TOSVersion.Check(4, 4) 1193 | {$ENDIF} then // Android 4.4 (Kitkat / api level 19) or later 1194 | begin 1195 | case AMode of 1196 | TipFormSystemBars.TVisibilityMode.Visible: 1197 | begin 1198 | if LHasGestureNavigationBar then 1199 | begin 1200 | LSystemUiVisibility := TJView.JavaClass.SYSTEM_UI_FLAG_VISIBLE or 1201 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_STABLE or 1202 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or 1203 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; 1204 | end 1205 | else 1206 | LSystemUiVisibility := TJView.JavaClass.SYSTEM_UI_FLAG_VISIBLE; 1207 | end; 1208 | TipFormSystemBars.TVisibilityMode.VisibleAndOverlap: 1209 | begin 1210 | LSystemUiVisibility := TJView.JavaClass.SYSTEM_UI_FLAG_VISIBLE or 1211 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_STABLE or 1212 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or 1213 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; 1214 | end; 1215 | TipFormSystemBars.TVisibilityMode.Invisible: 1216 | begin 1217 | LSystemUiVisibility := TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_STABLE or 1218 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or 1219 | TJView.JavaClass.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or 1220 | TJView.JavaClass.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar 1221 | TJView.JavaClass.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar 1222 | TJView.JavaClass.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 1223 | end; 1224 | else 1225 | LSystemUiVisibility := 0; 1226 | end; 1227 | LView.setSystemUiVisibility((LView.getSystemUiVisibility and LSystemUiVisibilityMask) or LSystemUiVisibility); 1228 | end; 1229 | end; 1230 | 1231 | if Assigned(LView) then 1232 | begin 1233 | LView.setFitsSystemWindows((AMode = TipFormSystemBars.TVisibilityMode.Visible) and not LHasGestureNavigationBar); 1234 | RefreshView(LView); 1235 | 1236 | LMainActivityContentView := GetMainActivityContentView; 1237 | if Assigned(LMainActivityContentView) then 1238 | begin 1239 | if HasGestureNavigationBar(AForm) and (AMode = TipFormSystemBars.TVisibilityMode.Visible) then 1240 | begin 1241 | LRect := DoGetAbsoluteTappableInsets(AForm); 1242 | LMainActivityContentView.setPadding(LRect.Left, LRect.Top, LRect.Right, LRect.Bottom); 1243 | end 1244 | else 1245 | LMainActivityContentView.setPadding(0, 0, 0, 0); 1246 | end; 1247 | end; 1248 | 1249 | if AMode <> TipFormSystemBars.TVisibilityMode.Invisible then 1250 | FDefaultFormVisibility.AddOrSetValue(AForm, AMode); 1251 | // Set again when the HasGestureNavigationBar changes due the config 1252 | if LHasGestureNavigationBar <> HasGestureNavigationBar(AForm) then 1253 | DoSetVisibility(AForm, AMode); 1254 | end; 1255 | 1256 | procedure TipSystemBarsServiceAndroid.FormActivate(const ASender: TObject; 1257 | const AMessage: TMessage); 1258 | var 1259 | LForm: TCommonCustomForm; 1260 | begin 1261 | if ASender is TCommonCustomForm then 1262 | begin 1263 | LForm := TCommonCustomForm(ASender); 1264 | SetVisibility(LForm, LForm.SystemStatusBar.Visibility); 1265 | end; 1266 | end; 1267 | 1268 | procedure TipSystemBarsServiceAndroid.FormReleased(const ASender: TObject; 1269 | const AMessage: TMessage); 1270 | begin 1271 | if ASender is TCommonCustomForm then 1272 | FDefaultFormVisibility.Remove(TCommonCustomForm(ASender)); 1273 | end; 1274 | 1275 | function TipSystemBarsServiceAndroid.GetAbsoluteInsets( 1276 | const AForm: TCommonCustomForm): TRect; 1277 | var 1278 | LAbsoluteTappableInsets: TRect; 1279 | begin 1280 | Result := TRect.Empty; 1281 | if not HasSystemBars(AForm) then 1282 | Exit; 1283 | case AForm.SystemBars.Visibility of 1284 | TipFormSystemBars.TVisibilityMode.Visible: // GestureBar 1285 | begin 1286 | Result := DoGetAbsoluteInsets(AForm); 1287 | LAbsoluteTappableInsets := DoGetAbsoluteTappableInsets(AForm); 1288 | Result := TRect.Create(Max(Result.Left - LAbsoluteTappableInsets.Left, 0), 1289 | Max(Result.Top - LAbsoluteTappableInsets.Top, 0), 1290 | Max(Result.Right - LAbsoluteTappableInsets.Right, 0), 1291 | Max(Result.Bottom - LAbsoluteTappableInsets.Bottom, 0)); 1292 | end; 1293 | TipFormSystemBars.TVisibilityMode.Invisible:; // None 1294 | TipFormSystemBars.TVisibilityMode.VisibleAndOverlap: Result := DoGetAbsoluteInsets(AForm); // StatusBar + NavigationBar + GestureBar 1295 | end; 1296 | end; 1297 | 1298 | function TipSystemBarsServiceAndroid.GetAbsoluteTappableInsets( 1299 | const AForm: TCommonCustomForm): TRect; 1300 | begin 1301 | Result := TRect.Empty; 1302 | if not HasSystemBars(AForm) then 1303 | Exit; 1304 | case AForm.SystemBars.Visibility of 1305 | TipFormSystemBars.TVisibilityMode.Visible:; // None 1306 | TipFormSystemBars.TVisibilityMode.Invisible:; // None 1307 | TipFormSystemBars.TVisibilityMode.VisibleAndOverlap: Result := DoGetAbsoluteTappableInsets(AForm); // StatusBar + NavigationBar 1308 | end; 1309 | end; 1310 | 1311 | function TipSystemBarsServiceAndroid.GetFullScreen( 1312 | const AForm: TCommonCustomForm): Boolean; 1313 | var 1314 | LSystemBars: TipFormSystemBars; 1315 | begin 1316 | Result := False; 1317 | if Assigned(AForm) then 1318 | begin 1319 | LSystemBars := AForm.SystemBars; 1320 | if Assigned(LSystemBars) then 1321 | Result := LSystemBars.Visibility = TipFormSystemBars.TVisibilityMode.Invisible; 1322 | end; 1323 | end; 1324 | 1325 | function TipSystemBarsServiceAndroid.GetInsets(const AForm: TCommonCustomForm): TRectF; 1326 | 1327 | function GetCurrentInsets(const AForm: TCommonCustomForm): TRectF; 1328 | var 1329 | LFormSystemBars: TipFormSystemBars; 1330 | begin 1331 | LFormSystemBars := AForm.SystemBars; 1332 | if Assigned(LFormSystemBars) then 1333 | Result := LFormSystemBars.Insets 1334 | else 1335 | Result := TRectF.Empty; 1336 | end; 1337 | 1338 | begin 1339 | if not Assigned(AForm) then 1340 | Result := TRectF.Empty 1341 | else if (not AForm.IsHandleAllocated) or (not AForm.Active) then 1342 | Result := GetCurrentInsets(AForm) 1343 | else 1344 | begin 1345 | Result := AbsoluteRectToScreenScaled(GetAbsoluteInsets(AForm)); 1346 | Result := RemoveKeyboardOverlappedBars(Result, AForm); 1347 | end; 1348 | end; 1349 | 1350 | class function TipSystemBarsServiceAndroid.GetMainActivityContentView: JViewGroup; 1351 | var 1352 | LMainActivity: JFMXNativeActivity; 1353 | begin 1354 | LMainActivity := MainActivity; 1355 | if LMainActivity <> nil then 1356 | begin 1357 | {$IF CompilerVersion >= 34.0} // Delphi Sydney 10.4 1358 | Result := LMainActivity.getContentView; 1359 | {$ELSE} 1360 | Result := LMainActivity.getViewGroup; 1361 | {$ENDIF} 1362 | end; 1363 | end; 1364 | 1365 | function TipSystemBarsServiceAndroid.GetTappableInsets(const AForm: TCommonCustomForm): TRectF; 1366 | 1367 | function GetCurrentTappableInsets(const AForm: TCommonCustomForm): TRectF; 1368 | var 1369 | LFormSystemBars: TipFormSystemBars; 1370 | begin 1371 | LFormSystemBars := AForm.SystemBars; 1372 | if Assigned(LFormSystemBars) then 1373 | Result := LFormSystemBars.TappableInsets 1374 | else 1375 | Result := TRectF.Empty; 1376 | end; 1377 | 1378 | begin 1379 | if not Assigned(AForm) then 1380 | Result := TRectF.Empty 1381 | else if (not AForm.IsHandleAllocated) or (not AForm.Active) then 1382 | Result := GetCurrentTappableInsets(AForm) 1383 | else 1384 | begin 1385 | Result := AbsoluteRectToScreenScaled(GetAbsoluteTappableInsets(AForm)); 1386 | Result := RemoveKeyboardOverlappedBars(Result, AForm); 1387 | end; 1388 | end; 1389 | 1390 | class function TipSystemBarsServiceAndroid.GetWindowDecorView: JView; 1391 | var 1392 | LActivity: JActivity; 1393 | LWindow: JWindow; 1394 | begin 1395 | Result := nil; 1396 | LActivity := TAndroidHelper.Activity; 1397 | if Assigned(LActivity) then 1398 | begin 1399 | LWindow := LActivity.getWindow; 1400 | if Assigned(LWindow) then 1401 | Result := LWindow.getDecorView; 1402 | end; 1403 | end; 1404 | 1405 | function TipSystemBarsServiceAndroid.HasGestureNavigationBar( 1406 | const AForm: TCommonCustomForm): Boolean; 1407 | 1408 | function SameRectIgnoringTop(const ALeft, ARight: TRect): Boolean; 1409 | begin 1410 | Result := (ALeft.Left = ARight.Left) and (ALeft.Right = ARight.Right) and 1411 | (ALeft.Bottom = ARight.Bottom); 1412 | end; 1413 | 1414 | begin 1415 | if TOSVersion.Check(10) then // Android 10 (api level 29) or later 1416 | Result := HasSystemBars(AForm) and AForm.Active and not SameRectIgnoringTop(DoGetAbsoluteInsets(AForm), DoGetAbsoluteTappableInsets(AForm)) 1417 | else 1418 | Result := False; 1419 | end; 1420 | 1421 | function TipSystemBarsServiceAndroid.HasSystemBars( 1422 | const AForm: TCommonCustomForm): Boolean; 1423 | var 1424 | LFormSystemBars: TipFormSystemBars; 1425 | begin 1426 | Result := False; 1427 | if Assigned(AForm) and AForm.IsHandleAllocated then 1428 | begin 1429 | LFormSystemBars := AForm.SystemBars; 1430 | if Assigned(LFormSystemBars) then 1431 | Result := LFormSystemBars.Visibility <> TipFormSystemBars.TVisibilityMode.Invisible; 1432 | end; 1433 | end; 1434 | 1435 | function TipSystemBarsServiceAndroid.IsDarkTheme: Boolean; 1436 | {$IF CompilerVersion >= 34.0} // Delphi Sydney 10.4 1437 | var 1438 | LSystemAppearanceService: IFMXSystemAppearanceService; 1439 | begin 1440 | if TPlatformServices.Current.SupportsPlatformService(IFMXSystemAppearanceService, LSystemAppearanceService) then 1441 | Result := LSystemAppearanceService.ThemeKind = TSystemThemeKind.Dark 1442 | else 1443 | Result := False; 1444 | {$ELSE} 1445 | begin 1446 | Result := False; 1447 | {$ENDIF} 1448 | end; 1449 | 1450 | procedure TipSystemBarsServiceAndroid.RefreshView(const AView: JView); 1451 | var 1452 | LViewParent: JViewParent; 1453 | begin 1454 | if Assigned(AView) then 1455 | begin 1456 | LViewParent := AView.getParent; 1457 | if Assigned(LViewParent) then 1458 | begin 1459 | LViewParent.requestFitSystemWindows; 1460 | LViewParent.requestLayout; 1461 | end; 1462 | end; 1463 | end; 1464 | 1465 | function TipSystemBarsServiceAndroid.RemoveKeyboardOverlappedBars( 1466 | const AInsets: TRectF; const AForm: TCommonCustomForm): TRectF; 1467 | 1468 | // This is really necessary because the TVKStateChangeMessage in android is not accurate, sometimes there are false positives. 1469 | // The problem is not from embarcadero code, this is a android problem (challenge) that was solve in api 30 1470 | function IsIMEReallyVisible: Boolean; 1471 | var 1472 | LView: JView; 1473 | LWindowInsets: JWindowInsets; 1474 | LWindowInsetsAndroid11: TAndroid11.JWindowInsets; 1475 | begin 1476 | Result := True; 1477 | if TOSVersion.Check(11) then // Android 11 (api level 30) or later 1478 | begin 1479 | LView := GetWindowDecorView; 1480 | if Assigned(LView) then 1481 | begin 1482 | LWindowInsets := LView.getRootWindowInsets; 1483 | if Assigned(LWindowInsets) then 1484 | begin 1485 | LWindowInsetsAndroid11 := TAndroid11.TJWindowInsets.Wrap(TAndroidHelper.JObjectToID(LWindowInsets)); 1486 | if Assigned(LWindowInsetsAndroid11) then 1487 | Result := LWindowInsetsAndroid11.isVisible(TAndroid11.TJWindowInsets_Type.JavaClass.ime); 1488 | end; 1489 | end; 1490 | end; 1491 | end; 1492 | 1493 | begin 1494 | Result := AInsets; 1495 | if (not FVirtualKeyboardBounds.IsEmpty) and (AInsets <> TRectF.Empty) and Assigned(AForm) and AForm.Active then 1496 | if (FVirtualKeyboardBounds.Top <> 0) and (FVirtualKeyboardBounds.Left = 0) and // Check if virtual keyboard is in bottom 1497 | (Result.Bottom <> Max(Result.Bottom - FVirtualKeyboardBounds.Height, 0)) and 1498 | IsIMEReallyVisible then 1499 | begin 1500 | Result.Bottom := Max(Result.Bottom - FVirtualKeyboardBounds.Height, 0); // Removing bottom system bars 1501 | end; 1502 | end; 1503 | 1504 | procedure TipSystemBarsServiceAndroid.SetFullScreen( 1505 | const AForm: TCommonCustomForm; const AValue: Boolean); 1506 | var 1507 | LDefaultFormVisibility: TipFormSystemBars.TVisibilityMode; 1508 | begin 1509 | if Assigned(AForm) then 1510 | begin 1511 | if AValue then 1512 | AForm.SystemBars.Visibility := TipFormSystemBars.TVisibilityMode.Invisible 1513 | else if GetFullScreen(AForm) then 1514 | begin 1515 | if FDefaultFormVisibility.TryGetValue(AForm, LDefaultFormVisibility) then 1516 | AForm.SystemBars.Visibility := LDefaultFormVisibility 1517 | else 1518 | AForm.SystemBars.Visibility := TipFormSystemBars.DefaultVisibility; 1519 | end; 1520 | end; 1521 | end; 1522 | 1523 | procedure TipSystemBarsServiceAndroid.SetNavigationBarBackgroundColor( 1524 | const AForm: TCommonCustomForm; const AColor: TAlphaColor); 1525 | var 1526 | LNavigationBarLight: Boolean; 1527 | LNavigationBarJColor: Integer; 1528 | LActivity: JActivity; 1529 | LWindow: JWindow; 1530 | LWindowAndroid10: TAndroid10.JWindow; 1531 | LView: JFormView; 1532 | LViewAndroid11: TAndroid11.JView; 1533 | LWindowInsetsController: TAndroid11.JWindowInsetsController; 1534 | LSystemUiVisibility: Integer; 1535 | LNewAlphaColor: TAlphaColor; 1536 | begin 1537 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1538 | if not TOSVersion.Check(5) then // Supported only Android 5.0 (Lollipop / api level 21) or later 1539 | Exit; 1540 | {$ENDIF} 1541 | if not CanFormChangeSystemBars(AForm) then 1542 | Exit; 1543 | 1544 | LNewAlphaColor := AColor; 1545 | case AForm.SystemBars.Visibility of 1546 | TipFormSystemBars.TVisibilityMode.Visible: 1547 | begin 1548 | if AColor = TAlphaColors.Null then // Like iOS 1549 | begin 1550 | // BEFORE the Android 8.0 (api level 26), the navigation bar buttons is always 1551 | // white, then is better set the background color to black as default 1552 | if IsDarkTheme or not TOSVersion.Check(8) then 1553 | LNewAlphaColor := TAlphaColors.Black 1554 | else 1555 | LNewAlphaColor := TAlphaColors.White; 1556 | end; 1557 | if HasGestureNavigationBar(AForm) then 1558 | TAlphaColorRec(LNewAlphaColor).A := 0; 1559 | end; 1560 | TipFormSystemBars.TVisibilityMode.VisibleAndOverlap: 1561 | if HasGestureNavigationBar(AForm) then 1562 | TAlphaColorRec(LNewAlphaColor).A := 0; 1563 | TipFormSystemBars.TVisibilityMode.Invisible: TAlphaColorRec(LNewAlphaColor).A := 0; 1564 | end; 1565 | 1566 | LNavigationBarLight := Luminance(LNewAlphaColor) > 0.5; 1567 | LNavigationBarJColor := TAndroidHelper.AlphaColorToJColor(LNewAlphaColor); 1568 | LActivity := TAndroidHelper.Activity; 1569 | if Assigned(LActivity) then 1570 | LWindow := LActivity.getWindow 1571 | else 1572 | LWindow := nil; 1573 | if AForm.IsHandleAllocated then 1574 | LView := TAndroidWindowHandle(AForm.Handle).View 1575 | else 1576 | LView := nil; 1577 | 1578 | if TOSVersion.Check(11) then // Android 11 (api level 30) or later 1579 | begin 1580 | if Assigned(LWindow) then 1581 | begin 1582 | LWindowAndroid10 := TAndroid10.TJWindow.Wrap(TAndroidHelper.JObjectToID(LWindow)); 1583 | if Assigned(LWindowAndroid10) then 1584 | LWindowAndroid10.setNavigationBarContrastEnforced(False); 1585 | if LWindow.getNavigationBarColor <> LNavigationBarJColor then 1586 | begin 1587 | LWindow.setNavigationBarColor(LNavigationBarJColor); 1588 | RefreshView(LView); 1589 | end; 1590 | end; 1591 | if Assigned(LView) then 1592 | begin 1593 | LViewAndroid11 := TAndroid11.TJView.Wrap(TAndroidHelper.JObjectToID(LView)); 1594 | if Assigned(LViewAndroid11) then 1595 | begin 1596 | LWindowInsetsController := LViewAndroid11.getWindowInsetsController; 1597 | if Assigned(LWindowInsetsController) then 1598 | begin 1599 | if LNavigationBarLight then 1600 | LWindowInsetsController.setSystemBarsAppearance(TAndroid11.TJWindowInsetsController.JavaClass.APPEARANCE_LIGHT_NAVIGATION_BARS, 1601 | TAndroid11.TJWindowInsetsController.JavaClass.APPEARANCE_LIGHT_NAVIGATION_BARS) 1602 | else 1603 | LWindowInsetsController.setSystemBarsAppearance(0, TAndroid11.TJWindowInsetsController.JavaClass.APPEARANCE_LIGHT_NAVIGATION_BARS); 1604 | end; 1605 | end; 1606 | end; 1607 | end 1608 | else // BEFORE Android 11 (api level 30) 1609 | begin 1610 | if TOSVersion.Check(8) and Assigned(LView) then // Android 8.0 (Oreo / api level 26) or later 1611 | begin 1612 | if LNavigationBarLight then 1613 | LSystemUiVisibility := LView.getSystemUiVisibility or TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 1614 | else 1615 | LSystemUiVisibility := LView.getSystemUiVisibility and not TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; 1616 | LView.setSystemUiVisibility(LSystemUiVisibility); 1617 | end; 1618 | if TOSVersion.Check(10) and Assigned(LWindow) then // Android 10 (api level 29) or later 1619 | begin 1620 | LWindowAndroid10 := TAndroid10.TJWindow.Wrap(TAndroidHelper.JObjectToID(LWindow)); 1621 | if Assigned(LWindowAndroid10) then 1622 | LWindowAndroid10.setNavigationBarContrastEnforced(False); 1623 | end; 1624 | if Assigned(LWindow) 1625 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1626 | and TOSVersion.Check(5) 1627 | {$ENDIF} then // Android 5.0 (Lollipop / api level 21) or later 1628 | begin 1629 | if LWindow.getNavigationBarColor <> LNavigationBarJColor then 1630 | begin 1631 | LWindow.setNavigationBarColor(LNavigationBarJColor); 1632 | RefreshView(LView); 1633 | end; 1634 | end; 1635 | end; 1636 | end; 1637 | 1638 | procedure TipSystemBarsServiceAndroid.SetShowFullScreenIcon( 1639 | const AForm: TCommonCustomForm; const AValue: Boolean); 1640 | begin 1641 | end; 1642 | 1643 | procedure TipSystemBarsServiceAndroid.SetStatusBarBackgroundColor( 1644 | const AForm: TCommonCustomForm; const AColor: TAlphaColor); 1645 | var 1646 | LStatusBarLight: Boolean; 1647 | LStatusBarJColor: Integer; 1648 | LActivity: JActivity; 1649 | LWindow: JWindow; 1650 | LWindowAndroid10: TAndroid10.JWindow; 1651 | LView: JFormView; 1652 | LViewAndroid11: TAndroid11.JView; 1653 | LWindowInsetsController: TAndroid11.JWindowInsetsController; 1654 | LSystemUiVisibility: Integer; 1655 | LNewAlphaColor: TAlphaColor; 1656 | LMainActivityContentView: JViewGroup; 1657 | begin 1658 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1659 | if not TOSVersion.Check(5) then // Supported only Android 5.0 (Lollipop / api level 21) or later 1660 | Exit; 1661 | {$ENDIF} 1662 | if not CanFormChangeSystemBars(AForm) then 1663 | Exit; 1664 | 1665 | LNewAlphaColor := AColor; 1666 | case AForm.SystemBars.Visibility of 1667 | TipFormSystemBars.TVisibilityMode.Visible: 1668 | if AColor = TAlphaColors.Null then // Like iOS 1669 | begin 1670 | if IsDarkTheme then 1671 | LNewAlphaColor := TAlphaColors.Black 1672 | else 1673 | LNewAlphaColor := TAlphaColors.White; 1674 | end; 1675 | TipFormSystemBars.TVisibilityMode.VisibleAndOverlap:; 1676 | TipFormSystemBars.TVisibilityMode.Invisible: TAlphaColorRec(LNewAlphaColor).A := 0; 1677 | end; 1678 | 1679 | LStatusBarLight := Luminance(LNewAlphaColor) > 0.5; 1680 | LStatusBarJColor := TAndroidHelper.AlphaColorToJColor(LNewAlphaColor); 1681 | LActivity := TAndroidHelper.Activity; 1682 | if Assigned(LActivity) then 1683 | LWindow := LActivity.getWindow 1684 | else 1685 | LWindow := nil; 1686 | if AForm.IsHandleAllocated then 1687 | LView := TAndroidWindowHandle(AForm.Handle).View 1688 | else 1689 | LView := nil; 1690 | 1691 | // Setting the configurations 1692 | if TOSVersion.Check(11) then // Android 11 (api level 30) or later 1693 | begin 1694 | if Assigned(LWindow) then 1695 | begin 1696 | LWindowAndroid10 := TAndroid10.TJWindow.Wrap(TAndroidHelper.JObjectToID(LWindow)); 1697 | if Assigned(LWindowAndroid10) then 1698 | LWindowAndroid10.setStatusBarContrastEnforced(False); 1699 | if LWindow.getStatusBarColor <> LStatusBarJColor then 1700 | begin 1701 | LWindow.setStatusBarColor(LStatusBarJColor); 1702 | RefreshView(LView); 1703 | end; 1704 | end; 1705 | if Assigned(LView) then 1706 | begin 1707 | if TOSVersion.Check(12) then 1708 | begin 1709 | LViewAndroid11 := TAndroid11.TJView.Wrap(TAndroidHelper.JObjectToID(LView)); 1710 | if Assigned(LViewAndroid11) then 1711 | begin 1712 | LWindowInsetsController := LViewAndroid11.getWindowInsetsController; 1713 | if Assigned(LWindowInsetsController) then 1714 | begin 1715 | if LStatusBarLight then 1716 | LWindowInsetsController.setSystemBarsAppearance(TAndroid11.TJWindowInsetsController.JavaClass.APPEARANCE_LIGHT_STATUS_BARS, 1717 | TAndroid11.TJWindowInsetsController.JavaClass.APPEARANCE_LIGHT_STATUS_BARS) 1718 | else 1719 | LWindowInsetsController.setSystemBarsAppearance(0, TAndroid11.TJWindowInsetsController.JavaClass.APPEARANCE_LIGHT_STATUS_BARS); 1720 | end; 1721 | end; 1722 | end 1723 | // In some versions of Android 11 there is a known issue where the WindowInsetsController.setSystemBarsAppearance 1724 | // doesn't work when the app's theme declares true/false, directly 1725 | // or indirectly in styles.xml, which this is the case with the FMX. The best workaround to this is using the old 1726 | // method (even being obsolete). 1727 | else 1728 | begin 1729 | if LStatusBarLight then 1730 | LSystemUiVisibility := LView.getSystemUiVisibility or TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 1731 | else 1732 | LSystemUiVisibility := LView.getSystemUiVisibility and not TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 1733 | LView.setSystemUiVisibility(LSystemUiVisibility); 1734 | end; 1735 | end; 1736 | end 1737 | else // BEFORE Android 11 (api level 30) 1738 | begin 1739 | if TOSVersion.Check(6) and Assigned(LView) then // Android 6 (Marshmallow / api level 23) or later 1740 | begin 1741 | if LStatusBarLight then 1742 | LSystemUiVisibility := LView.getSystemUiVisibility or TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 1743 | else 1744 | LSystemUiVisibility := LView.getSystemUiVisibility and not TJView.JavaClass.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 1745 | LView.setSystemUiVisibility(LSystemUiVisibility); 1746 | end; 1747 | if TOSVersion.Check(10) and Assigned(LWindow) then // Android 10 (api level 29) or later 1748 | begin 1749 | LWindowAndroid10 := TAndroid10.TJWindow.Wrap(TAndroidHelper.JObjectToID(LWindow)); 1750 | if Assigned(LWindowAndroid10) then 1751 | LWindowAndroid10.setStatusBarContrastEnforced(False); 1752 | end; 1753 | if Assigned(LWindow) 1754 | {$IF CompilerVersion < 34.0} // Delphi Sydney 10.4 1755 | and TOSVersion.Check(5) 1756 | {$ENDIF} then // Android 5.0 (Lollipop / api level 21) or later 1757 | begin 1758 | if LWindow.getStatusBarColor <> LStatusBarJColor then 1759 | begin 1760 | LWindow.setStatusBarColor(LStatusBarJColor); 1761 | RefreshView(LView); 1762 | end; 1763 | end; 1764 | end; 1765 | 1766 | if AForm.Active and (AForm.SystemBars.Visibility = TipFormSystemBars.TVisibilityMode.Visible) and HasGestureNavigationBar(AForm) then 1767 | begin 1768 | LMainActivityContentView := GetMainActivityContentView; 1769 | if Assigned(LMainActivityContentView) then 1770 | LMainActivityContentView.setBackgroundColor(TAndroidHelper.AlphaColorToJColor(TAlphaColors.White)); 1771 | end; 1772 | end; 1773 | 1774 | procedure TipSystemBarsServiceAndroid.SetVisibility( 1775 | const AForm: TCommonCustomForm; 1776 | const AMode: TipFormSystemBars.TVisibilityMode); 1777 | begin 1778 | FOnApplyWindowInsetsListener.ChangeChecksEnabled := False; 1779 | try 1780 | DoSetVisibility(AForm, AMode); 1781 | finally 1782 | FOnApplyWindowInsetsListener.ChangeChecksEnabled := True; 1783 | end; 1784 | CheckInsetsChanges(AForm); 1785 | end; 1786 | 1787 | // When in invisible mode, the status bar will be showed when the application becames active (after inactive) or when one message dialogs 1788 | // close or when virtual keyboard close. I believe that this is generated by the Embarcadero's FullScreenManager.java listener when detecting 1789 | // changes in the state of the application. As we cannot disable it, then we will detect when this occurs in order to be able to return to 1790 | // the default Invisible state. 1791 | procedure TipSystemBarsServiceAndroid.TryFixInvisibleMode; 1792 | var 1793 | LForm: TCommonCustomForm; 1794 | LFormSystemBars: TipFormSystemBars; 1795 | begin 1796 | if Assigned(Screen) then 1797 | begin 1798 | LForm := Screen.ActiveForm; 1799 | if Assigned(LForm) then 1800 | begin 1801 | LFormSystemBars := LForm.SystemBars; 1802 | if Assigned(LFormSystemBars) and (LFormSystemBars.Visibility = TipFormSystemBars.TVisibilityMode.Invisible) then 1803 | FSystemBarsServiceAndroid.SetVisibility(LForm, LFormSystemBars.Visibility); 1804 | end; 1805 | end; 1806 | end; 1807 | 1808 | procedure TipSystemBarsServiceAndroid.VirtualKeyboardChangeHandler( 1809 | const ASender: TObject; const AMessage: System.Messaging.TMessage); 1810 | begin 1811 | if AMessage is TVKStateChangeMessage then 1812 | begin 1813 | if TVKStateChangeMessage(AMessage).KeyboardVisible then 1814 | begin 1815 | FVirtualKeyboardBounds := TVKStateChangeMessage(AMessage).KeyboardBounds; 1816 | if FVirtualKeyboardBounds.IsEmpty then 1817 | FVirtualKeyboardBounds := TRect.Empty; 1818 | end 1819 | else 1820 | FVirtualKeyboardBounds := TRect.Empty; 1821 | if Assigned(Screen) then 1822 | CheckInsetsChanges(Screen.ActiveForm); 1823 | if FVirtualKeyboardBounds = TRect.Empty then 1824 | TryFixInvisibleMode; 1825 | end; 1826 | end; 1827 | 1828 | initialization 1829 | TRegTypes.RegisterType('iPub.FMX.SystemBars.Android.TAndroid10.JInsets', TypeInfo(iPub.FMX.SystemBars.Android.TAndroid10.JInsets)); 1830 | TRegTypes.RegisterType('iPub.FMX.SystemBars.Android.TAndroid11.JWindowInsetsController', TypeInfo(iPub.FMX.SystemBars.Android.TAndroid11.JWindowInsetsController)); 1831 | FSystemBarsServiceAndroid := TipSystemBarsServiceAndroid.Create; 1832 | {$ELSE} 1833 | implementation 1834 | {$ENDIF} 1835 | end. 1836 | -------------------------------------------------------------------------------- /delphi_system_bars/iPub.FMX.SystemBars.iOS.pas: -------------------------------------------------------------------------------- 1 | {************************************************************************} 2 | { } 3 | { iPub.FMX.SystemBars } 4 | { } 5 | { Copyright (c) 2021-2022 iPub } 6 | { https://github.com/viniciusfbb/fmx_tutorials } 7 | { } 8 | { Use of this source code is governed by a MIT license that can be found } 9 | { at https://opensource.org/licenses/MIT } 10 | { } 11 | {************************************************************************} 12 | unit iPub.FMX.SystemBars.iOS; 13 | 14 | interface 15 | 16 | {$SCOPEDENUMS ON} 17 | {$IFDEF iOS} 18 | 19 | implementation 20 | 21 | uses 22 | { Delphi } 23 | System.Classes, 24 | System.SysUtils, 25 | System.Types, 26 | System.UITypes, 27 | System.Math, 28 | System.Math.Vectors, 29 | System.Messaging, 30 | System.Rtti, 31 | System.TypInfo, 32 | iOSapi.Helpers, 33 | iOSapi.UIKit, 34 | iOSapi.Foundation, 35 | FMX.Forms, 36 | FMX.Platform, 37 | 38 | { iPub } 39 | iPub.FMX.SystemBars; 40 | 41 | type 42 | { TipSystemBarsServiceiOS } 43 | 44 | TipSystemBarsServiceiOS = class(TInterfacedObject, TipFormSystemBars.IFMXWindowSystemBarsService, IFMXWindowSystemStatusBarService) 45 | private 46 | FAfterCreateFormHandleMessageId: Integer; 47 | FFormActivateMessageId: Integer; 48 | FDefaultStatusBarService: IFMXWindowSystemStatusBarService; 49 | FGestureBarChecked: Boolean; 50 | FGestureBarOffset: Single; 51 | FOrientationChangedMessageId: Integer; 52 | FRegisteredBarsService: Boolean; 53 | FVirtualKeyboardBounds: TRect; 54 | FVirtualKeyboardMessageId: Integer; 55 | procedure AfterCreateFormHandle(const ASender: TObject; const AMessage: TMessage); 56 | procedure CheckInsetsChanges(const AForm: TCommonCustomForm); 57 | procedure FormActivate(const ASender: TObject; const AMessage: TMessage); 58 | function GetGestureBarOffset(const AForm: TCommonCustomForm): Single; 59 | function GetStatusBarOffset(const AForm: TCommonCustomForm): Single; 60 | procedure OrientationChanged(const ASender: TObject; const AMessage: TMessage); 61 | function RemoveKeyboardOverlappedBars(const AInsets: TRectF): TRectF; 62 | procedure VirtualKeyboardChangeHandler(const ASender: TObject; const AMessage: TMessage); 63 | public 64 | constructor Create; 65 | destructor Destroy; override; 66 | { IFMXWindowSystemBarsService } 67 | function GetInsets(const AForm: TCommonCustomForm): TRectF; 68 | function GetTappableInsets(const AForm: TCommonCustomForm): TRectF; 69 | procedure SetNavigationBarBackgroundColor(const AForm: TCommonCustomForm; const AColor: TAlphaColor); 70 | { IFMXWindowSystemStatusBarService / IFMXWindowSystemBarsService } 71 | procedure IFMXWindowSystemStatusBarService.SetBackgroundColor = SetStatusBarBackgroundColor; 72 | procedure SetStatusBarBackgroundColor(const AForm: TCommonCustomForm; const AColor: TAlphaColor); 73 | procedure SetVisibility(const AForm: TCommonCustomForm; const AMode: TFormSystemStatusBar.TVisibilityMode); 74 | end; 75 | 76 | var 77 | FSystemBarsServiceiOS: TipSystemBarsServiceiOS; 78 | 79 | { TipSystemBarsServiceiOS } 80 | 81 | procedure TipSystemBarsServiceiOS.AfterCreateFormHandle(const ASender: TObject; 82 | const AMessage: TMessage); 83 | begin 84 | if (ASender is TCommonCustomForm) and (TFmxFormState.Recreating in TCommonCustomForm(ASender).FormState) then 85 | CheckInsetsChanges(TCommonCustomForm(ASender)); 86 | end; 87 | 88 | procedure TipSystemBarsServiceiOS.CheckInsetsChanges( 89 | const AForm: TCommonCustomForm); 90 | var 91 | LNewInsets: TRectF; 92 | LNewTappableInsets: TRectF; 93 | LFormSystemBars: TipFormSystemBars; 94 | begin 95 | if Assigned(AForm) and AForm.Active then 96 | begin 97 | LFormSystemBars := AForm.SystemBars; 98 | if Assigned(LFormSystemBars) then 99 | begin 100 | LNewInsets := GetInsets(AForm); 101 | LNewTappableInsets := GetTappableInsets(AForm); 102 | if (not LNewInsets.EqualsTo(LFormSystemBars.Insets, TEpsilon.Position)) or 103 | (not LNewTappableInsets.EqualsTo(LFormSystemBars.TappableInsets, TEpsilon.Position)) then 104 | begin 105 | TMessageManager.DefaultManager.SendMessage(AForm, TipFormSystemBars.TInsetsChangeMessage.Create(LNewInsets, LNewTappableInsets)); 106 | end; 107 | end; 108 | end; 109 | end; 110 | 111 | constructor TipSystemBarsServiceiOS.Create; 112 | begin 113 | inherited; 114 | if TPlatformServices.Current.SupportsPlatformService(IFMXWindowSystemStatusBarService, FDefaultStatusBarService) then 115 | begin 116 | TPlatformServices.Current.RemovePlatformService(IFMXWindowSystemStatusBarService); 117 | TPlatformServices.Current.AddPlatformService(IFMXWindowSystemStatusBarService, Self); 118 | end 119 | else 120 | raise TipFormSystemBars.Exception.Create('Cannot possible to find the service IFMXWindowSystemStatusBarService'); 121 | if not TPlatformServices.Current.SupportsPlatformService(TipFormSystemBars.IFMXWindowSystemBarsService) then 122 | begin 123 | TPlatformServices.Current.AddPlatformService(TipFormSystemBars.IFMXWindowSystemBarsService, Self); 124 | FRegisteredBarsService := True; 125 | end; 126 | FAfterCreateFormHandleMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TAfterCreateFormHandle, AfterCreateFormHandle); 127 | FFormActivateMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TFormActivateMessage, FormActivate); 128 | FOrientationChangedMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TOrientationChangedMessage, OrientationChanged); 129 | FVirtualKeyboardMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TVKStateChangeMessage, VirtualKeyboardChangeHandler); 130 | end; 131 | 132 | destructor TipSystemBarsServiceiOS.Destroy; 133 | begin 134 | TMessageManager.DefaultManager.Unsubscribe(TVKStateChangeMessage, FVirtualKeyboardMessageId); 135 | TMessageManager.DefaultManager.Unsubscribe(TOrientationChangedMessage, FOrientationChangedMessageId); 136 | TMessageManager.DefaultManager.Unsubscribe(TFormActivateMessage, FFormActivateMessageId); 137 | TMessageManager.DefaultManager.Unsubscribe(TAfterCreateFormHandle, FAfterCreateFormHandleMessageId); 138 | if FRegisteredBarsService then 139 | TPlatformServices.Current.RemovePlatformService(TipFormSystemBars.IFMXWindowSystemBarsService); 140 | if Assigned(FDefaultStatusBarService) then 141 | begin 142 | TPlatformServices.Current.RemovePlatformService(IFMXWindowSystemStatusBarService); 143 | TPlatformServices.Current.AddPlatformService(IFMXWindowSystemStatusBarService, FDefaultStatusBarService); 144 | end; 145 | inherited; 146 | end; 147 | 148 | procedure TipSystemBarsServiceiOS.FormActivate(const ASender: TObject; 149 | const AMessage: TMessage); 150 | begin 151 | if ASender is TCommonCustomForm then 152 | CheckInsetsChanges(TCommonCustomForm(ASender)); 153 | end; 154 | 155 | function TipSystemBarsServiceiOS.GetGestureBarOffset( 156 | const AForm: TCommonCustomForm): Single; 157 | 158 | procedure CalcGestureBarOffset; 159 | begin 160 | TThread.Synchronize(nil, 161 | procedure() 162 | var 163 | LSharedApplication: UIApplication; 164 | LWindows: NSArray; 165 | LWindow: UIWindow; 166 | LViewController: UIViewController; 167 | LView: UIView; 168 | begin 169 | if not FSystemBarsServiceiOS.FGestureBarChecked then 170 | begin 171 | LSharedApplication := TiOSHelper.SharedApplication; 172 | if Assigned(LSharedApplication) then 173 | begin 174 | LWindows := LSharedApplication.windows; 175 | if Assigned(LWindows) and (LWindows.count > 0) then 176 | begin 177 | LWindow := TUIWindow.Wrap(LWindows.objectAtIndex(0)); 178 | if Assigned(LWindow) then 179 | begin 180 | LViewController := LWindow.rootViewController; 181 | if Assigned(LViewController) then 182 | begin 183 | LView := LViewController.view; 184 | if Assigned(LView) then 185 | begin 186 | FSystemBarsServiceiOS.FGestureBarOffset := (LView.bounds.origin.y + LView.bounds.size.height) - 187 | (LWindow.safeAreaLayoutGuide.layoutFrame.origin.y + LWindow.safeAreaLayoutGuide.layoutFrame.size.height); 188 | FSystemBarsServiceiOS.FGestureBarChecked := True; 189 | end; 190 | end; 191 | end; 192 | end; 193 | end; 194 | end; 195 | end); 196 | end; 197 | 198 | function HasFormGestureBar(const AForm: TCommonCustomForm): Boolean; 199 | begin 200 | Result := (AForm <> nil) and (AForm.BorderStyle <> TFmxFormBorderStyle.None); 201 | end; 202 | 203 | begin 204 | Result := 0; 205 | if HasFormGestureBar(AForm) then 206 | begin 207 | if not FGestureBarChecked then 208 | CalcGestureBarOffset; 209 | Result := FGestureBarOffset; 210 | end; 211 | end; 212 | 213 | function TipSystemBarsServiceiOS.GetInsets(const AForm: TCommonCustomForm): TRectF; 214 | begin 215 | Result := GetTappableInsets(AForm); 216 | Result.Bottom := Max(Result.Bottom, GetGestureBarOffset(AForm)); 217 | Result := RemoveKeyboardOverlappedBars(Result); 218 | end; 219 | 220 | function TipSystemBarsServiceiOS.GetStatusBarOffset(const AForm: TCommonCustomForm): Single; 221 | 222 | function HasFormStatusBarOffset(const AForm: TCommonCustomForm): Boolean; 223 | begin 224 | Result := (AForm <> nil) and (AForm.BorderStyle <> TFmxFormBorderStyle.None) and 225 | (AForm.SystemStatusBar.Visibility = TFormSystemStatusBar.TVisibilityMode.VisibleAndOverlap); 226 | end; 227 | 228 | function GetStatusBarOffsetUsingRtti: Single; 229 | const 230 | CLASS_NOT_FOUND = 'Cannot possible to find the class FMX.Platform.iOS.TCocoaTouchWindowManager'; 231 | PROPERTY_NOT_FOUND = 'Cannot possible to find the property "StatusBarOffset: Single" in class FMX.Platform.iOS.TCocoaTouchWindowManager'; 232 | var 233 | LRttiContext: TRttiContext; 234 | LRttiType: TRttiType; 235 | LRttiProperty: TRttiProperty; 236 | LCocoaTouchWindowManager: TObject; 237 | begin 238 | {$IF CompilerVersion > 35} // Delphi 11 Alexandria 239 | {$MESSAGE WARN 'Check in file FMX.Platform.iOS.pas if the class TCocoaTouchWindowManager have already the property "StatusBarOffset: Single" and adjust the IFDEF'} 240 | {$ENDIF} 241 | LCocoaTouchWindowManager := TObject(FDefaultStatusBarService); 242 | Assert(LCocoaTouchWindowManager.ClassName = 'TCocoaTouchWindowManager', CLASS_NOT_FOUND); 243 | LRttiContext := TRttiContext.Create; 244 | try 245 | LRttiType := LRttiContext.GetType(LCocoaTouchWindowManager.ClassType); 246 | if not (LRttiType is TRttiInstanceType) then 247 | raise TipFormSystemBars.Exception.Create(CLASS_NOT_FOUND); 248 | LRttiProperty := LRttiType.GetProperty('StatusBarOffset'); 249 | if (not Assigned(LRttiProperty)) or (LRttiProperty.PropertyType.Handle <> TypeInfo(Single)) then 250 | raise TipFormSystemBars.Exception.Create(PROPERTY_NOT_FOUND); 251 | Result := LRttiProperty.GetValue(LCocoaTouchWindowManager).AsExtended; 252 | finally 253 | LRttiContext.Free; 254 | end; 255 | end; 256 | 257 | begin 258 | if HasFormStatusBarOffset(AForm) and Assigned(FDefaultStatusBarService) then 259 | Result := GetStatusBarOffsetUsingRtti 260 | else 261 | Result := 0; 262 | end; 263 | 264 | function TipSystemBarsServiceiOS.GetTappableInsets(const AForm: TCommonCustomForm): TRectF; 265 | begin 266 | Result := TRectF.Create(0, GetStatusBarOffset(AForm), 0, 0); 267 | Result := RemoveKeyboardOverlappedBars(Result); 268 | end; 269 | 270 | procedure TipSystemBarsServiceiOS.OrientationChanged(const ASender: TObject; 271 | const AMessage: TMessage); 272 | begin 273 | if Assigned(Screen) then 274 | CheckInsetsChanges(Screen.ActiveForm); 275 | end; 276 | 277 | function TipSystemBarsServiceiOS.RemoveKeyboardOverlappedBars( 278 | const AInsets: TRectF): TRectF; 279 | var 280 | LScreenSize: TSizeF; 281 | begin 282 | Result := AInsets; 283 | if (not FVirtualKeyboardBounds.IsEmpty) and (AInsets <> TRectF.Empty) then 284 | begin 285 | LScreenSize := Screen.Size; 286 | // Check if virtual keyboard is in bottom 287 | if SameValue(LScreenSize.Height, FVirtualKeyboardBounds.Bottom, TEpsilon.Position) and (FVirtualKeyboardBounds.Left = 0) and 288 | SameValue(FVirtualKeyboardBounds.Right, LScreenSize.Width, TEpsilon.Position) then 289 | begin 290 | // Removing bottom system bars 291 | Result.Bottom := Max(Result.Bottom - FVirtualKeyboardBounds.Height, 0); 292 | end; 293 | end; 294 | end; 295 | 296 | procedure TipSystemBarsServiceiOS.SetNavigationBarBackgroundColor( 297 | const AForm: TCommonCustomForm; const AColor: TAlphaColor); 298 | begin 299 | end; 300 | 301 | procedure TipSystemBarsServiceiOS.SetStatusBarBackgroundColor( 302 | const AForm: TCommonCustomForm; const AColor: TAlphaColor); 303 | begin 304 | FDefaultStatusBarService.SetBackgroundColor(AForm, AColor); 305 | end; 306 | 307 | procedure TipSystemBarsServiceiOS.SetVisibility(const AForm: TCommonCustomForm; 308 | const AMode: TFormSystemStatusBar.TVisibilityMode); 309 | var 310 | LWindowService: IFMXWindowService; 311 | begin 312 | if Assigned(AForm) then 313 | begin 314 | FDefaultStatusBarService.SetVisibility(AForm, AMode); 315 | if AForm.Active and TPlatformServices.Current.SupportsPlatformService(IFMXWindowService, LWindowService) then 316 | LWindowService.ShowWindow(AForm); // Force update 317 | TMessageManager.DefaultManager.SendMessage(AForm, 318 | TipFormSystemBars.TInsetsChangeMessage.Create(GetInsets(AForm), GetTappableInsets(AForm))); 319 | end; 320 | end; 321 | 322 | procedure TipSystemBarsServiceiOS.VirtualKeyboardChangeHandler( 323 | const ASender: TObject; const AMessage: TMessage); 324 | begin 325 | if AMessage is TVKStateChangeMessage then 326 | begin 327 | FVirtualKeyboardBounds := TVKStateChangeMessage(AMessage).KeyboardBounds; 328 | if Assigned(Screen) then 329 | CheckInsetsChanges(Screen.ActiveForm); 330 | end; 331 | end; 332 | 333 | initialization 334 | FSystemBarsServiceiOS := TipSystemBarsServiceiOS.Create; 335 | finalization 336 | FreeAndNil(FSystemBarsServiceiOS); 337 | {$ELSE} 338 | implementation 339 | {$ENDIF} 340 | end. 341 | -------------------------------------------------------------------------------- /delphi_system_bars/iPub.FMX.SystemBars.pas: -------------------------------------------------------------------------------- 1 | {************************************************************************} 2 | { } 3 | { iPub.FMX.SystemBars } 4 | { } 5 | { Copyright (c) 2021-2022 iPub } 6 | { https://github.com/viniciusfbb/fmx_tutorials } 7 | { } 8 | { Use of this source code is governed by a MIT license that can be found } 9 | { at https://opensource.org/licenses/MIT } 10 | { } 11 | {************************************************************************} 12 | unit iPub.FMX.SystemBars; 13 | 14 | interface 15 | 16 | {$SCOPEDENUMS ON} 17 | 18 | uses 19 | { Delphi } 20 | System.SysUtils, 21 | System.Classes, 22 | System.Messaging, 23 | System.Types, 24 | System.UITypes, 25 | System.Generics.Collections, 26 | FMX.Forms; 27 | 28 | type 29 | { TipFormSystemBars } 30 | 31 | TipFormSystemBars = class(TPersistent) 32 | public type 33 | TVisibilityMode = TFormSystemStatusBar.TVisibilityMode; 34 | public const 35 | DefaultNavigationBarBackgroundColor = TAlphaColorRec.Null; 36 | DefaultStatusBarBackgroundColor = TFormSystemStatusBar.DefaultBackgroundColor; 37 | DefaultVisibility = TFormSystemStatusBar.DefaultVisibility; 38 | {$REGION 'internal'} 39 | public type 40 | Exception = class(System.SysUtils.Exception); 41 | 42 | /// Called from a form as Sender always when the form's insets has changed 43 | TInsetsChangeMessage = class(TMessage) 44 | private 45 | FInsets: TRectF; 46 | FTappableInsets: TRectF; 47 | public 48 | constructor Create(const AInsets, ATappableInsets: TRectF); 49 | property Insets: TRectF read FInsets; 50 | property TappableInsets: TRectF read FTappableInsets; 51 | end; 52 | 53 | /// Service for working with native system navigation bar 54 | IFMXWindowSystemBarsService = interface 55 | ['{124BEBCA-0F61-4A94-92E4-CA279E1BE2E3}'] 56 | /// Sizes of all current system bars 57 | function GetInsets(const AForm: TCommonCustomForm): TRectF; 58 | /// Sizes of all current system bars without gesture bar 59 | function GetTappableInsets(const AForm: TCommonCustomForm): TRectF; 60 | /// Sets background color of system status bar 61 | procedure SetStatusBarBackgroundColor(const AForm: TCommonCustomForm; const AColor: TAlphaColor); 62 | /// Sets background color of system navigation bar 63 | procedure SetNavigationBarBackgroundColor(const AForm: TCommonCustomForm; const AColor: TAlphaColor); 64 | /// Sets how system bars will be shown. See TipFormSystemBars.TVisibilityMode 65 | procedure SetVisibility(const AForm: TCommonCustomForm; const AMode: TipFormSystemBars.TVisibilityMode); 66 | end; 67 | strict private 68 | [Weak] FForm: TCommonCustomForm; 69 | FFormInsetsChangeMessageId: Integer; 70 | FInsets: TRectF; 71 | FNavigationBarBackgroundColor: TAlphaColor; 72 | FOnInsetsChange: TNotifyEvent; 73 | FTappableInsets: TRectF; 74 | procedure FormInsetsChange(const ASender: TObject; const AMessage: TMessage); 75 | function GetStatusBarBackgroundColor: TAlphaColor; 76 | function GetVisibility: TVisibilityMode; 77 | procedure SetNavigationBarBackgroundColor(const AValue: TAlphaColor); 78 | procedure SetStatusBarBackgroundColor(const AValue: TAlphaColor); 79 | procedure SetVisibility(const AValue: TVisibilityMode); 80 | protected 81 | procedure AssignTo(ADest: TPersistent); override; 82 | {$ENDREGION} 83 | public 84 | constructor Create(const AForm: TCommonCustomForm); 85 | destructor Destroy; override; 86 | /// Distances in which the system bars are overlapping the sides of the form (top, left, right and bottom) 87 | property Insets: TRectF read FInsets; 88 | /// Distances in which the system bars, without gesture bar, are overlapping the sides of the form (top, left, right and bottom) 89 | property TappableInsets: TRectF read FTappableInsets; 90 | /// When system bars overlapping distances change, like navigation bar going to right when the phone go to landscape 91 | property OnInsetsChange: TNotifyEvent read FOnInsetsChange write FOnInsetsChange; 92 | published 93 | /// Background color of system navigation bar 94 | property NavigationBarBackgroundColor: TAlphaColor read FNavigationBarBackgroundColor write SetNavigationBarBackgroundColor default DefaultNavigationBarBackgroundColor; 95 | /// Background color of system status bar 96 | property StatusBarBackgroundColor: TAlphaColor read GetStatusBarBackgroundColor write SetStatusBarBackgroundColor default DefaultStatusBarBackgroundColor; 97 | /// Different modes of showing system bars 98 | property Visibility: TVisibilityMode read GetVisibility write SetVisibility default DefaultVisibility; 99 | end; 100 | 101 | { TipFormHelper } 102 | 103 | TipFormHelper = class helper for TCommonCustomForm 104 | {$REGION 'internal'} 105 | // The correct solution would be to change the FMX source to insert the SystemBars 106 | // property in TCommonCustomForm. But to avoid patches on Embarcadero's source, we 107 | // made this helper for the forms 108 | strict private 109 | class var 110 | FAfterCreateFormHandleMessageId: Integer; 111 | FDictionary: TObjectDictionary; 112 | FFormReleasedMessageId: Integer; 113 | class procedure AfterCreateFormHandle(const ASender: TObject; const AMessage: TMessage); static; 114 | class constructor Create; 115 | class destructor Destroy; 116 | class procedure FormReleased(const ASender: TObject; const AMessage: TMessage); static; 117 | class function GetFormSystemBars(AForm: TCommonCustomForm): TipFormSystemBars; static; 118 | strict private 119 | function GetOnSystemBarsInsetsChange: TNotifyEvent; 120 | function GetSystemBars: TipFormSystemBars; 121 | procedure SetOnSystemBarsInsetsChange(const AValue: TNotifyEvent); 122 | procedure SetSystemBars(const AValue: TipFormSystemBars); 123 | {$ENDREGION} 124 | public 125 | /// Settings of system bars on mobile platforms 126 | property SystemBars: TipFormSystemBars read GetSystemBars write SetSystemBars; 127 | /// When system bars overlapping distances change, like navigation bar going to right when the phone go to landscape 128 | property OnSystemBarsInsetsChange: TNotifyEvent read GetOnSystemBarsInsetsChange write SetOnSystemBarsInsetsChange; 129 | end; 130 | 131 | implementation 132 | 133 | uses 134 | { Delphi } 135 | FMX.Platform, 136 | 137 | { iPub } 138 | iPub.FMX.SystemBars.Android, 139 | iPub.FMX.SystemBars.iOS; 140 | 141 | { TipFormSystemBars } 142 | 143 | procedure TipFormSystemBars.AssignTo(ADest: TPersistent); 144 | var 145 | LDestSystemBars: TipFormSystemBars; 146 | begin 147 | if ADest is TipFormSystemBars then 148 | begin 149 | LDestSystemBars := TipFormSystemBars(ADest); 150 | LDestSystemBars.NavigationBarBackgroundColor := NavigationBarBackgroundColor; 151 | LDestSystemBars.StatusBarBackgroundColor := StatusBarBackgroundColor; 152 | LDestSystemBars.Visibility := Visibility; 153 | end 154 | else 155 | inherited; 156 | end; 157 | 158 | constructor TipFormSystemBars.Create(const AForm: TCommonCustomForm); 159 | begin 160 | FForm := AForm; 161 | FNavigationBarBackgroundColor := DefaultNavigationBarBackgroundColor; 162 | FFormInsetsChangeMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TInsetsChangeMessage, FormInsetsChange); 163 | end; 164 | 165 | destructor TipFormSystemBars.Destroy; 166 | begin 167 | TMessageManager.DefaultManager.Unsubscribe(TInsetsChangeMessage, FFormInsetsChangeMessageId); 168 | inherited; 169 | end; 170 | 171 | procedure TipFormSystemBars.FormInsetsChange(const ASender: TObject; 172 | const AMessage: TMessage); 173 | begin 174 | if ASender = FForm then 175 | begin 176 | FInsets := TInsetsChangeMessage(AMessage).Insets; 177 | FTappableInsets := TInsetsChangeMessage(AMessage).TappableInsets; 178 | if Assigned(FOnInsetsChange) then 179 | FOnInsetsChange(FForm); 180 | end; 181 | end; 182 | 183 | function TipFormSystemBars.GetStatusBarBackgroundColor: TAlphaColor; 184 | begin 185 | Result := FForm.SystemStatusBar.BackgroundColor; 186 | end; 187 | 188 | function TipFormSystemBars.GetVisibility: TVisibilityMode; 189 | begin 190 | Result := FForm.SystemStatusBar.Visibility; 191 | end; 192 | 193 | procedure TipFormSystemBars.SetNavigationBarBackgroundColor( 194 | const AValue: TAlphaColor); 195 | var 196 | LService: IFMXWindowSystemBarsService; 197 | begin 198 | if FNavigationBarBackgroundColor <> AValue then 199 | begin 200 | FNavigationBarBackgroundColor := AValue; 201 | if TPlatformServices.Current.SupportsPlatformService(IFMXWindowSystemBarsService, LService) then 202 | LService.SetNavigationBarBackgroundColor(FForm, FNavigationBarBackgroundColor); 203 | end; 204 | end; 205 | 206 | procedure TipFormSystemBars.SetStatusBarBackgroundColor( 207 | const AValue: TAlphaColor); 208 | begin 209 | FForm.SystemStatusBar.BackgroundColor := AValue; 210 | end; 211 | 212 | procedure TipFormSystemBars.SetVisibility(const AValue: TVisibilityMode); 213 | begin 214 | FForm.SystemStatusBar.Visibility := AValue; 215 | end; 216 | 217 | { TipFormSystemBars.TInsetsChangeMessage } 218 | 219 | constructor TipFormSystemBars.TInsetsChangeMessage.Create(const AInsets, 220 | ATappableInsets: TRectF); 221 | begin 222 | inherited Create; 223 | FInsets := AInsets; 224 | FTappableInsets := ATappableInsets; 225 | end; 226 | 227 | { TipFormHelper } 228 | 229 | class procedure TipFormHelper.AfterCreateFormHandle(const ASender: TObject; 230 | const AMessage: TMessage); 231 | begin 232 | // To approach a simulation of the creation of the system bars property in TCommonCustomForm.Create, 233 | // because the TipSystemBars need to subscribe to the TInsetsChangeMessage as soon as possible 234 | if ASender is TCommonCustomForm then 235 | TCommonCustomForm(ASender).SystemBars; 236 | end; 237 | 238 | class constructor TipFormHelper.Create; 239 | begin 240 | FDictionary := TObjectDictionary.Create([doOwnsValues]); 241 | FAfterCreateFormHandleMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TAfterCreateFormHandle, AfterCreateFormHandle); 242 | FFormReleasedMessageId := TMessageManager.DefaultManager.SubscribeToMessage(TFormReleasedMessage, FormReleased); 243 | end; 244 | 245 | class destructor TipFormHelper.Destroy; 246 | begin 247 | TMessageManager.DefaultManager.Unsubscribe(TFormReleasedMessage, FFormReleasedMessageId); 248 | TMessageManager.DefaultManager.Unsubscribe(TAfterCreateFormHandle, FAfterCreateFormHandleMessageId); 249 | FreeAndNil(FDictionary); 250 | end; 251 | 252 | class procedure TipFormHelper.FormReleased(const ASender: TObject; 253 | const AMessage: TMessage); 254 | begin 255 | if ASender is TCommonCustomForm then 256 | FDictionary.Remove(TCommonCustomForm(ASender)); 257 | end; 258 | 259 | class function TipFormHelper.GetFormSystemBars( 260 | AForm: TCommonCustomForm): TipFormSystemBars; 261 | begin 262 | if not Assigned(AForm) then 263 | Exit(nil); 264 | if not FDictionary.TryGetValue(AForm, Result) then 265 | begin 266 | if (csDestroying in AForm.ComponentState) or (TFmxFormState.Released in AForm.FormState) then 267 | Exit(nil); 268 | Result := TipFormSystemBars.Create(AForm); 269 | FDictionary.Add(AForm, Result); 270 | end; 271 | end; 272 | 273 | function TipFormHelper.GetOnSystemBarsInsetsChange: TNotifyEvent; 274 | begin 275 | Result := SystemBars.OnInsetsChange; 276 | end; 277 | 278 | function TipFormHelper.GetSystemBars: TipFormSystemBars; 279 | begin 280 | Result := GetFormSystemBars(Self); 281 | end; 282 | 283 | procedure TipFormHelper.SetOnSystemBarsInsetsChange(const AValue: TNotifyEvent); 284 | begin 285 | SystemBars.OnInsetsChange := AValue; 286 | end; 287 | 288 | procedure TipFormHelper.SetSystemBars(const AValue: TipFormSystemBars); 289 | begin 290 | SystemBars.Assign(AValue); 291 | end; 292 | 293 | end. 294 | -------------------------------------------------------------------------------- /delphi_system_bars/screenshots/calculator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_system_bars/screenshots/calculator.png -------------------------------------------------------------------------------- /delphi_system_bars/screenshots/gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_system_bars/screenshots/gallery.png -------------------------------------------------------------------------------- /delphi_system_bars/screenshots/nubank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusfbb/fmx_tutorials/41df59df653dfa590d83ed0a1fd214d00bef4470/delphi_system_bars/screenshots/nubank.png --------------------------------------------------------------------------------