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 onCreate()60 public void onCreate() { 61 super.onCreate(); 62 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 63 // Disable contrast for extended navbar gradient. 64 getWindow().getWindow().setNavigationBarContrastEnforced(false); 65 } 66 } 67 68 @Override onCreateInputView()69 public View onCreateInputView() { 70 mLayout = new KeyboardLayoutView(this, getWindow().getWindow()); 71 return mLayout; 72 } 73 74 @Override onComputeInsets(Insets outInsets)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 updateBottomPaddingIfNecessary(int newPaddingBottom)134 private void updateBottomPaddingIfNecessary(int newPaddingBottom) { 135 if (getPaddingBottom() != newPaddingBottom) { 136 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom); 137 } 138 } 139 140 @Override onApplyWindowInsets(WindowInsets insets)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 KeyboardLayoutView(Context context, final Window window)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 { 193 final LinearLayout subLayout = new LinearLayout(context); 194 { 195 final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 196 LinearLayout.LayoutParams.MATCH_PARENT, 197 LinearLayout.LayoutParams.WRAP_CONTENT); 198 lp.weight = 50; 199 subLayout.addView(createButton("BACK_DISPOSITION\nDEFAULT", () -> { 200 setBackDisposition(BACK_DISPOSITION_DEFAULT); 201 }), lp); 202 } 203 { 204 final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 205 LinearLayout.LayoutParams.MATCH_PARENT, 206 LinearLayout.LayoutParams.WRAP_CONTENT); 207 lp.weight = 50; 208 subLayout.addView(createButton("BACK_DISPOSITION\nADJUST_NOTHING", () -> { 209 setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING); 210 }), lp); 211 } 212 addView(subLayout); 213 } 214 215 addView(createButton("Floating Mode", () -> { 216 switchToFloatingMode(); 217 setBackgroundColor(Color.TRANSPARENT); 218 })); 219 addView(createButton("Extended Dark Navigation Bar", () -> { 220 switchToExtendedNavBarMode(false /* lightNavBar */); 221 final GradientDrawable drawable = new GradientDrawable( 222 GradientDrawable.Orientation.TOP_BOTTOM, 223 new int[] {MINT_COLOR, Color.DKGRAY}); 224 setBackground(drawable); 225 })); 226 addView(createButton("Extended Light Navigation Bar", () -> { 227 switchToExtendedNavBarMode(true /* lightNavBar */); 228 final GradientDrawable drawable = new GradientDrawable( 229 GradientDrawable.Orientation.TOP_BOTTOM, 230 new int[] {MINT_COLOR, Color.WHITE}); 231 setBackground(drawable); 232 })); 233 addView(createButton("Separate Dark Navigation Bar", () -> { 234 switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */); 235 setBackgroundColor(MINT_COLOR); 236 })); 237 addView(createButton("Separate Light Navigation Bar", () -> { 238 switchToSeparateNavBarMode(Color.GRAY, true /* lightNavBar */); 239 setBackgroundColor(MINT_COLOR); 240 })); 241 242 // Spacer 243 addView(new View(getContext()), 0, 40); 244 } 245 isFloatingMode()246 public boolean isFloatingMode() { 247 return mMode == InputViewMode.FLOATING_LAYOUT; 248 } 249 createButton(String text, final Runnable onClickCallback)250 private View createButton(String text, final Runnable onClickCallback) { 251 final Button button = new Button(getContext()); 252 button.setText(text); 253 button.setOnClickListener(view -> onClickCallback.run()); 254 return button; 255 } 256 updateSystemUiFlag(int flags)257 private void updateSystemUiFlag(int flags) { 258 final int maskFlags = SYSTEM_UI_FLAG_LAYOUT_STABLE 259 | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 260 | SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; 261 final int visFlags = getSystemUiVisibility(); 262 setSystemUiVisibility((visFlags & ~maskFlags) | (flags & maskFlags)); 263 } 264 265 /** 266 * Updates the current input view mode to {@link InputViewMode#FLOATING_LAYOUT}. 267 */ switchToFloatingMode()268 private void switchToFloatingMode() { 269 mMode = InputViewMode.FLOATING_LAYOUT; 270 271 final int prevFlags = mWindow.getAttributes().flags; 272 273 // This allows us to keep the navigation bar appearance based on the target application, 274 // rather than the IME itself. 275 mWindow.setFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 276 277 updateSystemUiFlag(0); 278 279 // View#onApplyWindowInsets() will not be called if direct or indirect parent View 280 // consumes all the insets. Hence we need to make sure that the bottom padding is 281 // cleared here. 282 updateBottomPaddingIfNecessary(0); 283 284 // For some reasons, seems that we need to post another requestLayout() when 285 // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is changed. 286 // TODO: Investigate the reason. 287 if ((prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) { 288 post(() -> requestLayout()); 289 } 290 } 291 292 /** 293 * Updates the current input view mode to {@link InputViewMode#SYSTEM_OWNED_NAV_BAR_LAYOUT}. 294 * 295 * @param navBarColor color to be passed to {@link Window#setNavigationBarColor(int)}. 296 * {@link Color#TRANSPARENT} cannot be used here because it hides the 297 * color view itself. Consider floating mode for that use case. 298 * @param isLightNavBar {@code true} when the navigation bar should be optimized for light 299 * color 300 */ switchToSeparateNavBarMode(int navBarColor, boolean isLightNavBar)301 private void switchToSeparateNavBarMode(int navBarColor, boolean isLightNavBar) { 302 mMode = InputViewMode.SYSTEM_OWNED_NAV_BAR_LAYOUT; 303 mWindow.setNavigationBarColor(navBarColor); 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(isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0); 309 310 // View#onApplyWindowInsets() will not be called if direct or indirect parent View 311 // consumes all the insets. Hence we need to make sure that the bottom padding is 312 // cleared here. 313 updateBottomPaddingIfNecessary(0); 314 } 315 316 /** 317 * Updates the current input view mode to {@link InputViewMode#IME_OWNED_NAV_BAR_LAYOUT}. 318 * 319 * @param isLightNavBar {@code true} when the navigation bar should be optimized for light 320 * color 321 */ switchToExtendedNavBarMode(boolean isLightNavBar)322 private void switchToExtendedNavBarMode(boolean isLightNavBar) { 323 mMode = InputViewMode.IME_OWNED_NAV_BAR_LAYOUT; 324 325 // This hides the ColorView. 326 mWindow.setNavigationBarColor(Color.TRANSPARENT); 327 328 // This allows us to use setNavigationBarColor() + SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR. 329 mWindow.setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 330 331 updateSystemUiFlag(SYSTEM_UI_FLAG_LAYOUT_STABLE 332 | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 333 | (isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0)); 334 } 335 } 336 } 337