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