Unity tidbits: changing the visibility of Android’s navigation and status bars, and implementing immersive mode

I was recently working on a mobile Unity project at home and I wanted to take advantage of Android’s immersive mode, which allows you to hide elements of the OS chrome such as the navigation bar or the status (top) bar. Unity itself has a Screen.fullScreen property that tries to control the appearance of the game interface, but it falls short in this case: on Android, all it does is dim the system bars a little bit. The project settings have some kind of status bar control, but not only it doesn’t work for me, it’s also limited in what it can do. After all, the Android system itself allows you to control the visibility, color, and even scaling model of the bars, and that’s what I wanted to tap into.

That is somewhat easy in native Android code. However, I wanted to run my code from a pure Unity game since this current project is not wrapped by a native application, and I thought this was a good reason to investigate how well Unity allows you to call native Android code.

The answer is, pretty easily. Unity has two classes, AndroidJavaObject and AndroidJavaClass, that allows developers to create objects and interact with the Android side of your application by essentially injecting calls, all without never actually creating a native Java application. It has pitfalls of its own – for example, being forced to know the secret handshake so calls run from the UI thread – but all in all it does what it’s supposed to do in an efficient way.

This means that implementing calls to change how the status and navigation bars work from the Unity side was pretty straightforward, and this is the subject of this first “Unity tidbit” I’m sharing from the collection of random Unity classes I created to help me when developing Unity.

Without further ado, the class I came up with to solve this problem is called ApplicationChrome. It uses a simple interface to do all the underlying changes to the Android application Chrome from the Unity side:

#if UNITY_ANDROID && !UNITY_EDITOR
#define USE_ANDROID
#endif

using System;
using System.Collections.Generic;
using UnityEngine;

/**
 * @author zeh fernando
 */
class ApplicationChrome {

	/**
	 * Manipulates the system application chrome to change the way the status bar and navigation bar work
	 *
	 * References:
	 * . http://developer.android.com/reference/android/view/View.html#setSystemUiVisibility(int)
	 * . http://forum.unity3d.com/threads/calling-setsystemuivisibility.139445/#post-952946
	 * . http://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_LAYOUT_IN_SCREEN
	 **/

	// Enums
	public enum States {
		Unknown,
		Visible,
		VisibleOverContent,
		TranslucentOverContent,
		Hidden
	}

	// Constants
	private const uint DEFAULT_BACKGROUND_COLOR = 0xff000000;

	#if USE_ANDROID
		// Original Android flags
		private const int VIEW_SYSTEM_UI_FLAG_VISIBLE = 0;					// Added in API 14 (Android 4.0.x): Status bar visible (the default)
		private const int VIEW_SYSTEM_UI_FLAG_LOW_PROFILE = 1;				// Added in API 14 (Android 4.0.x): Low profile for games, book readers, and video players; the status bar and/or navigation icons are dimmed out (if visible)
		private const int VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION = 2;			// Added in API 14 (Android 4.0.x): Hides all navigation. Cleared when theres any user interaction.
		private const int VIEW_SYSTEM_UI_FLAG_FULLSCREEN = 4;				// Added in API 16 (Android 4.1.x): Hides status bar. Does nothing in Unity (already hidden if "status bar hidden" is checked)
		private const int VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE = 256;			// Added in API 16 (Android 4.1.x): ?
		private const int VIEW_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 512;	// Added in API 16 (Android 4.1.x): like HIDE_NAVIGATION, but for layouts? it causes the layout to be drawn like that, even if the whole view isn't (to avoid artifacts in animation)
		private const int VIEW_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 1024;		// Added in API 16 (Android 4.1.x): like FULLSCREEN, but for layouts? it causes the layout to be drawn like that, even if the whole view isn't (to avoid artifacts in animation)
		private const int VIEW_SYSTEM_UI_FLAG_IMMERSIVE = 2048;				// Added in API 19 (Android 4.4): like HIDE_NAVIGATION, but interactive (it's a modifier for HIDE_NAVIGATION, needs to be used with it)
		private const int VIEW_SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 4096;		// Added in API 19 (Android 4.4): tells that HIDE_NAVIGATION and FULSCREEN are interactive (also just a modifier)

		private static int WINDOW_FLAG_FULLSCREEN = 0x00000400;
		private static int WINDOW_FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
		private static int WINDOW_FLAG_LAYOUT_IN_SCREEN = 0x00000100;
		private static int WINDOW_FLAG_TRANSLUCENT_STATUS = 0x04000000;
		private static int WINDOW_FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;
		private static int WINDOW_FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = -2147483648; // 0x80000000; // Added in API 21 (Android 5.0): tells the Window is responsible for drawing the background for the system bars. If set, the system bars are drawn with a transparent background and the corresponding areas in this window are filled with the colors specified in getStatusBarColor() and getNavigationBarColor()

		// Current values
		private static int systemUiVisibilityValue;
		private static int flagsValue;
	#endif

	// Properties
	private static States _statusBarState;
	private static States _navigationBarState;

	private static uint _statusBarColor = DEFAULT_BACKGROUND_COLOR;
	private static uint _navigationBarColor = DEFAULT_BACKGROUND_COLOR;

	private static bool _isStatusBarTranslucent; // Just so we know whether its translucent when hidden or not
	private static bool _isNavigationBarTranslucent;

	private static bool _dimmed;


	// ================================================================================================================
	// INTERNAL INTERFACE ---------------------------------------------------------------------------------------------

	static ApplicationChrome() {
		applyUIStates();
		applyUIColors();
	}

	private static void applyUIStates() {
		#if USE_ANDROID
			applyUIStatesAndroid();
		#endif
	}

	private static void applyUIColors() {
		#if USE_ANDROID
			applyUIColorsAndroid();
		#endif
	}

	#if USE_ANDROID

		private static void applyUIStatesAndroid() {
			int newFlagsValue = 0;
			int newSystemUiVisibilityValue = 0;

			// Apply dim values
			if (_dimmed) newSystemUiVisibilityValue |= VIEW_SYSTEM_UI_FLAG_LOW_PROFILE;

			// Apply color values
			if (_navigationBarColor != DEFAULT_BACKGROUND_COLOR || _statusBarColor != DEFAULT_BACKGROUND_COLOR) newFlagsValue |= WINDOW_FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;

			// Apply status bar values
			switch (_statusBarState) {
				case States.Visible:
					_isStatusBarTranslucent = false;
					newFlagsValue |= WINDOW_FLAG_FORCE_NOT_FULLSCREEN;
					break;
				case States.VisibleOverContent:
					_isStatusBarTranslucent = false;
					newFlagsValue |= WINDOW_FLAG_FORCE_NOT_FULLSCREEN | WINDOW_FLAG_LAYOUT_IN_SCREEN;
					newSystemUiVisibilityValue |= VIEW_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
					break;
				case States.TranslucentOverContent:
					_isStatusBarTranslucent = true;
					newFlagsValue |= WINDOW_FLAG_FORCE_NOT_FULLSCREEN | WINDOW_FLAG_LAYOUT_IN_SCREEN | WINDOW_FLAG_TRANSLUCENT_STATUS;
					newSystemUiVisibilityValue |= VIEW_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
					break;
				case States.Hidden:
					newFlagsValue |= WINDOW_FLAG_FULLSCREEN | WINDOW_FLAG_LAYOUT_IN_SCREEN;
					if (_isStatusBarTranslucent) newFlagsValue |= WINDOW_FLAG_TRANSLUCENT_STATUS;
					break;
			}

			// Applies navigation values
			switch (_navigationBarState) {
				case States.Visible:
					_isNavigationBarTranslucent = false;
					newSystemUiVisibilityValue |= VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE;
					break;
				case States.VisibleOverContent:
					// TODO: Side effect: forces status bar over content if set to VISIBLE
					_isNavigationBarTranslucent = false;
					newSystemUiVisibilityValue |= VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE | VIEW_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
					break;
				case States.TranslucentOverContent:
					// TODO: Side effect: forces status bar over content if set to VISIBLE
					_isNavigationBarTranslucent = true;
					newFlagsValue |= WINDOW_FLAG_TRANSLUCENT_NAVIGATION;
					newSystemUiVisibilityValue |= VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE | VIEW_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
					break;
				case States.Hidden:
					newSystemUiVisibilityValue |= VIEW_SYSTEM_UI_FLAG_FULLSCREEN | VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION | VIEW_SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
					if (_isNavigationBarTranslucent) newFlagsValue |= WINDOW_FLAG_TRANSLUCENT_NAVIGATION;
					break;
			}

			if (Screen.fullScreen) Screen.fullScreen = false;

			// Applies everything natively
			setFlags(newFlagsValue);
			setSystemUiVisibility(newSystemUiVisibilityValue);
		}

		private static void runOnAndroidUiThread(Action target) {
			using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
				using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) {
					activity.Call("runOnUiThread", new AndroidJavaRunnable(target));
				}
			}
		}

		private static void setSystemUiVisibility(int value) {
			if (systemUiVisibilityValue != value) {
				systemUiVisibilityValue = value;
				runOnAndroidUiThread(setSystemUiVisibilityInThread);
			}
		}

		private static void setSystemUiVisibilityInThread() {
			//Debug.Log("SYSTEM FLAGS: " + systemUiVisibilityValue);
			using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
				using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) {
					using (var window = activity.Call<AndroidJavaObject>("getWindow")) {
						using (var view = window.Call<AndroidJavaObject>("getDecorView")) {
							view.Call("setSystemUiVisibility", systemUiVisibilityValue);
						}
					}
				}
			}
		}

		private static void setFlags(int value) {
			if (flagsValue != value) {
				flagsValue = value;
				runOnAndroidUiThread(setFlagsInThread);
			}
		}

		private static void setFlagsInThread() {
			//Debug.Log("FLAGS: " + flagsValue);
			using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
				using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) {
					using (var window = activity.Call<AndroidJavaObject>("getWindow")) {
						window.Call("setFlags", flagsValue, -1); // (int)0x7FFFFFFF
					}
				}
			}
		}

		private static void applyUIColorsAndroid() {
			runOnAndroidUiThread(applyUIColorsAndroidInThread);
		}

		private static void applyUIColorsAndroidInThread() {
			using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
				using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) {
					using (var window = activity.Call<AndroidJavaObject>("getWindow")) {
						//Debug.Log("Colors SET: " + _statusBarColor);
						window.Call("setStatusBarColor", unchecked((int)_statusBarColor));
						window.Call("setNavigationBarColor", unchecked((int)_navigationBarColor));
					}
				}
			}
		}

	#endif

	// ================================================================================================================
	// ACCESSOR INTERFACE ---------------------------------------------------------------------------------------------

	public static States navigationBarState {
		get { return _navigationBarState; }
		set {
			if (_navigationBarState != value) {
				_navigationBarState = value;
				applyUIStates();
			}
		}
	}

	public static States statusBarState {
		get { return _statusBarState; }
		set {
			if (_statusBarState != value) {
				_statusBarState = value;
				applyUIStates();
			}
		}
	}

	public static bool dimmed {
		get { return _dimmed; }
		set {
			if (_dimmed != value) {
				_dimmed = value;
				applyUIStates();
			}
		}
	}

	public static uint statusBarColor {
		get { return _statusBarColor; }
		set {
			if (_statusBarColor != value) {
				_statusBarColor = value;
				applyUIColors();
				applyUIStates();
			}
		}
	}

	public static uint navigationBarColor {
		get { return _navigationBarColor; }
		set {
			if (_navigationBarColor != value) {
				_navigationBarColor = value;
				applyUIColors();
				applyUIStates();
			}
		}
	}
}

And using it is, I hope, simple enough:

// Toggles the dimmed out state (where status/navigation content is darker)
ApplicationChrome.dimmed = !ApplicationChrome.dimmed;

// Set the status/navigation background color (set to 0xff000000 to disable)
ApplicationChrome.statusBarColor = ApplicationChrome.navigationBarColor = 0xffff3300;

// Makes the status bar and navigation bar visible (default)
ApplicationChrome.statusBarState = ApplicationChrome.navigationBarState = ApplicationChrome.States.Visible;

// Makes the status bar and navigation bar visible over the content (different content resize method) 
ApplicationChrome.statusBarState = ApplicationChrome.navigationBarState = ApplicationChrome.States.VisibleOverContent;

// Makes the status bar and navigation bar visible over the content, but a bit transparent
ApplicationChrome.statusBarState = ApplicationChrome.navigationBarState = ApplicationChrome.States.TranslucentOverContent;

// Makes the status bar and navigation bar invisible (animated)
ApplicationChrome.statusBarState = ApplicationChrome.navigationBarState = ApplicationChrome.States.Hidden;

This solution has caveats of its own: you can’t mix dimmed state with translucent state, for example, and using colored backgrounds only works well if the bars are fully opaque. I believe those are native Android problems more than anything; the APIs for screen control have grown little by little over time, and in different ways, causing some flags to be at odds with each other (I’ve love to be wrong, though, in case there’s some room for improvement in the implementation).

Finally, for reference, this is what each of those modes looks like:

Visible bars (default)

Visible bars (default)

Visible bars, dimmed

Visible bars, dimmed

Visible bars, colored background

Visible bars, colored background

Visible bars over content

Visible bars over content

Translucent bars over content

Translucent bars over content

Hidden bars

Hidden bars (“immersive”)

The images above were created with a test Unity application I compiled for Android. Feel free to download the installation APK and try for yourself on Android devices. And while the screenshots show the status and navigation bars having the same state for the sake of simplicity, they can be controlled separately.