| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.themednavbarkeyboard; |
| |
| import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; |
| |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.graphics.drawable.GradientDrawable; |
| import android.inputmethodservice.InputMethodService; |
| import android.os.Build; |
| import android.util.TypedValue; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.Window; |
| import android.view.WindowInsets; |
| import android.widget.Button; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| /** |
| * A sample {@link InputMethodService} to demonstrates how to integrate the software keyboard with |
| * custom themed navigation bar. |
| */ |
| public class ThemedNavBarKeyboard extends InputMethodService { |
| |
| private final int MINT_COLOR = 0xff98fb98; |
| private final int LIGHT_RED = 0xff98fb98; |
| |
| private static final class BuildCompat { |
| private static final boolean IS_RELEASE_BUILD = Build.VERSION.CODENAME.equals("REL"); |
| |
| /** |
| * The "effective" API version. |
| * {@link android.os.Build.VERSION#SDK_INT} if the platform is a release build. |
| * {@link android.os.Build.VERSION#SDK_INT} plus 1 if the platform is a development build. |
| */ |
| private static final int EFFECTIVE_SDK_INT = IS_RELEASE_BUILD |
| ? Build.VERSION.SDK_INT |
| : Build.VERSION.SDK_INT + 1; |
| } |
| |
| private KeyboardLayoutView mLayout; |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
| // Disable contrast for extended navbar gradient. |
| getWindow().getWindow().setNavigationBarContrastEnforced(false); |
| } |
| } |
| |
| @Override |
| public View onCreateInputView() { |
| mLayout = new KeyboardLayoutView(this, getWindow().getWindow()); |
| return mLayout; |
| } |
| |
| @Override |
| public void onComputeInsets(Insets outInsets) { |
| super.onComputeInsets(outInsets); |
| |
| // For floating mode, tweak Insets to avoid relayout in the target app. |
| if (mLayout != null && mLayout.isFloatingMode()) { |
| // Lying that the visible keyboard height is 0. |
| outInsets.visibleTopInsets = getWindow().getWindow().getDecorView().getHeight(); |
| outInsets.contentTopInsets = getWindow().getWindow().getDecorView().getHeight(); |
| |
| // But make sure that touch events are still sent to the IME. |
| final int[] location = new int[2]; |
| mLayout.getLocationInWindow(location); |
| final int x = location[0]; |
| final int y = location[1]; |
| outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION; |
| outInsets.touchableRegion.set(x, y, x + mLayout.getWidth(), y + mLayout.getHeight()); |
| } |
| } |
| |
| private enum InputViewMode { |
| /** |
| * The input view is adjacent to the bottom Navigation Bar (if present). In this mode the |
| * IME is expected to control Navigation Bar appearance, including button color. |
| * |
| * <p>Call {@link Window#setNavigationBarColor(int)} to change the navigation bar color.</p> |
| * |
| * <p>Call {@link View#setSystemUiVisibility(int)} with |
| * {@link View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} to optimize the navigation bar for |
| * light color.</p> |
| */ |
| SYSTEM_OWNED_NAV_BAR_LAYOUT, |
| /** |
| * The input view is extended to the bottom Navigation Bar (if present). In this mode the |
| * IME is expected to control Navigation Bar appearance, including button color. |
| * |
| * <p>In this state, the system does not automatically place the input view above the |
| * navigation bar. You need to take care of the inset manually.</p> |
| * |
| * <p>Call {@link View#setSystemUiVisibility(int)} with |
| * {@link View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} to optimize the navigation bar for |
| * light color.</p> |
| |
| * @see View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| * @see View#SYSTEM_UI_FLAG_LAYOUT_STABLE |
| */ |
| IME_OWNED_NAV_BAR_LAYOUT, |
| /** |
| * The input view is floating off of the bottom Navigation Bar region (if present). In this |
| * mode the target application is expected to control Navigation Bar appearance, including |
| * button color. |
| */ |
| FLOATING_LAYOUT, |
| } |
| |
| private final class KeyboardLayoutView extends LinearLayout { |
| |
| private final Window mWindow; |
| private InputViewMode mMode = InputViewMode.SYSTEM_OWNED_NAV_BAR_LAYOUT; |
| |
| private void updateBottomPaddingIfNecessary(int newPaddingBottom) { |
| if (getPaddingBottom() != newPaddingBottom) { |
| setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom); |
| } |
| } |
| |
| @Override |
| public WindowInsets onApplyWindowInsets(WindowInsets insets) { |
| if (insets.isConsumed() |
| || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) { |
| // In this case we are not interested in consuming NavBar region. |
| // Make sure that the bottom padding is empty. |
| updateBottomPaddingIfNecessary(0); |
| return insets; |
| } |
| |
| // In some cases the bottom system window inset is not a navigation bar. Wear devices |
| // that have bottom chin are examples. For now, assume that it's a navigation bar if it |
| // has the same height as the root window's stable bottom inset. |
| final WindowInsets rootWindowInsets = getRootWindowInsets(); |
| if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom() != |
| insets.getSystemWindowInsetBottom())) { |
| // This is probably not a NavBar. |
| updateBottomPaddingIfNecessary(0); |
| return insets; |
| } |
| |
| final int possibleNavBarHeight = insets.getSystemWindowInsetBottom(); |
| updateBottomPaddingIfNecessary(possibleNavBarHeight); |
| return possibleNavBarHeight <= 0 |
| ? insets |
| : insets.replaceSystemWindowInsets( |
| insets.getSystemWindowInsetLeft(), |
| insets.getSystemWindowInsetTop(), |
| insets.getSystemWindowInsetRight(), |
| 0 /* bottom */); |
| } |
| |
| public KeyboardLayoutView(Context context, final Window window) { |
| super(context); |
| mWindow = window; |
| setOrientation(VERTICAL); |
| |
| if (BuildCompat.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.O_MR1) { |
| final TextView textView = new TextView(context); |
| textView.setText("ThemedNavBarKeyboard works only on API 28 and higher devices"); |
| textView.setGravity(Gravity.CENTER); |
| textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); |
| textView.setPadding(20, 10, 20, 20); |
| addView(textView); |
| setBackgroundColor(LIGHT_RED); |
| return; |
| } |
| |
| // By default use "SeparateNavBarMode" mode. |
| switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */); |
| setBackgroundColor(MINT_COLOR); |
| |
| { |
| final LinearLayout subLayout = new LinearLayout(context); |
| { |
| final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( |
| LinearLayout.LayoutParams.MATCH_PARENT, |
| LinearLayout.LayoutParams.WRAP_CONTENT); |
| lp.weight = 50; |
| subLayout.addView(createButton("BACK_DISPOSITION\nDEFAULT", () -> { |
| setBackDisposition(BACK_DISPOSITION_DEFAULT); |
| }), lp); |
| } |
| { |
| final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( |
| LinearLayout.LayoutParams.MATCH_PARENT, |
| LinearLayout.LayoutParams.WRAP_CONTENT); |
| lp.weight = 50; |
| subLayout.addView(createButton("BACK_DISPOSITION\nADJUST_NOTHING", () -> { |
| setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING); |
| }), lp); |
| } |
| addView(subLayout); |
| } |
| |
| addView(createButton("Floating Mode", () -> { |
| switchToFloatingMode(); |
| setBackgroundColor(Color.TRANSPARENT); |
| })); |
| addView(createButton("Extended Dark Navigation Bar", () -> { |
| switchToExtendedNavBarMode(false /* lightNavBar */); |
| final GradientDrawable drawable = new GradientDrawable( |
| GradientDrawable.Orientation.TOP_BOTTOM, |
| new int[] {MINT_COLOR, Color.DKGRAY}); |
| setBackground(drawable); |
| })); |
| addView(createButton("Extended Light Navigation Bar", () -> { |
| switchToExtendedNavBarMode(true /* lightNavBar */); |
| final GradientDrawable drawable = new GradientDrawable( |
| GradientDrawable.Orientation.TOP_BOTTOM, |
| new int[] {MINT_COLOR, Color.WHITE}); |
| setBackground(drawable); |
| })); |
| addView(createButton("Separate Dark Navigation Bar", () -> { |
| switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */); |
| setBackgroundColor(MINT_COLOR); |
| })); |
| addView(createButton("Separate Light Navigation Bar", () -> { |
| switchToSeparateNavBarMode(Color.GRAY, true /* lightNavBar */); |
| setBackgroundColor(MINT_COLOR); |
| })); |
| |
| // Spacer |
| addView(new View(getContext()), 0, 40); |
| } |
| |
| public boolean isFloatingMode() { |
| return mMode == InputViewMode.FLOATING_LAYOUT; |
| } |
| |
| private View createButton(String text, final Runnable onClickCallback) { |
| final Button button = new Button(getContext()); |
| button.setText(text); |
| button.setOnClickListener(view -> onClickCallback.run()); |
| return button; |
| } |
| |
| private void updateSystemUiFlag(int flags) { |
| final int maskFlags = SYSTEM_UI_FLAG_LAYOUT_STABLE |
| | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| | SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; |
| final int visFlags = getSystemUiVisibility(); |
| setSystemUiVisibility((visFlags & ~maskFlags) | (flags & maskFlags)); |
| } |
| |
| /** |
| * Updates the current input view mode to {@link InputViewMode#FLOATING_LAYOUT}. |
| */ |
| private void switchToFloatingMode() { |
| mMode = InputViewMode.FLOATING_LAYOUT; |
| |
| final int prevFlags = mWindow.getAttributes().flags; |
| |
| // This allows us to keep the navigation bar appearance based on the target application, |
| // rather than the IME itself. |
| mWindow.setFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); |
| |
| updateSystemUiFlag(0); |
| |
| // View#onApplyWindowInsets() will not be called if direct or indirect parent View |
| // consumes all the insets. Hence we need to make sure that the bottom padding is |
| // cleared here. |
| updateBottomPaddingIfNecessary(0); |
| |
| // For some reasons, seems that we need to post another requestLayout() when |
| // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is changed. |
| // TODO: Investigate the reason. |
| if ((prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) { |
| post(() -> requestLayout()); |
| } |
| } |
| |
| /** |
| * Updates the current input view mode to {@link InputViewMode#SYSTEM_OWNED_NAV_BAR_LAYOUT}. |
| * |
| * @param navBarColor color to be passed to {@link Window#setNavigationBarColor(int)}. |
| * {@link Color#TRANSPARENT} cannot be used here because it hides the |
| * color view itself. Consider floating mode for that use case. |
| * @param isLightNavBar {@code true} when the navigation bar should be optimized for light |
| * color |
| */ |
| private void switchToSeparateNavBarMode(int navBarColor, boolean isLightNavBar) { |
| mMode = InputViewMode.SYSTEM_OWNED_NAV_BAR_LAYOUT; |
| mWindow.setNavigationBarColor(navBarColor); |
| |
| // This allows us to use setNavigationBarColor() + SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR. |
| mWindow.setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); |
| |
| updateSystemUiFlag(isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0); |
| |
| // View#onApplyWindowInsets() will not be called if direct or indirect parent View |
| // consumes all the insets. Hence we need to make sure that the bottom padding is |
| // cleared here. |
| updateBottomPaddingIfNecessary(0); |
| } |
| |
| /** |
| * Updates the current input view mode to {@link InputViewMode#IME_OWNED_NAV_BAR_LAYOUT}. |
| * |
| * @param isLightNavBar {@code true} when the navigation bar should be optimized for light |
| * color |
| */ |
| private void switchToExtendedNavBarMode(boolean isLightNavBar) { |
| mMode = InputViewMode.IME_OWNED_NAV_BAR_LAYOUT; |
| |
| // This hides the ColorView. |
| mWindow.setNavigationBarColor(Color.TRANSPARENT); |
| |
| // This allows us to use setNavigationBarColor() + SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR. |
| mWindow.setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); |
| |
| updateSystemUiFlag(SYSTEM_UI_FLAG_LAYOUT_STABLE |
| | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| | (isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0)); |
| } |
| } |
| } |