| /* |
| * 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<ButtonDispatcher> 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); |
| |
| getImeSwitchButton().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<View> 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<ButtonDispatcher> 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); |
| } |
| } |
| } |