11package com .getcapacitor .plugin ;
22
3- import android .annotation .SuppressLint ;
43import android .content .Context ;
54import android .content .pm .PackageInfo ;
65import android .content .res .Configuration ;
76import android .content .res .Resources ;
8- import android .os .Build ;
97import android .util .TypedValue ;
108import android .view .View ;
119import android .view .Window ;
12- import android .webkit .JavascriptInterface ;
1310import android .webkit .WebView ;
1411import androidx .core .graphics .Insets ;
1512import androidx .core .view .ViewCompat ;
@@ -34,9 +31,9 @@ public class SystemBars extends Plugin {
3431 static final String BAR_STATUS_BAR = "StatusBar" ;
3532 static final String BAR_GESTURE_BAR = "NavigationBar" ;
3633
37- // TODO: In Cap 9, add an additional option "full"
3834 static final String INSETS_HANDLING_CSS = "css" ;
3935 static final String INSETS_HANDLING_DISABLE = "disable" ;
36+ static final String INSETS_HANDLING_NATIVE = "native" ;
4037
4138 // https://issues.chromium.org/issues/40699457
4239 private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140 ;
@@ -62,11 +59,22 @@ function capacitorSystemBarsCheckMetaViewport() {
6259 private String currentStatusBarStyle = STYLE_DEFAULT ;
6360 private String currentGestureBarStyle = STYLE_DEFAULT ;
6461
65- private boolean navBarVisible = true ;
62+ // Declare variable at this scope to help prevent adding multiple listeners.
63+ private WebViewListener webViewListener ;
64+
65+ private void warnAboutUnsupportedConfigurationValues () {
66+ boolean keyboardResizeOnFullScreen = bridge .getConfig ().getPluginConfiguration ("Keyboard" ).getBoolean ("resizeOnFullScreen" , false );
67+
68+ if (!INSETS_HANDLING_DISABLE .equals (insetsHandling ) && keyboardResizeOnFullScreen ) {
69+ Logger .warn (
70+ "SystemBars" ,
71+ "You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior."
72+ );
73+ }
74+ }
6675
6776 @ Override
6877 public void load () {
69- getBridge ().getWebView ().addJavascriptInterface (this , "CapacitorSystemBarsAndroidInterface" );
7078 super .load ();
7179
7280 initSystemBars ();
@@ -76,23 +84,27 @@ public void load() {
7684 protected void handleOnStart () {
7785 super .handleOnStart ();
7886
79- this .getBridge ().addWebViewListener (
80- new WebViewListener () {
87+ if (INSETS_HANDLING_DISABLE .equals (insetsHandling )) {
88+ return ;
89+ }
90+
91+ if (webViewListener == null ) {
92+ webViewListener = new WebViewListener () {
8193 @ Override
8294 public void onPageCommitVisible (WebView view , String url ) {
8395 super .onPageCommitVisible (view , url );
84- View parentView = (View ) getBridge ().getWebView ().getParent ();
85- ViewCompat .requestApplyInsets (parentView );
86- if (INSETS_HANDLING_CSS .equals (insetsHandling )) {
87- WindowInsetsCompat rootInsets = ViewCompat .getRootWindowInsets (parentView );
88- if (rootInsets != null ) {
89- Insets safeArea = calcSafeAreaInsets (rootInsets );
90- injectSafeAreaCSS (safeArea .top , safeArea .right , safeArea .bottom , safeArea .left );
91- }
92- }
96+ bridge
97+ .getWebView ()
98+ .evaluateJavascript (viewportMetaJSFunction , (res ) -> {
99+ hasViewportCover = res .equals ("true" );
100+
101+ // Request new execution tree of `setOnApplyWindowInsetsListener`
102+ bridge .getWebView ().requestApplyInsets ();
103+ });
93104 }
94- }
95- );
105+ };
106+ this .getBridge ().addWebViewListener (webViewListener );
107+ }
96108 }
97109
98110 @ Override
@@ -104,11 +116,20 @@ protected void handleOnConfigurationChanged(Configuration newConfig) {
104116 }
105117
106118 private void initSystemBars () {
119+ // If you already know what the value of the `viewport-fit=` meta tag is going to be,
120+ // passing it here through `initialViewportFitValueHint` can help prevent layout shifting.
121+ String configuredInitialViewportFitValueHint = getConfig ().getString ("initialViewportFitValueHint" , "" );
122+ hasViewportCover = "cover" .equals (configuredInitialViewportFitValueHint );
123+
107124 String style = getConfig ().getString ("style" , STYLE_DEFAULT ).toUpperCase (Locale .US );
108125 boolean hidden = getConfig ().getBoolean ("hidden" , false );
109126
110127 String configuredInsetsHandling = getConfig ().getString ("insetsHandling" , INSETS_HANDLING_CSS );
111- if (INSETS_HANDLING_CSS .equals (configuredInsetsHandling ) || INSETS_HANDLING_DISABLE .equals (configuredInsetsHandling )) {
128+ if (
129+ INSETS_HANDLING_CSS .equals (configuredInsetsHandling ) ||
130+ INSETS_HANDLING_DISABLE .equals (configuredInsetsHandling ) ||
131+ INSETS_HANDLING_NATIVE .equals (configuredInsetsHandling )
132+ ) {
112133 insetsHandling = configuredInsetsHandling ;
113134 } else {
114135 Logger .warn (
@@ -118,21 +139,13 @@ private void initSystemBars() {
118139 insetsHandling = INSETS_HANDLING_CSS ;
119140 }
120141
142+ warnAboutUnsupportedConfigurationValues ();
143+
121144 initWindowInsetsListener ();
122- initSafeAreaCSSVariables ();
123145
124146 getBridge ().executeOnMainThread (() -> {
125- Window window = getActivity ().getWindow ();
126- WindowCompat .setDecorFitsSystemWindows (window , false );
127- if (Build .VERSION .SDK_INT < Build .VERSION_CODES .VANILLA_ICE_CREAM ) {
128- window .setNavigationBarColor (android .graphics .Color .TRANSPARENT );
129- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
130- window .setNavigationBarContrastEnforced (false );
131- }
132- }
133147 setStyle (style , "" );
134148 setHidden (hidden , "" );
135- ViewCompat .requestApplyInsets ((View ) getBridge ().getWebView ().getParent ());
136149 });
137150 }
138151
@@ -172,96 +185,14 @@ public void setAnimation(final PluginCall call) {
172185 call .resolve ();
173186 }
174187
175- @ Override
176- protected void handleOnResume () {
177- super .handleOnResume ();
178- getBridge ().executeOnMainThread (() -> {
179- Window window = getActivity ().getWindow ();
180- WindowCompat .setDecorFitsSystemWindows (window , false );
181- if (Build .VERSION .SDK_INT < Build .VERSION_CODES .VANILLA_ICE_CREAM ) {
182- window .setNavigationBarColor (android .graphics .Color .TRANSPARENT );
183- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
184- window .setNavigationBarContrastEnforced (false );
185- }
186- }
187- setStyle (currentGestureBarStyle , BAR_GESTURE_BAR );
188- setStyle (currentStatusBarStyle , BAR_STATUS_BAR );
189- ViewCompat .requestApplyInsets ((View ) getBridge ().getWebView ().getParent ());
190- });
191- }
192-
193- @ JavascriptInterface
194- public void onDOMReady () {
195- if (INSETS_HANDLING_CSS .equals (insetsHandling )) {
196- getActivity ().runOnUiThread (() -> {
197- this .bridge .getWebView ().evaluateJavascript (viewportMetaJSFunction , (res ) -> {
198- hasViewportCover = res .equals ("true" );
199-
200- getBridge ().getWebView ().requestApplyInsets ();
201- });
202- });
203- }
204- }
205-
206- private Insets calcSafeAreaInsets (WindowInsetsCompat insets ) {
207- Insets safeArea = insets .getInsets (WindowInsetsCompat .Type .systemBars () | WindowInsetsCompat .Type .displayCutout ());
208-
209- int bottom = safeArea .bottom ;
210-
211- if (bottom == 0 && Build .VERSION .SDK_INT < Build .VERSION_CODES .R && safeArea .left == 0 && safeArea .right == 0 ) {
212- // skip if nav bar is on a side (landscape)
213- bottom = getNavBarHeightFromResources ();
214- }
215-
216- boolean imeVisible = Build .VERSION .SDK_INT >= Build .VERSION_CODES .O
217- ? insets .isVisible (WindowInsetsCompat .Type .ime ())
218- : insets .getInsets (WindowInsetsCompat .Type .ime ()).bottom > 0 ;
219-
220- if (imeVisible ) {
221- return Insets .of (safeArea .left , safeArea .top , safeArea .right , 0 );
222- }
223- return Insets .of (safeArea .left , safeArea .top , safeArea .right , bottom );
224- }
225-
226- // Only reached on API < 30 when the window reports no inset on any edge, so the
227- // choice is this approximation or a hardcoded 0. Lint rejects the internal
228- // resource because OEMs may change it; a wrong height still beats no inset.
229- @ SuppressLint ({ "InternalInsetResource" , "DiscouragedApi" })
230- private int getNavBarHeightFromResources () {
231- if (!navBarVisible ) return 0 ;
232- android .content .res .Resources res = getActivity ().getResources ();
233- int heightId = res .getIdentifier ("navigation_bar_height" , "dimen" , "android" );
234- return heightId > 0 ? res .getDimensionPixelSize (heightId ) : 0 ;
235- }
236-
237- private void initSafeAreaCSSVariables () {
238- if (INSETS_HANDLING_CSS .equals (insetsHandling )) {
239- WindowInsetsCompat insets ;
240-
241- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .VANILLA_ICE_CREAM ) {
242- View v = (View ) this .getBridge ().getWebView ().getParent ();
243- insets = ViewCompat .getRootWindowInsets (v );
244- } else {
245- insets = WindowInsetsCompat .CONSUMED ;
246- }
247-
248- if (insets != null ) {
249- Insets safeAreaInsets = calcSafeAreaInsets (insets );
250- injectSafeAreaCSS (safeAreaInsets .top , safeAreaInsets .right , safeAreaInsets .bottom , safeAreaInsets .left );
251- }
252- }
253- }
254-
255188 private void initWindowInsetsListener () {
256189 if (INSETS_HANDLING_DISABLE .equals (insetsHandling )) {
257190 return ;
258191 }
259192
260- ViewCompat .setOnApplyWindowInsetsListener ((View ) getBridge ().getWebView ().getParent (), (v , insets ) -> {
261- // getRootWindowInsets() bypasses AppCompat's intermediate view consuming the bottom inset on API < 30
262- WindowInsetsCompat rawInsets = ViewCompat .getRootWindowInsets (v );
263- WindowInsetsCompat safeAreaSource = (rawInsets != null ) ? rawInsets : insets ;
193+ View view = getActivity ().getWindow ().getDecorView ();
264194
195+ ViewCompat .setOnApplyWindowInsetsListener (view , (v , insets ) -> {
265196 boolean shouldPassthroughInsets = getWebViewMajorVersion () >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover ;
266197
267198 Insets systemBarsInsets = insets .getInsets (WindowInsetsCompat .Type .systemBars () | WindowInsetsCompat .Type .displayCutout ());
@@ -272,10 +203,7 @@ private void initWindowInsetsListener() {
272203 // We need to correct for a possible shown IME
273204 v .setPadding (0 , 0 , 0 , keyboardVisible ? imeInsets .bottom : 0 );
274205
275- Insets safeAreaInsets = calcSafeAreaInsets (safeAreaSource );
276- injectSafeAreaCSS (safeAreaInsets .top , safeAreaInsets .right , safeAreaInsets .bottom , safeAreaInsets .left );
277-
278- return new WindowInsetsCompat .Builder (insets )
206+ WindowInsetsCompat newInsets = new WindowInsetsCompat .Builder (insets )
279207 .setInsets (
280208 WindowInsetsCompat .Type .systemBars () | WindowInsetsCompat .Type .displayCutout (),
281209 Insets .of (
@@ -286,9 +214,19 @@ private void initWindowInsetsListener() {
286214 )
287215 )
288216 .build ();
217+
218+ injectSafeAreaCSS (newInsets );
219+
220+ return newInsets ;
289221 }
290222
291- v .setPadding (0 , 0 , 0 , keyboardVisible ? imeInsets .bottom : 0 );
223+ // We need to correct for a possible shown IME
224+ v .setPadding (
225+ systemBarsInsets .left ,
226+ systemBarsInsets .top ,
227+ systemBarsInsets .right ,
228+ keyboardVisible ? imeInsets .bottom : systemBarsInsets .bottom
229+ );
292230
293231 // Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
294232 // So we have to explicitly set insets to `0`
@@ -297,20 +235,28 @@ private void initWindowInsetsListener() {
297235 .setInsets (WindowInsetsCompat .Type .systemBars () | WindowInsetsCompat .Type .displayCutout (), Insets .of (0 , 0 , 0 , 0 ))
298236 .build ();
299237
300- Insets safeAreaInsets = calcSafeAreaInsets (safeAreaSource );
301- injectSafeAreaCSS (safeAreaInsets .top , safeAreaInsets .right , safeAreaInsets .bottom , safeAreaInsets .left );
238+ injectSafeAreaCSS (newInsets );
302239
303240 return newInsets ;
304241 });
305242 }
306243
307- private void injectSafeAreaCSS (int top , int right , int bottom , int left ) {
244+ private void injectSafeAreaCSS (WindowInsetsCompat insets ) {
245+ if (!INSETS_HANDLING_CSS .equals (insetsHandling )) {
246+ return ;
247+ }
248+
249+ Insets systemBarsInsets = insets .getInsets (WindowInsetsCompat .Type .systemBars () | WindowInsetsCompat .Type .displayCutout ());
250+ boolean keyboardVisible = insets .isVisible (WindowInsetsCompat .Type .ime ());
251+
308252 // Convert pixels to density-independent pixels
309253 float density = getActivity ().getResources ().getDisplayMetrics ().density ;
310- float topPx = top / density ;
311- float rightPx = right / density ;
312- float bottomPx = bottom / density ;
313- float leftPx = left / density ;
254+ float topPx = systemBarsInsets .top / density ;
255+ float rightPx = systemBarsInsets .right / density ;
256+ // For native insets the value gets automatically corrected when the IME is visible (in newer WebView versions),
257+ // but for these injected values we have to handle that manually (for all WebView versions).
258+ float bottomPx = (keyboardVisible ? 0 : systemBarsInsets .bottom ) / density ;
259+ float leftPx = systemBarsInsets .left / density ;
314260
315261 // Execute JavaScript to inject the CSS
316262 getBridge ().executeOnMainThread (() -> {
@@ -337,20 +283,19 @@ private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
337283 }
338284
339285 private void setStyle (String style , String bar ) {
340- String requestedStyle = style ;
341286 if (style .equals (STYLE_DEFAULT )) {
342287 style = getStyleForTheme ();
343288 }
344289
345290 Window window = getActivity ().getWindow ();
346291 WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat .getInsetsController (window , window .getDecorView ());
347292 if (bar .isEmpty () || bar .equals (BAR_STATUS_BAR )) {
348- currentStatusBarStyle = requestedStyle ;
293+ currentStatusBarStyle = style ;
349294 windowInsetsControllerCompat .setAppearanceLightStatusBars (!style .equals (STYLE_DARK ));
350295 }
351296
352297 if (bar .isEmpty () || bar .equals (BAR_GESTURE_BAR )) {
353- currentGestureBarStyle = requestedStyle ;
298+ currentGestureBarStyle = style ;
354299 windowInsetsControllerCompat .setAppearanceLightNavigationBars (!style .equals (STYLE_DARK ));
355300 }
356301
@@ -364,24 +309,20 @@ private void setHidden(boolean hide, String bar) {
364309 if (hide ) {
365310 if (bar .isEmpty ()) {
366311 windowInsetsControllerCompat .hide (WindowInsetsCompat .Type .systemBars ());
367- navBarVisible = false ;
368312 } else if (bar .equals (BAR_STATUS_BAR )) {
369313 windowInsetsControllerCompat .hide (WindowInsetsCompat .Type .statusBars ());
370314 } else if (bar .equals (BAR_GESTURE_BAR )) {
371315 windowInsetsControllerCompat .hide (WindowInsetsCompat .Type .navigationBars ());
372- navBarVisible = false ;
373316 }
374317 return ;
375318 }
376319
377320 if (bar .isEmpty ()) {
378321 windowInsetsControllerCompat .show (WindowInsetsCompat .Type .systemBars ());
379- navBarVisible = true ;
380322 } else if (bar .equals (BAR_STATUS_BAR )) {
381323 windowInsetsControllerCompat .show (WindowInsetsCompat .Type .statusBars ());
382324 } else if (bar .equals (BAR_GESTURE_BAR )) {
383325 windowInsetsControllerCompat .show (WindowInsetsCompat .Type .navigationBars ());
384- navBarVisible = true ;
385326 }
386327 }
387328
0 commit comments