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