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 (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 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 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 isFloatingMode()223 public boolean isFloatingMode() { 224 return mMode == InputViewMode.FLOATING_LAYOUT; 225 } 226 createButton(String text, final Runnable onClickCallback)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 updateSystemUiFlag(int flags)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 */ switchToFloatingMode()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 */ switchToSeparateNavBarMode(int navBarColor, boolean isLightNavBar)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 */ switchToExtendedNavBarMode(boolean isLightNavBar)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