• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 android.inputmethodservice.navigationbar;
18 
19 import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME;
20 import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE;
21 import static android.inputmethodservice.navigationbar.NavigationBarConstants.DARK_MODE_ICON_COLOR_SINGLE_TONE;
22 import static android.inputmethodservice.navigationbar.NavigationBarConstants.LIGHT_MODE_ICON_COLOR_SINGLE_TONE;
23 import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVBAR_BACK_BUTTON_IME_OFFSET;
24 import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx;
25 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
26 
27 import android.animation.ObjectAnimator;
28 import android.animation.PropertyValuesHolder;
29 import android.annotation.DrawableRes;
30 import android.annotation.FloatRange;
31 import android.annotation.NonNull;
32 import android.app.StatusBarManager;
33 import android.app.StatusBarManager.NavbarFlags;
34 import android.content.Context;
35 import android.content.res.Configuration;
36 import android.graphics.Canvas;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.SparseArray;
40 import android.view.Display;
41 import android.view.MotionEvent;
42 import android.view.Surface;
43 import android.view.View;
44 import android.view.animation.Interpolator;
45 import android.view.animation.PathInterpolator;
46 import android.view.inputmethod.Flags;
47 import android.view.inputmethod.InputMethodManager;
48 import android.widget.FrameLayout;
49 
50 import java.util.function.Consumer;
51 
52 /**
53  * @hide
54  */
55 public final class NavigationBarView extends FrameLayout {
56     private static final boolean DEBUG = false;
57     private static final String TAG = "NavBarView";
58 
59     // Copied from com.android.systemui.animation.Interpolators#FAST_OUT_SLOW_IN
60     private static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
61 
62     // The current view is always mHorizontal.
63     View mCurrentView = null;
64     private View mHorizontal;
65 
66     private int mCurrentRotation = -1;
67 
68     int mDisabledFlags = 0;
69     @NavbarFlags
70     private int mNavbarFlags;
71     private final int mNavBarMode = NAV_BAR_MODE_GESTURAL;
72 
73     private KeyButtonDrawable mBackIcon;
74     private KeyButtonDrawable mImeSwitcherIcon;
75     private Context mLightContext;
76     private final int mLightIconColor;
77     private final int mDarkIconColor;
78 
79     private final android.inputmethodservice.navigationbar.DeadZone mDeadZone;
80     private boolean mDeadZoneConsuming = false;
81 
82     private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>();
83     private Configuration mConfiguration;
84     private Configuration mTmpLastConfiguration;
85 
86     private NavigationBarInflaterView mNavigationInflaterView;
87 
88     /**
89      * Interface definition for callbacks to be invoked when navigation bar buttons are clicked.
90      */
91     public interface ButtonClickListener {
92 
93         /**
94          * Called when the IME switch button is clicked.
95          *
96          * @param v The view that was clicked.
97          */
onImeSwitchButtonClick(View v)98         void onImeSwitchButtonClick(View v);
99 
100         /**
101          * Called when the IME switch button has been clicked and held.
102          *
103          * @param v The view that was clicked and held.
104          *
105          * @return true if the callback consumed the long click, false otherwise.
106          */
onImeSwitchButtonLongClick(View v)107         boolean onImeSwitchButtonLongClick(View v);
108     }
109 
NavigationBarView(Context context, AttributeSet attrs)110     public NavigationBarView(Context context, AttributeSet attrs) {
111         super(context, attrs);
112 
113         mLightContext = context;
114         mLightIconColor = LIGHT_MODE_ICON_COLOR_SINGLE_TONE;
115         mDarkIconColor = DARK_MODE_ICON_COLOR_SINGLE_TONE;
116 
117         mConfiguration = new Configuration();
118         mTmpLastConfiguration = new Configuration();
119         mConfiguration.updateFrom(context.getResources().getConfiguration());
120 
121         mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_back,
122                 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_back));
123         mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_ime_switcher,
124                 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_ime_switcher));
125         mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_home_handle,
126                 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle));
127 
128         mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this);
129     }
130 
131     /**
132      * Prepares the navigation bar buttons to be used and sets the on click listeners.
133      *
134      * @param listener The listener used to handle the clicks on the navigation bar buttons.
135      */
prepareNavButtons(@onNull ButtonClickListener listener)136     public void prepareNavButtons(@NonNull ButtonClickListener listener) {
137         getBackButton().setLongClickable(false);
138 
139         if (Flags.imeSwitcherRevamp()) {
140             final var imeSwitchButton = getImeSwitchButton();
141             imeSwitchButton.setLongClickable(true);
142             imeSwitchButton.setOnClickListener(listener::onImeSwitchButtonClick);
143             imeSwitchButton.setOnLongClickListener(listener::onImeSwitchButtonLongClick);
144         } else {
145             final ButtonDispatcher imeSwitchButton = getImeSwitchButton();
146             imeSwitchButton.setLongClickable(false);
147             imeSwitchButton.setOnClickListener(view -> view.getContext()
148                     .getSystemService(InputMethodManager.class).showInputMethodPicker());
149         }
150     }
151 
152     @Override
onInterceptTouchEvent(MotionEvent event)153     public boolean onInterceptTouchEvent(MotionEvent event) {
154         return shouldDeadZoneConsumeTouchEvents(event) || super.onInterceptTouchEvent(event);
155     }
156 
157     @Override
onTouchEvent(MotionEvent event)158     public boolean onTouchEvent(MotionEvent event) {
159         shouldDeadZoneConsumeTouchEvents(event);
160         return super.onTouchEvent(event);
161     }
162 
shouldDeadZoneConsumeTouchEvents(MotionEvent event)163     private boolean shouldDeadZoneConsumeTouchEvents(MotionEvent event) {
164         int action = event.getActionMasked();
165         if (action == MotionEvent.ACTION_DOWN) {
166             mDeadZoneConsuming = false;
167         }
168         if (mDeadZone.onTouchEvent(event) || mDeadZoneConsuming) {
169             switch (action) {
170                 case MotionEvent.ACTION_DOWN:
171                     mDeadZoneConsuming = true;
172                     break;
173                 case MotionEvent.ACTION_CANCEL:
174                 case MotionEvent.ACTION_UP:
175                     mDeadZoneConsuming = false;
176                     break;
177             }
178             return true;
179         }
180         return false;
181     }
182 
getCurrentView()183     public View getCurrentView() {
184         return mCurrentView;
185     }
186 
187     /**
188      * Applies {@code consumer} to each of the nav bar views.
189      */
forEachView(Consumer<View> consumer)190     public void forEachView(Consumer<View> consumer) {
191         if (mHorizontal != null) {
192             consumer.accept(mHorizontal);
193         }
194     }
195 
getBackButton()196     public ButtonDispatcher getBackButton() {
197         return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_back);
198     }
199 
getImeSwitchButton()200     public ButtonDispatcher getImeSwitchButton() {
201         return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_ime_switcher);
202     }
203 
getHomeHandle()204     public ButtonDispatcher getHomeHandle() {
205         return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_home_handle);
206     }
207 
getButtonDispatchers()208     public SparseArray<ButtonDispatcher> getButtonDispatchers() {
209         return mButtonDispatchers;
210     }
211 
reloadNavIcons()212     private void reloadNavIcons() {
213         updateIcons(Configuration.EMPTY);
214     }
215 
updateIcons(Configuration oldConfig)216     private void updateIcons(Configuration oldConfig) {
217         final boolean orientationChange = oldConfig.orientation != mConfiguration.orientation;
218         final boolean densityChange = oldConfig.densityDpi != mConfiguration.densityDpi;
219         final boolean dirChange =
220                 oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection();
221 
222         if (densityChange || dirChange) {
223             final int switcherResId = Flags.imeSwitcherRevamp()
224                     ? com.android.internal.R.drawable.ic_ime_switcher_new
225                     : com.android.internal.R.drawable.ic_ime_switcher;
226 
227             mImeSwitcherIcon = getDrawable(switcherResId);
228         }
229         if (orientationChange || densityChange || dirChange) {
230             mBackIcon = getBackDrawable();
231         }
232     }
233 
getBackDrawable()234     private KeyButtonDrawable getBackDrawable() {
235         KeyButtonDrawable drawable = getDrawable(com.android.internal.R.drawable.ic_ime_nav_back);
236         orientBackButton(drawable);
237         return drawable;
238     }
239 
240     /**
241      * @return whether this nav bar mode is edge to edge
242      */
isGesturalMode(int mode)243     public static boolean isGesturalMode(int mode) {
244         return mode == NAV_BAR_MODE_GESTURAL;
245     }
246 
orientBackButton(KeyButtonDrawable drawable)247     private void orientBackButton(KeyButtonDrawable drawable) {
248         final boolean isBackDismissIme = (mNavbarFlags & NAVBAR_BACK_DISMISS_IME) != 0;
249         final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
250         float degrees = isBackDismissIme ? (isRtl ? 90 : -90) : 0;
251         if (drawable.getRotation() == degrees) {
252             return;
253         }
254 
255         if (isGesturalMode(mNavBarMode)) {
256             drawable.setRotation(degrees);
257             return;
258         }
259 
260         // Animate the back button's rotation to the new degrees and only in portrait move up the
261         // back button to line up with the other buttons
262         float targetY = isBackDismissIme
263                 ? -dpToPx(NAVBAR_BACK_BUTTON_IME_OFFSET, getResources())
264                 : 0;
265         ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable,
266                 PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_ROTATE, degrees),
267                 PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_TRANSLATE_Y, targetY));
268         navBarAnimator.setInterpolator(FAST_OUT_SLOW_IN);
269         navBarAnimator.setDuration(200);
270         navBarAnimator.start();
271     }
272 
getDrawable(@rawableRes int icon)273     private KeyButtonDrawable getDrawable(@DrawableRes int icon) {
274         return KeyButtonDrawable.create(mLightContext, mLightIconColor, mDarkIconColor, icon,
275                 true /* hasShadow */, null /* ovalBackgroundColor */);
276     }
277 
278     @Override
setLayoutDirection(int layoutDirection)279     public void setLayoutDirection(int layoutDirection) {
280         reloadNavIcons();
281 
282         super.setLayoutDirection(layoutDirection);
283     }
284 
285     /**
286      * Sets the navigation bar state flags.
287      *
288      * @param flags the navigation bar state flags.
289      */
setNavbarFlags(@avbarFlags int flags)290     public void setNavbarFlags(@NavbarFlags int flags) {
291         if (flags == mNavbarFlags) {
292             return;
293         }
294         final boolean backDismissIme = (flags & StatusBarManager.NAVBAR_BACK_DISMISS_IME) != 0;
295         final boolean oldBackDismissIme =
296                 (mNavbarFlags & StatusBarManager.NAVBAR_BACK_DISMISS_IME) != 0;
297         if (backDismissIme != oldBackDismissIme) {
298             //onBackDismissImeChanged(backDismissIme);
299         }
300 
301         if (DEBUG) {
302             android.widget.Toast.makeText(getContext(), "Navbar flags = " + flags, 500)
303                     .show();
304         }
305         mNavbarFlags = flags;
306         updateNavButtonIcons();
307     }
308 
updateNavButtonIcons()309     private void updateNavButtonIcons() {
310         // We have to replace or restore the back and home button icons when exiting or entering
311         // carmode, respectively. Recents are not available in CarMode in nav bar so change
312         // to recent icon is not required.
313         KeyButtonDrawable backIcon = mBackIcon;
314         orientBackButton(backIcon);
315         getBackButton().setImageDrawable(backIcon);
316 
317         getImeSwitchButton().setImageDrawable(mImeSwitcherIcon);
318 
319         // Update IME switcher button visibility, a11y and rotate button always overrides
320         // the appearance.
321         final boolean isImeSwitcherButtonVisible =
322                 (mNavbarFlags & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0;
323         getImeSwitchButton()
324                 .setVisibility(isImeSwitcherButtonVisible ? View.VISIBLE : View.INVISIBLE);
325 
326         getBackButton().setVisibility(View.VISIBLE);
327         getHomeHandle().setVisibility(View.INVISIBLE);
328 
329         // We used to be reporting the touch regions via notifyActiveTouchRegions() here.
330         // TODO(b/215593010): Consider taking care of this in the Launcher side.
331     }
332 
getContextDisplay()333     private Display getContextDisplay() {
334         return getContext().getDisplay();
335     }
336 
337     @Override
onFinishInflate()338     public void onFinishInflate() {
339         super.onFinishInflate();
340         mNavigationInflaterView = findViewById(com.android.internal.R.id.input_method_nav_inflater);
341         mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
342 
343         updateOrientationViews();
344         reloadNavIcons();
345     }
346 
347     @Override
onDraw(Canvas canvas)348     protected void onDraw(Canvas canvas) {
349         mDeadZone.onDraw(canvas);
350         super.onDraw(canvas);
351     }
352 
updateOrientationViews()353     private void updateOrientationViews() {
354         mHorizontal = findViewById(com.android.internal.R.id.input_method_nav_horizontal);
355 
356         updateCurrentView();
357     }
358 
updateCurrentView()359     private void updateCurrentView() {
360         resetViews();
361         mCurrentView = mHorizontal;
362         mCurrentView.setVisibility(View.VISIBLE);
363         mCurrentRotation = getContextDisplay().getRotation();
364         mNavigationInflaterView.setAlternativeOrder(mCurrentRotation == Surface.ROTATION_90);
365         mNavigationInflaterView.updateButtonDispatchersCurrentView();
366     }
367 
resetViews()368     private void resetViews() {
369         mHorizontal.setVisibility(View.GONE);
370     }
371 
reorient()372     private void reorient() {
373         updateCurrentView();
374 
375         final android.inputmethodservice.navigationbar.NavigationBarFrame frame =
376                 getRootView().findViewByPredicate(view -> view instanceof NavigationBarFrame);
377         frame.setDeadZone(mDeadZone);
378         mDeadZone.onConfigurationChanged(mCurrentRotation);
379 
380         if (DEBUG) {
381             Log.d(TAG, "reorient(): rot=" + mCurrentRotation);
382         }
383 
384         // Resolve layout direction if not resolved since components changing layout direction such
385         // as changing languages will recreate this view and the direction will be resolved later
386         if (!isLayoutDirectionResolved()) {
387             resolveLayoutDirection();
388         }
389         updateNavButtonIcons();
390     }
391 
392     @Override
onConfigurationChanged(Configuration newConfig)393     protected void onConfigurationChanged(Configuration newConfig) {
394         super.onConfigurationChanged(newConfig);
395         mTmpLastConfiguration.updateFrom(mConfiguration);
396         final int changes = mConfiguration.updateFrom(newConfig);
397 
398         updateIcons(mTmpLastConfiguration);
399         if (mTmpLastConfiguration.densityDpi != mConfiguration.densityDpi
400                 || mTmpLastConfiguration.getLayoutDirection()
401                         != mConfiguration.getLayoutDirection()) {
402             // If car mode or density changes, we need to reset the icons.
403             updateNavButtonIcons();
404         }
405     }
406 
407     @Override
onAttachedToWindow()408     protected void onAttachedToWindow() {
409         super.onAttachedToWindow();
410         // This needs to happen first as it can changed the enabled state which can affect whether
411         // the back button is visible
412         requestApplyInsets();
413         reorient();
414         updateNavButtonIcons();
415     }
416 
417     @Override
onDetachedFromWindow()418     protected void onDetachedFromWindow() {
419         super.onDetachedFromWindow();
420         for (int i = 0; i < mButtonDispatchers.size(); ++i) {
421             mButtonDispatchers.valueAt(i).onDestroy();
422         }
423     }
424 
425     /**
426      * Updates the dark intensity.
427      *
428      * @param intensity The intensity of darkness from {@code 0.0f} to {@code 1.0f}.
429      */
setDarkIntensity(@loatRangefrom = 0.0f, to = 1.0f) float intensity)430     public void setDarkIntensity(@FloatRange(from = 0.0f, to = 1.0f) float intensity) {
431         for (int i = 0; i < mButtonDispatchers.size(); ++i) {
432             mButtonDispatchers.valueAt(i).setDarkIntensity(intensity);
433         }
434     }
435 }
436