/* * Copyright (C) 2022 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 android.inputmethodservice.navigationbar; import static android.inputmethodservice.navigationbar.NavigationBarConstants.DARK_MODE_ICON_COLOR_SINGLE_TONE; import static android.inputmethodservice.navigationbar.NavigationBarConstants.LIGHT_MODE_ICON_COLOR_SINGLE_TONE; import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVBAR_BACK_BUTTON_IME_OFFSET; import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.DrawableRes; import android.annotation.FloatRange; import android.app.StatusBarManager; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; import java.util.function.Consumer; /** * @hide */ public final class NavigationBarView extends FrameLayout { private static final boolean DEBUG = false; private static final String TAG = "NavBarView"; // Copied from com.android.systemui.animation.Interpolators#FAST_OUT_SLOW_IN private static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); // The current view is always mHorizontal. View mCurrentView = null; private View mHorizontal; private int mCurrentRotation = -1; int mDisabledFlags = 0; int mNavigationIconHints = StatusBarManager.NAVIGATION_HINT_BACK_ALT; private final int mNavBarMode = NAV_BAR_MODE_GESTURAL; private KeyButtonDrawable mBackIcon; private KeyButtonDrawable mImeSwitcherIcon; private Context mLightContext; private final int mLightIconColor; private final int mDarkIconColor; private final android.inputmethodservice.navigationbar.DeadZone mDeadZone; private boolean mDeadZoneConsuming = false; private final SparseArray mButtonDispatchers = new SparseArray<>(); private Configuration mConfiguration; private Configuration mTmpLastConfiguration; private NavigationBarInflaterView mNavigationInflaterView; public NavigationBarView(Context context, AttributeSet attrs) { super(context, attrs); mLightContext = context; mLightIconColor = LIGHT_MODE_ICON_COLOR_SINGLE_TONE; mDarkIconColor = DARK_MODE_ICON_COLOR_SINGLE_TONE; mConfiguration = new Configuration(); mTmpLastConfiguration = new Configuration(); mConfiguration.updateFrom(context.getResources().getConfiguration()); mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_back, new ButtonDispatcher(com.android.internal.R.id.input_method_nav_back)); mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_ime_switcher, new ButtonDispatcher(com.android.internal.R.id.input_method_nav_ime_switcher)); mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_home_handle, new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle)); mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this); getBackButton().setLongClickable(false); final ButtonDispatcher imeSwitchButton = getImeSwitchButton(); imeSwitchButton.setLongClickable(false); imeSwitchButton.setOnClickListener(view -> view.getContext() .getSystemService(InputMethodManager.class).showInputMethodPicker()); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return shouldDeadZoneConsumeTouchEvents(event) || super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { shouldDeadZoneConsumeTouchEvents(event); return super.onTouchEvent(event); } private boolean shouldDeadZoneConsumeTouchEvents(MotionEvent event) { int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { mDeadZoneConsuming = false; } if (mDeadZone.onTouchEvent(event) || mDeadZoneConsuming) { switch (action) { case MotionEvent.ACTION_DOWN: mDeadZoneConsuming = true; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mDeadZoneConsuming = false; break; } return true; } return false; } public View getCurrentView() { return mCurrentView; } /** * Applies {@code consumer} to each of the nav bar views. */ public void forEachView(Consumer consumer) { if (mHorizontal != null) { consumer.accept(mHorizontal); } } public ButtonDispatcher getBackButton() { return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_back); } public ButtonDispatcher getImeSwitchButton() { return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_ime_switcher); } public ButtonDispatcher getHomeHandle() { return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_home_handle); } public SparseArray getButtonDispatchers() { return mButtonDispatchers; } private void reloadNavIcons() { updateIcons(Configuration.EMPTY); } private void updateIcons(Configuration oldConfig) { final boolean orientationChange = oldConfig.orientation != mConfiguration.orientation; final boolean densityChange = oldConfig.densityDpi != mConfiguration.densityDpi; final boolean dirChange = oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection(); if (densityChange || dirChange) { mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher); } if (orientationChange || densityChange || dirChange) { mBackIcon = getBackDrawable(); } } private KeyButtonDrawable getBackDrawable() { KeyButtonDrawable drawable = getDrawable(com.android.internal.R.drawable.ic_ime_nav_back); orientBackButton(drawable); return drawable; } /** * @return whether this nav bar mode is edge to edge */ public static boolean isGesturalMode(int mode) { return mode == NAV_BAR_MODE_GESTURAL; } private void orientBackButton(KeyButtonDrawable drawable) { final boolean useAltBack = (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; float degrees = useAltBack ? (isRtl ? 90 : -90) : 0; if (drawable.getRotation() == degrees) { return; } if (isGesturalMode(mNavBarMode)) { drawable.setRotation(degrees); return; } // Animate the back button's rotation to the new degrees and only in portrait move up the // back button to line up with the other buttons float targetY = useAltBack ? -dpToPx(NAVBAR_BACK_BUTTON_IME_OFFSET, getResources()) : 0; ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable, PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_ROTATE, degrees), PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_TRANSLATE_Y, targetY)); navBarAnimator.setInterpolator(FAST_OUT_SLOW_IN); navBarAnimator.setDuration(200); navBarAnimator.start(); } private KeyButtonDrawable getDrawable(@DrawableRes int icon) { return KeyButtonDrawable.create(mLightContext, mLightIconColor, mDarkIconColor, icon, true /* hasShadow */, null /* ovalBackgroundColor */); } @Override public void setLayoutDirection(int layoutDirection) { reloadNavIcons(); super.setLayoutDirection(layoutDirection); } /** * Updates the navigation icons based on {@code hints}. * * @param hints bit flags defined in {@link StatusBarManager}. */ public void setNavigationIconHints(int hints) { if (hints == mNavigationIconHints) return; final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; final boolean oldBackAlt = (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; if (newBackAlt != oldBackAlt) { //onImeVisibilityChanged(newBackAlt); } if (DEBUG) { android.widget.Toast.makeText(getContext(), "Navigation icon hints = " + hints, 500) .show(); } mNavigationIconHints = hints; updateNavButtonIcons(); } private void updateNavButtonIcons() { // We have to replace or restore the back and home button icons when exiting or entering // carmode, respectively. Recents are not available in CarMode in nav bar so change // to recent icon is not required. KeyButtonDrawable backIcon = mBackIcon; orientBackButton(backIcon); getBackButton().setImageDrawable(backIcon); getImeSwitchButton().setImageDrawable(mImeSwitcherIcon); // Update IME button visibility, a11y and rotate button always overrides the appearance final boolean imeSwitcherVisible = (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0; getImeSwitchButton().setVisibility(imeSwitcherVisible ? View.VISIBLE : View.INVISIBLE); getBackButton().setVisibility(View.VISIBLE); getHomeHandle().setVisibility(View.INVISIBLE); // We used to be reporting the touch regions via notifyActiveTouchRegions() here. // TODO(b/215593010): Consider taking care of this in the Launcher side. } private Display getContextDisplay() { return getContext().getDisplay(); } @Override public void onFinishInflate() { super.onFinishInflate(); mNavigationInflaterView = findViewById(com.android.internal.R.id.input_method_nav_inflater); mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers); updateOrientationViews(); reloadNavIcons(); } @Override protected void onDraw(Canvas canvas) { mDeadZone.onDraw(canvas); super.onDraw(canvas); } private void updateOrientationViews() { mHorizontal = findViewById(com.android.internal.R.id.input_method_nav_horizontal); updateCurrentView(); } private void updateCurrentView() { resetViews(); mCurrentView = mHorizontal; mCurrentView.setVisibility(View.VISIBLE); mCurrentRotation = getContextDisplay().getRotation(); mNavigationInflaterView.setAlternativeOrder(mCurrentRotation == Surface.ROTATION_90); mNavigationInflaterView.updateButtonDispatchersCurrentView(); } private void resetViews() { mHorizontal.setVisibility(View.GONE); } private void reorient() { updateCurrentView(); final android.inputmethodservice.navigationbar.NavigationBarFrame frame = getRootView().findViewByPredicate(view -> view instanceof NavigationBarFrame); frame.setDeadZone(mDeadZone); mDeadZone.onConfigurationChanged(mCurrentRotation); if (DEBUG) { Log.d(TAG, "reorient(): rot=" + mCurrentRotation); } // Resolve layout direction if not resolved since components changing layout direction such // as changing languages will recreate this view and the direction will be resolved later if (!isLayoutDirectionResolved()) { resolveLayoutDirection(); } updateNavButtonIcons(); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mTmpLastConfiguration.updateFrom(mConfiguration); final int changes = mConfiguration.updateFrom(newConfig); updateIcons(mTmpLastConfiguration); if (mTmpLastConfiguration.densityDpi != mConfiguration.densityDpi || mTmpLastConfiguration.getLayoutDirection() != mConfiguration.getLayoutDirection()) { // If car mode or density changes, we need to reset the icons. updateNavButtonIcons(); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); // This needs to happen first as it can changed the enabled state which can affect whether // the back button is visible requestApplyInsets(); reorient(); updateNavButtonIcons(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); for (int i = 0; i < mButtonDispatchers.size(); ++i) { mButtonDispatchers.valueAt(i).onDestroy(); } } /** * Updates the dark intensity. * * @param intensity The intensity of darkness from {@code 0.0f} to {@code 1.0f}. */ public void setDarkIntensity(@FloatRange(from = 0.0f, to = 1.0f) float intensity) { for (int i = 0; i < mButtonDispatchers.size(); ++i) { mButtonDispatchers.valueAt(i).setDarkIntensity(intensity); } } }