├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------