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