387 lines
14 KiB
Java
387 lines
14 KiB
Java
/*
|
|
* 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);
|
|
|
|
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<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);
|
|
}
|
|
}
|
|
}
|