Home | History | Annotate | Download | only in util
      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