1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.setupwizardlib.util; 18 19 import android.annotation.SuppressLint; 20 import android.annotation.TargetApi; 21 import android.app.Dialog; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.os.Build.VERSION; 25 import android.os.Build.VERSION_CODES; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.Window; 31 import android.view.WindowInsets; 32 import android.view.WindowManager; 33 34 import com.android.setupwizardlib.R; 35 36 /** 37 * A helper class to manage the system navigation bar and status bar. This will add various 38 * systemUiVisibility flags to the given Window or View to make them follow the Setup Wizard style. 39 * 40 * When the useImmersiveMode intent extra is true, a screen in Setup Wizard should hide the system 41 * bars using methods from this class. For Lollipop, {@link #hideSystemBars(android.view.Window)} 42 * will completely hide the system navigation bar and change the status bar to transparent, and 43 * layout the screen contents (usually the illustration) behind it. 44 */ 45 public class SystemBarHelper { 46 47 private static final String TAG = "SystemBarHelper"; 48 49 @SuppressLint("InlinedApi") 50 private static final int DEFAULT_IMMERSIVE_FLAGS = 51 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 52 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 53 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 54 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; 55 56 @SuppressLint("InlinedApi") 57 private static final int DIALOG_IMMERSIVE_FLAGS = 58 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 59 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 60 61 /** 62 * Needs to be equal to View.STATUS_BAR_DISABLE_BACK 63 */ 64 private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; 65 66 /** 67 * The maximum number of retries when peeking the decor view. When polling for the decor view, 68 * waiting it to be installed, set a maximum number of retries. 69 */ 70 private static final int PEEK_DECOR_VIEW_RETRIES = 3; 71 72 /** 73 * Hide the navigation bar for a dialog. 74 * 75 * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. 76 */ hideSystemBars(final Dialog dialog)77 public static void hideSystemBars(final Dialog dialog) { 78 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 79 final Window window = dialog.getWindow(); 80 temporarilyDisableDialogFocus(window); 81 addVisibilityFlag(window, DIALOG_IMMERSIVE_FLAGS); 82 addImmersiveFlagsToDecorView(window, DIALOG_IMMERSIVE_FLAGS); 83 84 // Also set the navigation bar and status bar to transparent color. Note that this 85 // doesn't work if android.R.boolean.config_enableTranslucentDecor is false. 86 window.setNavigationBarColor(0); 87 window.setStatusBarColor(0); 88 } 89 } 90 91 /** 92 * Hide the navigation bar, make the color of the status and navigation bars transparent, and 93 * specify {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} flag so that the content is laid-out 94 * behind the transparent status bar. This is commonly used with 95 * {@link android.app.Activity#getWindow()} to make the navigation and status bars follow the 96 * Setup Wizard style. 97 * 98 * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. 99 */ hideSystemBars(final Window window)100 public static void hideSystemBars(final Window window) { 101 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 102 addVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS); 103 addImmersiveFlagsToDecorView(window, DEFAULT_IMMERSIVE_FLAGS); 104 105 // Also set the navigation bar and status bar to transparent color. Note that this 106 // doesn't work if android.R.boolean.config_enableTranslucentDecor is false. 107 window.setNavigationBarColor(0); 108 window.setStatusBarColor(0); 109 } 110 } 111 112 /** 113 * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility 114 * flags regardless of whether it is originally present. You should also manually reset the 115 * navigation bar and status bar colors, as this method doesn't know what value to revert it to. 116 */ showSystemBars(final Dialog dialog, final Context context)117 public static void showSystemBars(final Dialog dialog, final Context context) { 118 showSystemBars(dialog.getWindow(), context); 119 } 120 121 /** 122 * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility 123 * flags regardless of whether it is originally present. You should also manually reset the 124 * navigation bar and status bar colors, as this method doesn't know what value to revert it to. 125 */ showSystemBars(final Window window, final Context context)126 public static void showSystemBars(final Window window, final Context context) { 127 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 128 removeVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS); 129 removeImmersiveFlagsFromDecorView(window, DEFAULT_IMMERSIVE_FLAGS); 130 131 if (context != null) { 132 //noinspection AndroidLintInlinedApi 133 final TypedArray typedArray = context.obtainStyledAttributes(new int[]{ 134 android.R.attr.statusBarColor, android.R.attr.navigationBarColor}); 135 final int statusBarColor = typedArray.getColor(0, 0); 136 final int navigationBarColor = typedArray.getColor(1, 0); 137 window.setStatusBarColor(statusBarColor); 138 window.setNavigationBarColor(navigationBarColor); 139 typedArray.recycle(); 140 } 141 } 142 } 143 144 /** 145 * Convenience method to add a visibility flag in addition to the existing ones. 146 */ addVisibilityFlag(final View view, final int flag)147 public static void addVisibilityFlag(final View view, final int flag) { 148 if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { 149 final int vis = view.getSystemUiVisibility(); 150 view.setSystemUiVisibility(vis | flag); 151 } 152 } 153 154 /** 155 * Convenience method to add a visibility flag in addition to the existing ones. 156 */ addVisibilityFlag(final Window window, final int flag)157 public static void addVisibilityFlag(final Window window, final int flag) { 158 if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { 159 WindowManager.LayoutParams attrs = window.getAttributes(); 160 attrs.systemUiVisibility |= flag; 161 window.setAttributes(attrs); 162 } 163 } 164 165 /** 166 * Convenience method to remove a visibility flag from the view, leaving other flags that are 167 * not specified intact. 168 */ removeVisibilityFlag(final View view, final int flag)169 public static void removeVisibilityFlag(final View view, final int flag) { 170 if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { 171 final int vis = view.getSystemUiVisibility(); 172 view.setSystemUiVisibility(vis & ~flag); 173 } 174 } 175 176 /** 177 * Convenience method to remove a visibility flag from the window, leaving other flags that are 178 * not specified intact. 179 */ removeVisibilityFlag(final Window window, final int flag)180 public static void removeVisibilityFlag(final Window window, final int flag) { 181 if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { 182 WindowManager.LayoutParams attrs = window.getAttributes(); 183 attrs.systemUiVisibility &= ~flag; 184 window.setAttributes(attrs); 185 } 186 } 187 setBackButtonVisible(final Window window, final boolean visible)188 public static void setBackButtonVisible(final Window window, final boolean visible) { 189 if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { 190 if (visible) { 191 removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); 192 } else { 193 addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); 194 } 195 } 196 } 197 198 /** 199 * Set a view to be resized when the keyboard is shown. This will set the bottom margin of the 200 * view to be immediately above the keyboard, and assumes that the view sits immediately above 201 * the navigation bar. 202 * 203 * <p>Note that you must set {@link android.R.attr#windowSoftInputMode} to {@code adjustResize} 204 * for this class to work. Otherwise window insets are not dispatched and this method will have 205 * no effect. 206 * 207 * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. 208 * 209 * @param view The view to be resized when the keyboard is shown. 210 */ setImeInsetView(final View view)211 public static void setImeInsetView(final View view) { 212 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 213 view.setOnApplyWindowInsetsListener(new WindowInsetsListener()); 214 } 215 } 216 217 /** 218 * Add the specified immersive flags to the decor view of the window, because 219 * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view 220 * instead of the window. 221 */ 222 @TargetApi(VERSION_CODES.LOLLIPOP) addImmersiveFlagsToDecorView(final Window window, final int vis)223 private static void addImmersiveFlagsToDecorView(final Window window, final int vis) { 224 getDecorView(window, new OnDecorViewInstalledListener() { 225 @Override 226 public void onDecorViewInstalled(View decorView) { 227 addVisibilityFlag(decorView, vis); 228 } 229 }); 230 } 231 232 @TargetApi(VERSION_CODES.LOLLIPOP) removeImmersiveFlagsFromDecorView(final Window window, final int vis)233 private static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) { 234 getDecorView(window, new OnDecorViewInstalledListener() { 235 @Override 236 public void onDecorViewInstalled(View decorView) { 237 removeVisibilityFlag(decorView, vis); 238 } 239 }); 240 } 241 getDecorView(Window window, OnDecorViewInstalledListener callback)242 private static void getDecorView(Window window, OnDecorViewInstalledListener callback) { 243 new DecorViewFinder().getDecorView(window, callback, PEEK_DECOR_VIEW_RETRIES); 244 } 245 246 private static class DecorViewFinder { 247 248 private final Handler mHandler = new Handler(); 249 private Window mWindow; 250 private int mRetries; 251 private OnDecorViewInstalledListener mCallback; 252 253 private Runnable mCheckDecorViewRunnable = new Runnable() { 254 @Override 255 public void run() { 256 // Use peekDecorView instead of getDecorView so that clients can still set window 257 // features after calling this method. 258 final View decorView = mWindow.peekDecorView(); 259 if (decorView != null) { 260 mCallback.onDecorViewInstalled(decorView); 261 } else { 262 mRetries--; 263 if (mRetries >= 0) { 264 // If the decor view is not installed yet, try again in the next loop. 265 mHandler.post(mCheckDecorViewRunnable); 266 } else { 267 Log.w(TAG, "Cannot get decor view of window: " + mWindow); 268 } 269 } 270 } 271 }; 272 getDecorView(Window window, OnDecorViewInstalledListener callback, int retries)273 public void getDecorView(Window window, OnDecorViewInstalledListener callback, 274 int retries) { 275 mWindow = window; 276 mRetries = retries; 277 mCallback = callback; 278 mCheckDecorViewRunnable.run(); 279 } 280 } 281 282 private interface OnDecorViewInstalledListener { 283 onDecorViewInstalled(View decorView)284 void onDecorViewInstalled(View decorView); 285 } 286 287 /** 288 * Apply a hack to temporarily set the window to not focusable, so that the navigation bar 289 * will not show up during the transition. 290 */ temporarilyDisableDialogFocus(final Window window)291 private static void temporarilyDisableDialogFocus(final Window window) { 292 window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 293 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); 294 // Add the SOFT_INPUT_IS_FORWARD_NAVIGATION_FLAG. This is normally done by the system when 295 // FLAG_NOT_FOCUSABLE is not set. Setting this flag allows IME to be shown automatically 296 // if the dialog has editable text fields. 297 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION); 298 new Handler().post(new Runnable() { 299 @Override 300 public void run() { 301 window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); 302 } 303 }); 304 } 305 306 @TargetApi(VERSION_CODES.LOLLIPOP) 307 private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener { 308 private int mBottomOffset; 309 private boolean mHasCalculatedBottomOffset = false; 310 311 @Override onApplyWindowInsets(View view, WindowInsets insets)312 public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { 313 if (!mHasCalculatedBottomOffset) { 314 mBottomOffset = getBottomDistance(view); 315 mHasCalculatedBottomOffset = true; 316 } 317 318 int bottomInset = insets.getSystemWindowInsetBottom(); 319 320 final int bottomMargin = Math.max( 321 insets.getSystemWindowInsetBottom() - mBottomOffset, 0); 322 323 final ViewGroup.MarginLayoutParams lp = 324 (ViewGroup.MarginLayoutParams) view.getLayoutParams(); 325 // Check that we have enough space to apply the bottom margins before applying it. 326 // Otherwise the framework may think that the view is empty and exclude it from layout. 327 if (bottomMargin < lp.bottomMargin + view.getHeight()) { 328 lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin); 329 view.setLayoutParams(lp); 330 bottomInset = 0; 331 } 332 333 334 return insets.replaceSystemWindowInsets( 335 insets.getSystemWindowInsetLeft(), 336 insets.getSystemWindowInsetTop(), 337 insets.getSystemWindowInsetRight(), 338 bottomInset 339 ); 340 } 341 } 342 getBottomDistance(View view)343 private static int getBottomDistance(View view) { 344 int[] coords = new int[2]; 345 view.getLocationInWindow(coords); 346 return view.getRootView().getHeight() - coords[1] - view.getHeight(); 347 } 348 } 349