Back to Blog Home
← all posts

Android - fully hiding system bars for immersive mode with NativeScript

January 15, 2025 — by Shamim Haider

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

  1. 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);
    const windowInsetsControllerCompat =
      androidx.core.view.WindowCompat.getInsetsController(window, decorView);
    windowInsetsControllerCompat.hide(
      androidx.core.view.WindowInsetsCompat.Type.statusBars()
    );
  }

  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
    );
  }
}
  1. Adjust App_Resources/Android/src/main/AndroidManifest.xml to use it:
<activity
    android:name="org.nativescript.mycoolapp.MyCustomActivity">
    <!-- additional ommitted for brevity -->
</activity>
  1. Adjust webpack.config.js to 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 onWindowFocusChanged method to detect when the app gains or loses focus.
  • Set Initial State: In the onCreate method, configure the status bar to be transparent and hidden with setFullScreen.
  • Touch Event Monitoring: To address the issue of onWindowFocusChanged not being triggered during slight drags, override the dispatchTouchEvent method. 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.

Special Thanks for Bringing Ideas to Light

A heartfelt thank you to those who have contributed valuable ideas and insights:

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!