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