Android - fully hiding system bars for immersive mode with NativeScript
Ah, the full screen Android experience sure is nice isn't it? But what about that status bar sometimes?
Problem Overview
The Android status bar may reappear when you minimize the app or pull up the menu, disrupting the full-screen experience. Sometimes onWindowFocusChanged will not be triggered if the user is slightly dragging the top of screen.
Solution
Did you know you can add your own custom Android Activity for your NativeScript app?
You can create your own with TypeScript as mentioned in the docs here.
To effectively manage the visibility of the status bar, you should implement the onWindowFocusChanged method, which allows you to monitor when the app is minimized. Additionally, use the onCreate method to set the initial state of the status bar to be transparent and hidden. To address the issue of onWindowFocusChanged not being triggered during slight drags, override the dispatchTouchEvent method to monitor touch events. This will help you determine if the user is dragging from the top of the screen; if so, you can use a boolean flag to hide the status bar accordingly.
Implementation Steps
- Add a new file:
./src/activity.android.ts
import {
Application,
setActivityCallbacks,
AndroidActivityCallbacks,
Utils,
} from "@nativescript/core";
@NativeClass()
@JavaProxy("org.nativescript.mycoolapp.MyCustomActivity")
class MyCustomActivity extends androidx.appcompat.app.AppCompatActivity {
isNativeScriptActivity: boolean;
isDragFromTop = false;
check = false;
private _callbacks: AndroidActivityCallbacks;
onCreate(savedInstanceState: android.os.Bundle): void {
Application.android.init(this.getApplication());
this.isNativeScriptActivity = true;
if (!this._callbacks) {
setActivityCallbacks(this);
}
this._callbacks.onCreate(
this,
savedInstanceState,
this.getIntent(),
super.onCreate
);
this.setFullScreen();
}
setFullScreen() {
const activity = Utils.android.getCurrentActivity();
const window = activity.getWindow();
const decorView = window.getDecorView();
let uiOptions = decorView.getSystemUiVisibility();
uiOptions =
uiOptions |
android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
window.setStatusBarColor(android.graphics.Color.TRANSPARENT);
window.setFlags(
android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
);
window.setNavigationBarColor(android.graphics.Color.TRANSPARENT);
}
dispatchTouchEvent(event: android.view.MotionEvent) {
const action = event.getAction();
switch (action) {
case android.view.MotionEvent.ACTION_DOWN:
if (event.getY() < 100) {
this.isDragFromTop = true;
}
break;
case android.view.MotionEvent.ACTION_MOVE:
if (this.isDragFromTop && this.check) {
this.hideit();
}
break;
case android.view.MotionEvent.ACTION_UP:
case android.view.MotionEvent.ACTION_CANCEL:
this.isDragFromTop = false;
break;
}
return super.dispatchTouchEvent(event);
}
hideit() {
const activity = Utils.android.getCurrentActivity();
const window = activity.getWindow();
const decorView = window.getDecorView();
const windowInsetsControllerCompat =
androidx.core.view.WindowCompat.getInsetsController(window, decorView);
windowInsetsControllerCompat.hide(
androidx.core.view.WindowInsetsCompat.Type.statusBars()
);
}
onWindowFocusChanged(hasFocus: boolean) {
super.onWindowFocusChanged(hasFocus);
this.check = hasFocus;
if (hasFocus) {
this.hideit();
}
}
onNewIntent(intent: android.content.Intent): void {
this._callbacks.onNewIntent(
this,
intent,
super.setIntent,
super.onNewIntent
);
}
onSaveInstanceState(outState: android.os.Bundle): void {
this._callbacks.onSaveInstanceState(
this,
outState,
super.onSaveInstanceState
);
}
onStart(): void {
this._callbacks.onStart(this, super.onStart);
}
onStop(): void {
this._callbacks.onStop(this, super.onStop);
}
onDestroy(): void {
this._callbacks.onDestroy(this, super.onDestroy);
}
onPostResume(): void {
this._callbacks.onPostResume(this, super.onPostResume);
}
onBackPressed(): void {
this._callbacks.onBackPressed(this, super.onBackPressed);
}
onRequestPermissionsResult(
requestCode: number,
permissions: Array<string>,
grantResults: Array<number>
): void {
this._callbacks.onRequestPermissionsResult(
this,
requestCode,
permissions,
grantResults,
undefined
);
}
onActivityResult(
requestCode: number,
resultCode: number,
data: android.content.Intent
): void {
this._callbacks.onActivityResult(
this,
requestCode,
resultCode,
data,
super.onActivityResult
);
}
}
- Add
core-splashscreento dependencies to ensure edge to edge Android devices fill entire surface of screen.
In app.gradle, add this dependency:
dependencies {
implementation "androidx.core:core-splashscreen:1.0.0"
}
Add App_Resources/Android/src/main/res/values/themes.xml file with these contents:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Vixacare.StartingScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/ns_primary</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/logo</item>
<item name="postSplashScreenTheme">@android:style/Theme.Material.NoActionBar</item>
</style>
</resources>
- Adjust
App_Resources/Android/src/main/AndroidManifest.xmlto use the custom activity and theme:
<activity
android:name="org.nativescript.mycoolapp.MyCustomActivity"
android:theme="@style/Theme.Vixacare.StartingScreen">
<!-- additional ommitted for brevity -->
</activity>
- Adjust
webpack.config.jsto include your custom activity:
const webpack = require("@nativescript/webpack");
module.exports = (env) => {
webpack.init(env);
env.appComponents = (env.appComponents || []).concat(['./src/activity.android'])
return webpack.resolveConfig();
};
You will now have a full screen immersize app with no status bar 🎉
Explanation
- Monitor Window Focus Changes: We override the
onWindowFocusChangedmethod to detect when the app gains or loses focus. - Set Initial State: In the
onCreatemethod, configure the status bar to be transparent and hidden withsetFullScreen. - Touch Event Monitoring: To address the issue of
onWindowFocusChangednot being triggered during slight drags, override thedispatchTouchEventmethod. This allows us to monitor touch events and determine if the user is dragging from the top of the screen. - Hide Status Bar on Drag: If a drag is detected from the top, use a boolean flag to check the state and hide the status bar accordingly.
- Adding core-splashscreen: Ensure edge to edge devices fill entire surface of available screen space.
Special Thanks for Bringing Ideas to Light
A heartfelt thank you to those who have contributed valuable ideas and insights:
- "@Nove [876 Gaming]"" in NativeScript Community Discord
- triniwiz
- wwwalkerrun
Join Our Community
For any questions or discussions, feel free to join us on Discord! Connect with fellow developers and enthusiasts here: NativeScript Discord
Your participation is greatly appreciated!