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