Home | History | Annotate | Download | only in themednavbarkeyboard
      1 /*
      2  * Copyright (C) 2017 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.example.android.themednavbarkeyboard;
     18 
     19 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
     20 
     21 import android.content.Context;
     22 import android.graphics.Color;
     23 import android.graphics.drawable.GradientDrawable;
     24 import android.inputmethodservice.InputMethodService;
     25 import android.os.Build;
     26 import android.util.TypedValue;
     27 import android.view.Gravity;
     28 import android.view.View;
     29 import android.view.Window;
     30 import android.view.WindowInsets;
     31 import android.widget.Button;
     32 import android.widget.LinearLayout;
     33 import android.widget.TextView;
     34 
     35 /**
     36  * A sample {@link InputMethodService} to demonstrates how to integrate the software keyboard with
     37  * custom themed navigation bar.
     38  */
     39 public class ThemedNavBarKeyboard extends InputMethodService {
     40 
     41     private final int MINT_COLOR = 0xff98fb98;
     42     private final int LIGHT_RED = 0xff98fb98;
     43 
     44     private static final class BuildCompat {
     45         private static final boolean IS_RELEASE_BUILD = Build.VERSION.CODENAME.equals("REL");
     46 
     47         /**
     48          * The "effective" API version.
     49          * {@link android.os.Build.VERSION#SDK_INT} if the platform is a release build.
     50          * {@link android.os.Build.VERSION#SDK_INT} plus 1 if the platform is a development build.
     51          */
     52         private static final int EFFECTIVE_SDK_INT = IS_RELEASE_BUILD
     53                 ? Build.VERSION.SDK_INT
     54                 : Build.VERSION.SDK_INT + 1;
     55     }
     56 
     57     private KeyboardLayoutView mLayout;
     58 
     59     @Override
     60     public void onCreate() {
     61         super.onCreate();
     62         if (BuildCompat.EFFECTIVE_SDK_INT > Build.VERSION_CODES.P) {
     63             // Disable contrast for extended navbar gradient.
     64             getWindow().getWindow().setNavigationBarContrastEnforced(false);
     65         }
     66     }
     67 
     68     @Override
     69     public View onCreateInputView() {
     70         mLayout = new KeyboardLayoutView(this, getWindow().getWindow());
     71         return mLayout;
     72     }
     73 
     74     @Override
     75     public void onComputeInsets(Insets outInsets) {
     76         super.onComputeInsets(outInsets);
     77 
     78         // For floating mode, tweak Insets to avoid relayout in the target app.
     79         if (mLayout != null && mLayout.isFloatingMode()) {
     80             // Lying that the visible keyboard height is 0.
     81             outInsets.visibleTopInsets = getWindow().getWindow().getDecorView().getHeight();
     82             outInsets.contentTopInsets = getWindow().getWindow().getDecorView().getHeight();
     83 
     84             // But make sure that touch events are still sent to the IME.
     85             final int[] location = new int[2];
     86             mLayout.getLocationInWindow(location);
     87             final int x = location[0];
     88             final int y = location[1];
     89             outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION;
     90             outInsets.touchableRegion.set(x, y, x + mLayout.getWidth(), y + mLayout.getHeight());
     91         }
     92     }
     93 
     94     private enum InputViewMode {
     95         /**
     96          * The input view is adjacent to the bottom Navigation Bar (if present). In this mode the
     97          * IME is expected to control Navigation Bar appearance, including button color.
     98          *
     99          * <p>Call {@link Window#setNavigationBarColor(int)} to change the navigation bar color.</p>
    100          *
    101          * <p>Call {@link View#setSystemUiVisibility(int)} with
    102          * {@link View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} to optimize the navigation bar for
    103          * light color.</p>
    104          */
    105         SYSTEM_OWNED_NAV_BAR_LAYOUT,
    106         /**
    107          * The input view is extended to the bottom Navigation Bar (if present). In this mode the
    108          * IME is expected to control Navigation Bar appearance, including button color.
    109          *
    110          * <p>In this state, the system does not automatically place the input view above the
    111          * navigation bar.  You need to take care of the inset manually.</p>
    112          *
    113          * <p>Call {@link View#setSystemUiVisibility(int)} with
    114          * {@link View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} to optimize the navigation bar for
    115          * light color.</p>
    116 
    117          * @see View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    118          * @see View#SYSTEM_UI_FLAG_LAYOUT_STABLE
    119          */
    120         IME_OWNED_NAV_BAR_LAYOUT,
    121         /**
    122          * The input view is floating off of the bottom Navigation Bar region (if present). In this
    123          * mode the target application is expected to control Navigation Bar appearance, including
    124          * button color.
    125          */
    126         FLOATING_LAYOUT,
    127     }
    128 
    129     private final class KeyboardLayoutView extends LinearLayout {
    130 
    131         private final Window mWindow;
    132         private InputViewMode mMode = InputViewMode.SYSTEM_OWNED_NAV_BAR_LAYOUT;
    133 
    134         private void updateBottomPaddingIfNecessary(int newPaddingBottom) {
    135             if (getPaddingBottom() != newPaddingBottom) {
    136                 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom);
    137             }
    138         }
    139 
    140         @Override
    141         public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    142             if (insets.isConsumed()
    143                     || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) {
    144                 // In this case we are not interested in consuming NavBar region.
    145                 // Make sure that the bottom padding is empty.
    146                 updateBottomPaddingIfNecessary(0);
    147                 return insets;
    148             }
    149 
    150             // In some cases the bottom system window inset is not a navigation bar. Wear devices
    151             // that have bottom chin are examples.  For now, assume that it's a navigation bar if it
    152             // has the same height as the root window's stable bottom inset.
    153             final WindowInsets rootWindowInsets = getRootWindowInsets();
    154             if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom() !=
    155                     insets.getSystemWindowInsetBottom())) {
    156                 // This is probably not a NavBar.
    157                 updateBottomPaddingIfNecessary(0);
    158                 return insets;
    159             }
    160 
    161             final int possibleNavBarHeight = insets.getSystemWindowInsetBottom();
    162             updateBottomPaddingIfNecessary(possibleNavBarHeight);
    163             return possibleNavBarHeight <= 0
    164                     ? insets
    165                     : insets.replaceSystemWindowInsets(
    166                             insets.getSystemWindowInsetLeft(),
    167                             insets.getSystemWindowInsetTop(),
    168                             insets.getSystemWindowInsetRight(),
    169                             0 /* bottom */);
    170         }
    171 
    172         public KeyboardLayoutView(Context context, final Window window) {
    173             super(context);
    174             mWindow = window;
    175             setOrientation(VERTICAL);
    176 
    177             if (BuildCompat.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.O_MR1) {
    178                 final TextView textView = new TextView(context);
    179                 textView.setText("ThemedNavBarKeyboard works only on API 28 and higher devices");
    180                 textView.setGravity(Gravity.CENTER);
    181                 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
    182                 textView.setPadding(20, 10, 20, 20);
    183                 addView(textView);
    184                 setBackgroundColor(LIGHT_RED);
    185                 return;
    186             }
    187 
    188             // By default use "SeparateNavBarMode" mode.
    189             switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */);
    190             setBackgroundColor(MINT_COLOR);
    191 
    192             addView(createButton("Floating Mode", () -> {
    193                 switchToFloatingMode();
    194                 setBackgroundColor(Color.TRANSPARENT);
    195             }));
    196             addView(createButton("Extended Dark Navigation Bar", () -> {
    197                 switchToExtendedNavBarMode(false /* lightNavBar */);
    198                 final GradientDrawable drawable = new GradientDrawable(
    199                         GradientDrawable.Orientation.TOP_BOTTOM,
    200                         new int[] {MINT_COLOR, Color.DKGRAY});
    201                 setBackground(drawable);
    202             }));
    203             addView(createButton("Extended Light Navigation Bar", () -> {
    204                 switchToExtendedNavBarMode(true /* lightNavBar */);
    205                 final GradientDrawable drawable = new GradientDrawable(
    206                         GradientDrawable.Orientation.TOP_BOTTOM,
    207                         new int[] {MINT_COLOR, Color.WHITE});
    208                 setBackground(drawable);
    209             }));
    210             addView(createButton("Separate Dark Navigation Bar", () -> {
    211                 switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */);
    212                 setBackgroundColor(MINT_COLOR);
    213             }));
    214             addView(createButton("Separate Light Navigation Bar", () -> {
    215                 switchToSeparateNavBarMode(Color.GRAY, true /* lightNavBar */);
    216                 setBackgroundColor(MINT_COLOR);
    217             }));
    218 
    219             // Spacer
    220             addView(new View(getContext()), 0, 40);
    221         }
    222 
    223         public boolean isFloatingMode() {
    224             return mMode == InputViewMode.FLOATING_LAYOUT;
    225         }
    226 
    227         private View createButton(String text, final Runnable onClickCallback) {
    228             final Button button = new Button(getContext());
    229             button.setText(text);
    230             button.setOnClickListener(view -> onClickCallback.run());
    231             return button;
    232         }
    233 
    234         private void updateSystemUiFlag(int flags) {
    235             final int maskFlags = SYSTEM_UI_FLAG_LAYOUT_STABLE
    236                     | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    237                     | SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
    238             final int visFlags = getSystemUiVisibility();
    239             setSystemUiVisibility((visFlags & ~maskFlags) | (flags & maskFlags));
    240         }
    241 
    242         /**
    243          * Updates the current input view mode to {@link InputViewMode#FLOATING_LAYOUT}.
    244          */
    245         private void switchToFloatingMode() {
    246             mMode = InputViewMode.FLOATING_LAYOUT;
    247 
    248             final int prevFlags = mWindow.getAttributes().flags;
    249 
    250             // This allows us to keep the navigation bar appearance based on the target application,
    251             // rather than the IME itself.
    252             mWindow.setFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    253 
    254             updateSystemUiFlag(0);
    255 
    256             // View#onApplyWindowInsets() will not be called if direct or indirect parent View
    257             // consumes all the insets.  Hence we need to make sure that the bottom padding is
    258             // cleared here.
    259             updateBottomPaddingIfNecessary(0);
    260 
    261             // For some reasons, seems that we need to post another requestLayout() when
    262             // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is changed.
    263             // TODO: Investigate the reason.
    264             if ((prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) {
    265                 post(() -> requestLayout());
    266             }
    267         }
    268 
    269         /**
    270          * Updates the current input view mode to {@link InputViewMode#SYSTEM_OWNED_NAV_BAR_LAYOUT}.
    271          *
    272          * @param navBarColor color to be passed to {@link Window#setNavigationBarColor(int)}.
    273          *                    {@link Color#TRANSPARENT} cannot be used here because it hides the
    274          *                    color view itself. Consider floating mode for that use case.
    275          * @param isLightNavBar {@code true} when the navigation bar should be optimized for light
    276          *                      color
    277          */
    278         private void switchToSeparateNavBarMode(int navBarColor, boolean isLightNavBar) {
    279             mMode = InputViewMode.SYSTEM_OWNED_NAV_BAR_LAYOUT;
    280             mWindow.setNavigationBarColor(navBarColor);
    281 
    282             // This allows us to use setNavigationBarColor() + SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.
    283             mWindow.setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    284 
    285             updateSystemUiFlag(isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0);
    286 
    287             // View#onApplyWindowInsets() will not be called if direct or indirect parent View
    288             // consumes all the insets.  Hence we need to make sure that the bottom padding is
    289             // cleared here.
    290             updateBottomPaddingIfNecessary(0);
    291         }
    292 
    293         /**
    294          * Updates the current input view mode to {@link InputViewMode#IME_OWNED_NAV_BAR_LAYOUT}.
    295          *
    296          * @param isLightNavBar {@code true} when the navigation bar should be optimized for light
    297          *                      color
    298          */
    299         private void switchToExtendedNavBarMode(boolean isLightNavBar) {
    300             mMode = InputViewMode.IME_OWNED_NAV_BAR_LAYOUT;
    301 
    302             // This hides the ColorView.
    303             mWindow.setNavigationBarColor(Color.TRANSPARENT);
    304 
    305             // This allows us to use setNavigationBarColor() + SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.
    306             mWindow.setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    307 
    308             updateSystemUiFlag(SYSTEM_UI_FLAG_LAYOUT_STABLE
    309                     | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    310                     | (isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0));
    311         }
    312     }
    313 }
    314