Minimum impl of nav buttons rendered by IME

This is the minimum implementation of rendering the back button and
the IME switcher button in the InputMethodService process.

Note that the feature is still disabled by default behind the flag.
Newly added UI logic should be running only when the feature flag is
enabled.

 Bug 215545985: Enable canImeRenderGesturalNavButtons() by default

Here is the list of known blockers before enabling the flag.

 * Bug 215552600: Subscribe gesture mode change.
 * Bug 215551357: Show IME swicher button only when necessary.
 * Bug 215549533: Support light navigation bar.
 * Bug 215550296: Support floating IME.
 * Bug 215554582: Set "input_method_rounded_corner_content_padding"
                  for each devices.

Also, after enabling the flag by default, we start cleaning up newly
added code under

  android.inputmethodservice.navigationbar.

at Bug 215443343.

Bug: 205803355
Test: Manually done as follows
  1. Build aosp_coral-userdebug and flash it
  2. Switch to gestural navigation mode.
  3. adb root
  4. adb shell setprop \
       persist.sys.ime.can_render_gestural_nav_buttons true
  5. adb reboot
  6. Tap any edit field to show AOSP Keyboard.
  7. Confirm that the back button on the navbar works.
  8. Confirm that the IME switcher button on the navbar works.
Change-Id: I3e7e1f83554444131e2765dc159617bb9e2337c7
This commit is contained in:
Yohei Yukawa
2022-01-20 21:28:34 -08:00
parent 20ba147a11
commit ff7b453ca8
29 changed files with 3746 additions and 0 deletions

View File

@@ -469,6 +469,10 @@ public class InputMethodService extends AbstractInputMethodService {
InputMethodManager mImm;
private InputMethodPrivilegedOperations mPrivOps = new InputMethodPrivilegedOperations();
@NonNull
private final NavigationBarController mNavigationBarController =
new NavigationBarController(this);
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
int mTheme = 0;
@@ -611,6 +615,7 @@ public class InputMethodService extends AbstractInputMethodService {
info.touchableRegion.set(mTmpInsets.touchableRegion);
info.setTouchableInsets(mTmpInsets.touchableInsets);
}
mNavigationBarController.updateTouchableInsets(mTmpInsets, info);
if (mInputFrame != null) {
setImeExclusionRect(mTmpInsets.visibleTopInsets);
@@ -1534,6 +1539,7 @@ public class InputMethodService extends AbstractInputMethodService {
mCandidatesVisibility = getCandidatesHiddenVisibility();
mCandidatesFrame.setVisibility(mCandidatesVisibility);
mInputFrame.setVisibility(View.GONE);
mNavigationBarController.onViewInitialized();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
@@ -1543,6 +1549,7 @@ public class InputMethodService extends AbstractInputMethodService {
mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
mInsetsComputer);
doFinishInput();
mNavigationBarController.onDestroy();
mWindow.dismissForDestroyIfNecessary();
if (mSettingsObserver != null) {
mSettingsObserver.unregister();
@@ -2451,6 +2458,7 @@ public class InputMethodService extends AbstractInputMethodService {
setImeWindowStatus(nextImeWindowStatus, mBackDisposition);
}
mNavigationBarController.onWindowShown();
// compute visibility
onWindowShown();
mWindowVisible = true;
@@ -3656,6 +3664,7 @@ public class InputMethodService extends AbstractInputMethodService {
+ " touchableInsets=" + mTmpInsets.touchableInsets
+ " touchableRegion=" + mTmpInsets.touchableRegion);
p.println(" mSettingsObserver=" + mSettingsObserver);
p.println(" mNavigationBarController=" + mNavigationBarController.toDebugString());
}
private final ImeTracing.ServiceDumper mDumper = new ImeTracing.ServiceDumper() {

View File

@@ -0,0 +1,328 @@
/*
* 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.StatusBarManager;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.Region;
import android.inputmethodservice.navigationbar.NavigationBarFrame;
import android.inputmethodservice.navigationbar.NavigationBarView;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManagerPolicyConstants;
import android.widget.FrameLayout;
import java.util.Objects;
/**
* This class hides details behind {@link InputMethodService#canImeRenderGesturalNavButtons()} from
* {@link InputMethodService}.
*
* <p>All the package-private methods are no-op when
* {@link InputMethodService#canImeRenderGesturalNavButtons()} returns {@code false}.</p>
*/
final class NavigationBarController {
private interface Callback {
default void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
@NonNull ViewTreeObserver.InternalInsetsInfo dest) {
}
default void onViewInitialized() {
}
default void onWindowShown() {
}
default void onDestroy() {
}
default String toDebugString() {
return "No-op implementation";
}
Callback NOOP = new Callback() {
};
}
private final Callback mImpl;
NavigationBarController(@NonNull InputMethodService inputMethodService) {
mImpl = InputMethodService.canImeRenderGesturalNavButtons()
? new Impl(inputMethodService) : Callback.NOOP;
}
void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
@NonNull ViewTreeObserver.InternalInsetsInfo dest) {
mImpl.updateTouchableInsets(originalInsets, dest);
}
void onViewInitialized() {
mImpl.onViewInitialized();
}
void onWindowShown() {
mImpl.onWindowShown();
}
void onDestroy() {
mImpl.onDestroy();
}
String toDebugString() {
return mImpl.toDebugString();
}
private static final class Impl implements Callback {
@NonNull
private final InputMethodService mService;
private boolean mDestroyed = false;
private boolean mRenderGesturalNavButtons;
@Nullable
private NavigationBarFrame mNavigationBarFrame;
@Nullable
Insets mLastInsets;
Impl(@NonNull InputMethodService inputMethodService) {
mService = inputMethodService;
}
@Nullable
private Insets getSystemInsets() {
if (mService.mWindow == null) {
return null;
}
final View decorView = mService.mWindow.getWindow().getDecorView();
if (decorView == null) {
return null;
}
final WindowInsets windowInsets = decorView.getRootWindowInsets();
if (windowInsets == null) {
return null;
}
final Insets stableBarInsets =
windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
return Insets.min(windowInsets.getInsets(WindowInsets.Type.systemBars()
| WindowInsets.Type.displayCutout()), stableBarInsets);
}
private void installNavigationBarFrameIfNecessary() {
if (!mRenderGesturalNavButtons) {
return;
}
final View rawDecorView = mService.mWindow.getWindow().getDecorView();
if (!(rawDecorView instanceof ViewGroup)) {
return;
}
final ViewGroup decorView = (ViewGroup) rawDecorView;
mNavigationBarFrame = decorView.findViewByPredicate(
NavigationBarFrame.class::isInstance);
final Insets systemInsets = getSystemInsets();
if (mNavigationBarFrame == null) {
mNavigationBarFrame = new NavigationBarFrame(mService);
LayoutInflater.from(mService).inflate(
com.android.internal.R.layout.input_method_navigation_bar,
mNavigationBarFrame);
if (systemInsets != null) {
decorView.addView(mNavigationBarFrame, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
systemInsets.bottom, Gravity.BOTTOM));
mLastInsets = systemInsets;
} else {
decorView.addView(mNavigationBarFrame);
}
final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate(
NavigationBarView.class::isInstance);
if (navigationBarView != null) {
// TODO(b/213337792): Support InputMethodService#setBackDisposition().
// TODO(b/213337792): Set NAVIGATION_HINT_IME_SHOWN only when necessary.
final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT
| StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
navigationBarView.setNavigationIconHints(hints);
}
} else {
mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, systemInsets.bottom, Gravity.BOTTOM));
mLastInsets = systemInsets;
}
mNavigationBarFrame.setBackground(null);
}
@Override
public void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
@NonNull ViewTreeObserver.InternalInsetsInfo dest) {
if (!mRenderGesturalNavButtons || mNavigationBarFrame == null
|| mService.isExtractViewShown()) {
return;
}
final Insets systemInsets = getSystemInsets();
if (systemInsets != null) {
final Window window = mService.mWindow.getWindow();
final View decor = window.getDecorView();
Region touchableRegion = null;
final View inputFrame = mService.mInputFrame;
switch (originalInsets.touchableInsets) {
case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME:
if (inputFrame.getVisibility() == View.VISIBLE) {
touchableRegion = new Region(inputFrame.getLeft(),
inputFrame.getTop(), inputFrame.getRight(),
inputFrame.getBottom());
}
break;
case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT:
if (inputFrame.getVisibility() == View.VISIBLE) {
touchableRegion = new Region(inputFrame.getLeft(),
originalInsets.contentTopInsets, inputFrame.getRight(),
inputFrame.getBottom());
}
break;
case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE:
if (inputFrame.getVisibility() == View.VISIBLE) {
touchableRegion = new Region(inputFrame.getLeft(),
originalInsets.visibleTopInsets, inputFrame.getRight(),
inputFrame.getBottom());
}
break;
case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION:
touchableRegion = new Region();
touchableRegion.set(originalInsets.touchableRegion);
break;
}
final Rect navBarRect = new Rect(decor.getLeft(),
decor.getBottom() - systemInsets.bottom,
decor.getRight(), decor.getBottom());
if (touchableRegion == null) {
touchableRegion = new Region(navBarRect);
} else {
touchableRegion.union(navBarRect);
}
dest.touchableRegion.set(touchableRegion);
dest.setTouchableInsets(
ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
// TODO(b/205803355): See if we can use View#OnLayoutChangeListener().
// TODO(b/205803355): See if we can replace DecorView#mNavigationColorViewState.view
boolean zOrderChanged = false;
if (decor instanceof ViewGroup) {
ViewGroup decorGroup = (ViewGroup) decor;
final View navbarBackgroundView = window.getNavigationBarBackgroundView();
zOrderChanged = navbarBackgroundView != null
&& decorGroup.indexOfChild(navbarBackgroundView)
> decorGroup.indexOfChild(mNavigationBarFrame);
}
final boolean insetChanged = !Objects.equals(systemInsets, mLastInsets);
if (zOrderChanged || insetChanged) {
final NavigationBarFrame that = mNavigationBarFrame;
that.post(() -> {
if (!that.isAttachedToWindow()) {
return;
}
final Insets currentSystemInsets = getSystemInsets();
if (!Objects.equals(currentSystemInsets, mLastInsets)) {
that.setLayoutParams(
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
currentSystemInsets.bottom, Gravity.BOTTOM));
mLastInsets = currentSystemInsets;
}
if (decor instanceof ViewGroup) {
ViewGroup decorGroup = (ViewGroup) decor;
final View navbarBackgroundView =
window.getNavigationBarBackgroundView();
if (navbarBackgroundView != null
&& decorGroup.indexOfChild(navbarBackgroundView)
> decorGroup.indexOfChild(that)) {
decorGroup.bringChildToFront(that);
}
}
});
}
}
}
private boolean isGesturalNavigationEnabled() {
final Resources resources = mService.getResources();
if (resources == null) {
return false;
}
return resources.getInteger(com.android.internal.R.integer.config_navBarInteractionMode)
== WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
}
@Override
public void onViewInitialized() {
if (mDestroyed) {
return;
}
mRenderGesturalNavButtons = isGesturalNavigationEnabled();
installNavigationBarFrameIfNecessary();
}
@Override
public void onDestroy() {
mDestroyed = true;
}
@Override
public void onWindowShown() {
if (mDestroyed || !mRenderGesturalNavButtons || mNavigationBarFrame == null) {
return;
}
final Insets systemInsets = getSystemInsets();
if (systemInsets != null) {
if (!Objects.equals(systemInsets, mLastInsets)) {
mNavigationBarFrame.setLayoutParams(new NavigationBarFrame.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
systemInsets.bottom, Gravity.BOTTOM));
mLastInsets = systemInsets;
}
final Window window = mService.mWindow.getWindow();
View rawDecorView = window.getDecorView();
if (rawDecorView instanceof ViewGroup) {
final ViewGroup decor = (ViewGroup) rawDecorView;
final View navbarBackgroundView = window.getNavigationBarBackgroundView();
if (navbarBackgroundView != null
&& decor.indexOfChild(navbarBackgroundView)
> decor.indexOfChild(mNavigationBarFrame)) {
decor.bringChildToFront(mNavigationBarFrame);
}
}
mNavigationBarFrame.setVisibility(View.VISIBLE);
}
}
@Override
public String toDebugString() {
return "{mRenderGesturalNavButtons=" + mRenderGesturalNavButtons + "}";
}
}
}

View File

@@ -0,0 +1,302 @@
/*
* 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 android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import java.util.ArrayList;
/**
* Dispatches common view calls to multiple views. This is used to handle
* multiples of the same nav bar icon appearing.
*/
final class ButtonDispatcher {
private static final int FADE_DURATION_IN = 150;
private static final int FADE_DURATION_OUT = 250;
public static final Interpolator LINEAR = new LinearInterpolator();
private final ArrayList<View> mViews = new ArrayList<>();
private final int mId;
private View.OnClickListener mClickListener;
private View.OnTouchListener mTouchListener;
private View.OnLongClickListener mLongClickListener;
private View.OnHoverListener mOnHoverListener;
private Boolean mLongClickable;
private float mAlpha = 1.0f;
private Float mDarkIntensity;
private int mVisibility = View.VISIBLE;
private Boolean mDelayTouchFeedback;
private KeyButtonDrawable mImageDrawable;
private View mCurrentView;
private ValueAnimator mFadeAnimator;
private AccessibilityDelegate mAccessibilityDelegate;
private final ValueAnimator.AnimatorUpdateListener mAlphaListener = animation ->
setAlpha(
(float) animation.getAnimatedValue(),
false /* animate */,
false /* cancelAnimator */);
private final AnimatorListenerAdapter mFadeListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mFadeAnimator = null;
setVisibility(getAlpha() == 1 ? View.VISIBLE : View.INVISIBLE);
}
};
public ButtonDispatcher(int id) {
mId = id;
}
public void clear() {
mViews.clear();
}
public void addView(View view) {
mViews.add(view);
view.setOnClickListener(mClickListener);
view.setOnTouchListener(mTouchListener);
view.setOnLongClickListener(mLongClickListener);
view.setOnHoverListener(mOnHoverListener);
if (mLongClickable != null) {
view.setLongClickable(mLongClickable);
}
view.setAlpha(mAlpha);
view.setVisibility(mVisibility);
if (mAccessibilityDelegate != null) {
view.setAccessibilityDelegate(mAccessibilityDelegate);
}
if (view instanceof ButtonInterface) {
final ButtonInterface button = (ButtonInterface) view;
if (mDarkIntensity != null) {
button.setDarkIntensity(mDarkIntensity);
}
if (mImageDrawable != null) {
button.setImageDrawable(mImageDrawable);
}
if (mDelayTouchFeedback != null) {
button.setDelayTouchFeedback(mDelayTouchFeedback);
}
}
}
public int getId() {
return mId;
}
public int getVisibility() {
return mVisibility;
}
public boolean isVisible() {
return getVisibility() == View.VISIBLE;
}
public float getAlpha() {
return mAlpha;
}
public KeyButtonDrawable getImageDrawable() {
return mImageDrawable;
}
public void setImageDrawable(KeyButtonDrawable drawable) {
mImageDrawable = drawable;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
if (mViews.get(i) instanceof ButtonInterface) {
((ButtonInterface) mViews.get(i)).setImageDrawable(mImageDrawable);
}
}
if (mImageDrawable != null) {
mImageDrawable.setCallback(mCurrentView);
}
}
public void setVisibility(int visibility) {
if (mVisibility == visibility) return;
if (mFadeAnimator != null) {
mFadeAnimator.cancel();
}
mVisibility = visibility;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setVisibility(mVisibility);
}
}
public void setAlpha(float alpha) {
setAlpha(alpha, false /* animate */);
}
public void setAlpha(float alpha, boolean animate) {
setAlpha(alpha, animate, true /* cancelAnimator */);
}
public void setAlpha(float alpha, boolean animate, long duration) {
setAlpha(alpha, animate, duration, true /* cancelAnimator */);
}
public void setAlpha(float alpha, boolean animate, boolean cancelAnimator) {
setAlpha(
alpha,
animate,
(getAlpha() < alpha) ? FADE_DURATION_IN : FADE_DURATION_OUT,
cancelAnimator);
}
public void setAlpha(float alpha, boolean animate, long duration, boolean cancelAnimator) {
if (mFadeAnimator != null && (cancelAnimator || animate)) {
mFadeAnimator.cancel();
}
if (animate) {
setVisibility(View.VISIBLE);
mFadeAnimator = ValueAnimator.ofFloat(getAlpha(), alpha);
mFadeAnimator.setDuration(duration);
mFadeAnimator.setInterpolator(LINEAR);
mFadeAnimator.addListener(mFadeListener);
mFadeAnimator.addUpdateListener(mAlphaListener);
mFadeAnimator.start();
} else {
// Discretize the alpha updates to prevent too frequent updates when there is a long
// alpha animation
int prevAlpha = (int) (getAlpha() * 255);
int nextAlpha = (int) (alpha * 255);
if (prevAlpha != nextAlpha) {
mAlpha = nextAlpha / 255f;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setAlpha(mAlpha);
}
}
}
}
public void setDarkIntensity(float darkIntensity) {
mDarkIntensity = darkIntensity;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
if (mViews.get(i) instanceof ButtonInterface) {
((ButtonInterface) mViews.get(i)).setDarkIntensity(darkIntensity);
}
}
}
public void setDelayTouchFeedback(boolean delay) {
mDelayTouchFeedback = delay;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
if (mViews.get(i) instanceof ButtonInterface) {
((ButtonInterface) mViews.get(i)).setDelayTouchFeedback(delay);
}
}
}
public void setOnClickListener(View.OnClickListener clickListener) {
mClickListener = clickListener;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setOnClickListener(mClickListener);
}
}
public void setOnTouchListener(View.OnTouchListener touchListener) {
mTouchListener = touchListener;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setOnTouchListener(mTouchListener);
}
}
public void setLongClickable(boolean isLongClickable) {
mLongClickable = isLongClickable;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setLongClickable(mLongClickable);
}
}
public void setOnLongClickListener(View.OnLongClickListener longClickListener) {
mLongClickListener = longClickListener;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setOnLongClickListener(mLongClickListener);
}
}
public void setOnHoverListener(View.OnHoverListener hoverListener) {
mOnHoverListener = hoverListener;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setOnHoverListener(mOnHoverListener);
}
}
public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
mAccessibilityDelegate = delegate;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
mViews.get(i).setAccessibilityDelegate(delegate);
}
}
public void setTranslation(int x, int y, int z) {
final int N = mViews.size();
for (int i = 0; i < N; i++) {
final View view = mViews.get(i);
view.setTranslationX(x);
view.setTranslationY(y);
view.setTranslationZ(z);
}
}
public ArrayList<View> getViews() {
return mViews;
}
public View getCurrentView() {
return mCurrentView;
}
public void setCurrentView(View currentView) {
mCurrentView = currentView.findViewById(mId);
if (mImageDrawable != null) {
mImageDrawable.setCallback(mCurrentView);
}
if (mCurrentView != null) {
mCurrentView.setTranslationX(0);
mCurrentView.setTranslationY(0);
mCurrentView.setTranslationZ(0);
}
}
/**
* Executes when button is detached from window.
*/
public void onDestroy() {
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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 android.annotation.Nullable;
import android.graphics.drawable.Drawable;
interface ButtonInterface {
void setImageDrawable(@Nullable Drawable drawable);
void setDarkIntensity(float intensity);
void setDelayTouchFeedback(boolean shouldDelay);
}

View File

@@ -0,0 +1,203 @@
/*
* 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.NAVIGATION_BAR_DEADZONE_DECAY;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVIGATION_BAR_DEADZONE_HOLD;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVIGATION_BAR_DEADZONE_SIZE;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVIGATION_BAR_DEADZONE_SIZE_MAX;
import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx;
import android.animation.ObjectAnimator;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
/**
* The "dead zone" consumes unintentional taps along the top edge of the navigation bar.
* When users are typing quickly on an IME they may attempt to hit the space bar, overshoot, and
* accidentally hit the home button. The DeadZone expands temporarily after each tap in the UI
* outside the navigation bar (since this is when accidental taps are more likely), then contracts
* back over time (since a later tap might be intended for the top of the bar).
*/
final class DeadZone {
public static final String TAG = "DeadZone";
public static final boolean DEBUG = false;
public static final int HORIZONTAL = 0; // Consume taps along the top edge.
public static final int VERTICAL = 1; // Consume taps along the left edge.
private static final boolean CHATTY = true; // print to logcat when we eat a click
private final NavigationBarView mNavigationBarView;
private boolean mShouldFlash;
private float mFlashFrac = 0f;
private int mSizeMax;
private int mSizeMin;
// Upon activity elsewhere in the UI, the dead zone will hold steady for
// mHold ms, then move back over the course of mDecay ms
private int mHold, mDecay;
private boolean mVertical;
private long mLastPokeTime;
private int mDisplayRotation;
private final Runnable mDebugFlash = new Runnable() {
@Override
public void run() {
ObjectAnimator.ofFloat(DeadZone.this, "flash", 1f, 0f).setDuration(150).start();
}
};
public DeadZone(NavigationBarView view) {
mNavigationBarView = view;
onConfigurationChanged(Surface.ROTATION_0);
}
static float lerp(float a, float b, float f) {
return (b - a) * f + a;
}
private float getSize(long now) {
if (mSizeMax == 0)
return 0;
long dt = (now - mLastPokeTime);
if (dt > mHold + mDecay)
return mSizeMin;
if (dt < mHold)
return mSizeMax;
return (int) lerp(mSizeMax, mSizeMin, (float) (dt - mHold) / mDecay);
}
public void setFlashOnTouchCapture(boolean dbg) {
mShouldFlash = dbg;
mFlashFrac = 0f;
mNavigationBarView.postInvalidate();
}
public void onConfigurationChanged(int rotation) {
mDisplayRotation = rotation;
final Resources res = mNavigationBarView.getResources();
mHold = NAVIGATION_BAR_DEADZONE_HOLD;
mDecay = NAVIGATION_BAR_DEADZONE_DECAY;
mSizeMin = dpToPx(NAVIGATION_BAR_DEADZONE_SIZE, res);
mSizeMax = dpToPx(NAVIGATION_BAR_DEADZONE_SIZE_MAX, res);
mVertical = (res.getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
if (DEBUG) {
Log.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
+ (mVertical ? " vertical" : " horizontal"));
}
setFlashOnTouchCapture(false); // hard-coded from "bool/config_dead_zone_flash"
}
// I made you a touch event...
public boolean onTouchEvent(MotionEvent event) {
if (DEBUG) {
Log.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
}
// Don't consume events for high precision pointing devices. For this purpose a stylus is
// considered low precision (like a finger), so its events may be consumed.
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return false;
}
final int action = event.getAction();
if (action == MotionEvent.ACTION_OUTSIDE) {
poke(event);
return true;
} else if (action == MotionEvent.ACTION_DOWN) {
if (DEBUG) {
Log.v(TAG, this + " ACTION_DOWN: " + event.getX() + "," + event.getY());
}
//TODO(b/205803355): call mNavBarController.touchAutoDim(mDisplayId); here
int size = (int) getSize(event.getEventTime());
// In the vertical orientation consume taps along the left edge.
// In horizontal orientation consume taps along the top edge.
final boolean consumeEvent;
if (mVertical) {
if (mDisplayRotation == Surface.ROTATION_270) {
consumeEvent = event.getX() > mNavigationBarView.getWidth() - size;
} else {
consumeEvent = event.getX() < size;
}
} else {
consumeEvent = event.getY() < size;
}
if (consumeEvent) {
if (CHATTY) {
Log.v(TAG, "consuming errant click: (" + event.getX() + ","
+ event.getY() + ")");
}
if (mShouldFlash) {
mNavigationBarView.post(mDebugFlash);
mNavigationBarView.postInvalidate();
}
return true; // ...but I eated it
}
}
return false;
}
private void poke(MotionEvent event) {
mLastPokeTime = event.getEventTime();
if (DEBUG)
Log.v(TAG, "poked! size=" + getSize(mLastPokeTime));
if (mShouldFlash) mNavigationBarView.postInvalidate();
}
public void setFlash(float f) {
mFlashFrac = f;
mNavigationBarView.postInvalidate();
}
public float getFlash() {
return mFlashFrac;
}
public void onDraw(Canvas can) {
if (!mShouldFlash || mFlashFrac <= 0f) {
return;
}
final int size = (int) getSize(SystemClock.uptimeMillis());
if (mVertical) {
if (mDisplayRotation == Surface.ROTATION_270) {
can.clipRect(can.getWidth() - size, 0, can.getWidth(), can.getHeight());
} else {
can.clipRect(0, 0, size, can.getHeight());
}
} else {
can.clipRect(0, 0, can.getWidth(), size);
}
final float frac = DEBUG ? (mFlashFrac - 0.5f) + 0.5f : mFlashFrac;
can.drawARGB((int) (frac * 0xFF), 0xDD, 0xEE, 0xAA);
if (DEBUG && size > mSizeMin) {
// Very aggressive redrawing here, for debugging only
mNavigationBarView.postInvalidateDelayed(100);
}
}
}

View File

@@ -0,0 +1,483 @@
/*
* 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.NAV_KEY_BUTTON_SHADOW_COLOR;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAV_KEY_BUTTON_SHADOW_OFFSET_X;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAV_KEY_BUTTON_SHADOW_OFFSET_Y;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAV_KEY_BUTTON_SHADOW_RADIUS;
import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx;
import android.animation.ArgbEvaluator;
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlurMaskFilter;
import android.graphics.BlurMaskFilter.Blur;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.view.View;
/**
* Drawable for {@link KeyButtonView}s that supports tinting between two colors, rotation and shows
* a shadow. AnimatedVectorDrawable will only support tinting from intensities but has no support
* for shadows nor rotations.
*/
final class KeyButtonDrawable extends Drawable {
public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_ROTATE =
new FloatProperty<KeyButtonDrawable>("KeyButtonRotation") {
@Override
public void setValue(KeyButtonDrawable drawable, float degree) {
drawable.setRotation(degree);
}
@Override
public Float get(KeyButtonDrawable drawable) {
return drawable.getRotation();
}
};
public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_TRANSLATE_Y =
new FloatProperty<KeyButtonDrawable>("KeyButtonTranslateY") {
@Override
public void setValue(KeyButtonDrawable drawable, float y) {
drawable.setTranslationY(y);
}
@Override
public Float get(KeyButtonDrawable drawable) {
return drawable.getTranslationY();
}
};
private final Paint mIconPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final ShadowDrawableState mState;
private AnimatedVectorDrawable mAnimatedDrawable;
private final Callback mAnimatedDrawableCallback = new Callback() {
@Override
public void invalidateDrawable(@NonNull Drawable who) {
invalidateSelf();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
unscheduleSelf(what);
}
};
public KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor,
boolean horizontalFlip, Color ovalBackgroundColor) {
this(d, new ShadowDrawableState(lightColor, darkColor,
d instanceof AnimatedVectorDrawable, horizontalFlip, ovalBackgroundColor));
}
private KeyButtonDrawable(Drawable d, ShadowDrawableState state) {
mState = state;
if (d != null) {
mState.mBaseHeight = d.getIntrinsicHeight();
mState.mBaseWidth = d.getIntrinsicWidth();
mState.mChangingConfigurations = d.getChangingConfigurations();
mState.mChildState = d.getConstantState();
}
if (canAnimate()) {
mAnimatedDrawable = (AnimatedVectorDrawable) mState.mChildState.newDrawable().mutate();
mAnimatedDrawable.setCallback(mAnimatedDrawableCallback);
setDrawableBounds(mAnimatedDrawable);
}
}
public void setDarkIntensity(float intensity) {
mState.mDarkIntensity = intensity;
final int color = (int) ArgbEvaluator.getInstance()
.evaluate(intensity, mState.mLightColor, mState.mDarkColor);
updateShadowAlpha();
setColorFilter(new PorterDuffColorFilter(color, Mode.SRC_ATOP));
}
public void setRotation(float degrees) {
if (canAnimate()) {
// AnimatedVectorDrawables will not support rotation
return;
}
if (mState.mRotateDegrees != degrees) {
mState.mRotateDegrees = degrees;
invalidateSelf();
}
}
public void setTranslationX(float x) {
setTranslation(x, mState.mTranslationY);
}
public void setTranslationY(float y) {
setTranslation(mState.mTranslationX, y);
}
public void setTranslation(float x, float y) {
if (mState.mTranslationX != x || mState.mTranslationY != y) {
mState.mTranslationX = x;
mState.mTranslationY = y;
invalidateSelf();
}
}
public void setShadowProperties(int x, int y, int size, int color) {
if (canAnimate()) {
// AnimatedVectorDrawables will not support shadows
return;
}
if (mState.mShadowOffsetX != x || mState.mShadowOffsetY != y
|| mState.mShadowSize != size || mState.mShadowColor != color) {
mState.mShadowOffsetX = x;
mState.mShadowOffsetY = y;
mState.mShadowSize = size;
mState.mShadowColor = color;
mShadowPaint.setColorFilter(
new PorterDuffColorFilter(mState.mShadowColor, Mode.SRC_ATOP));
updateShadowAlpha();
invalidateSelf();
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (changed) {
// End any existing animations when the visibility changes
jumpToCurrentState();
}
return changed;
}
@Override
public void jumpToCurrentState() {
super.jumpToCurrentState();
if (mAnimatedDrawable != null) {
mAnimatedDrawable.jumpToCurrentState();
}
}
@Override
public void setAlpha(int alpha) {
mState.mAlpha = alpha;
mIconPaint.setAlpha(alpha);
updateShadowAlpha();
invalidateSelf();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mIconPaint.setColorFilter(colorFilter);
if (mAnimatedDrawable != null) {
if (hasOvalBg()) {
mAnimatedDrawable.setColorFilter(
new PorterDuffColorFilter(mState.mLightColor, PorterDuff.Mode.SRC_IN));
} else {
mAnimatedDrawable.setColorFilter(colorFilter);
}
}
invalidateSelf();
}
public float getDarkIntensity() {
return mState.mDarkIntensity;
}
public float getRotation() {
return mState.mRotateDegrees;
}
public float getTranslationX() {
return mState.mTranslationX;
}
public float getTranslationY() {
return mState.mTranslationY;
}
@Override
public ConstantState getConstantState() {
return mState;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicHeight() {
return mState.mBaseHeight + (mState.mShadowSize + Math.abs(mState.mShadowOffsetY)) * 2;
}
@Override
public int getIntrinsicWidth() {
return mState.mBaseWidth + (mState.mShadowSize + Math.abs(mState.mShadowOffsetX)) * 2;
}
public boolean canAnimate() {
return mState.mSupportsAnimation;
}
public void startAnimation() {
if (mAnimatedDrawable != null) {
mAnimatedDrawable.start();
}
}
public void resetAnimation() {
if (mAnimatedDrawable != null) {
mAnimatedDrawable.reset();
}
}
public void clearAnimationCallbacks() {
if (mAnimatedDrawable != null) {
mAnimatedDrawable.clearAnimationCallbacks();
}
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
if (bounds.isEmpty()) {
return;
}
if (mAnimatedDrawable != null) {
mAnimatedDrawable.draw(canvas);
} else {
// If no cache or previous cached bitmap is hardware/software acceleration does not
// match the current canvas on draw then regenerate
boolean hwBitmapChanged = mState.mIsHardwareBitmap != canvas.isHardwareAccelerated();
if (hwBitmapChanged) {
mState.mIsHardwareBitmap = canvas.isHardwareAccelerated();
}
if (mState.mLastDrawnIcon == null || hwBitmapChanged) {
regenerateBitmapIconCache();
}
canvas.save();
canvas.translate(mState.mTranslationX, mState.mTranslationY);
canvas.rotate(mState.mRotateDegrees, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2);
if (mState.mShadowSize > 0) {
if (mState.mLastDrawnShadow == null || hwBitmapChanged) {
regenerateBitmapShadowCache();
}
// Translate (with rotation offset) before drawing the shadow
final float radians = (float) (mState.mRotateDegrees * Math.PI / 180);
final float shadowOffsetX = (float) (Math.sin(radians) * mState.mShadowOffsetY
+ Math.cos(radians) * mState.mShadowOffsetX) - mState.mTranslationX;
final float shadowOffsetY = (float) (Math.cos(radians) * mState.mShadowOffsetY
- Math.sin(radians) * mState.mShadowOffsetX) - mState.mTranslationY;
canvas.drawBitmap(mState.mLastDrawnShadow, shadowOffsetX, shadowOffsetY,
mShadowPaint);
}
canvas.drawBitmap(mState.mLastDrawnIcon, null, bounds, mIconPaint);
canvas.restore();
}
}
@Override
public boolean canApplyTheme() {
return mState.canApplyTheme();
}
@ColorInt int getDrawableBackgroundColor() {
return mState.mOvalBackgroundColor.toArgb();
}
boolean hasOvalBg() {
return mState.mOvalBackgroundColor != null;
}
private void regenerateBitmapIconCache() {
final int width = getIntrinsicWidth();
final int height = getIntrinsicHeight();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
// Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
final Drawable d = mState.mChildState.newDrawable().mutate();
setDrawableBounds(d);
canvas.save();
if (mState.mHorizontalFlip) {
canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
}
d.draw(canvas);
canvas.restore();
if (mState.mIsHardwareBitmap) {
bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
}
mState.mLastDrawnIcon = bitmap;
}
private void regenerateBitmapShadowCache() {
if (mState.mShadowSize == 0) {
// No shadow
mState.mLastDrawnIcon = null;
return;
}
final int width = getIntrinsicWidth();
final int height = getIntrinsicHeight();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
// Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
final Drawable d = mState.mChildState.newDrawable().mutate();
setDrawableBounds(d);
canvas.save();
if (mState.mHorizontalFlip) {
canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
}
d.draw(canvas);
canvas.restore();
// Draws the shadow from original drawable
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, Blur.NORMAL));
int[] offset = new int[2];
final Bitmap shadow = bitmap.extractAlpha(paint, offset);
paint.setMaskFilter(null);
bitmap.eraseColor(Color.TRANSPARENT);
canvas.drawBitmap(shadow, offset[0], offset[1], paint);
if (mState.mIsHardwareBitmap) {
bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
}
mState.mLastDrawnShadow = bitmap;
}
/**
* Set the alpha of the shadow. As dark intensity increases, drop the alpha of the shadow since
* dark color and shadow should not be visible at the same time.
*/
private void updateShadowAlpha() {
// Update the color from the original color's alpha as the max
int alpha = Color.alpha(mState.mShadowColor);
mShadowPaint.setAlpha(
Math.round(alpha * (mState.mAlpha / 255f) * (1 - mState.mDarkIntensity)));
}
/**
* Prevent shadow clipping by offsetting the drawable bounds by the shadow and its offset
* @param d the drawable to set the bounds
*/
private void setDrawableBounds(Drawable d) {
final int offsetX = mState.mShadowSize + Math.abs(mState.mShadowOffsetX);
final int offsetY = mState.mShadowSize + Math.abs(mState.mShadowOffsetY);
d.setBounds(offsetX, offsetY, getIntrinsicWidth() - offsetX,
getIntrinsicHeight() - offsetY);
}
private static class ShadowDrawableState extends ConstantState {
int mChangingConfigurations;
int mBaseWidth;
int mBaseHeight;
float mRotateDegrees;
float mTranslationX;
float mTranslationY;
int mShadowOffsetX;
int mShadowOffsetY;
int mShadowSize;
int mShadowColor;
float mDarkIntensity;
int mAlpha;
boolean mHorizontalFlip;
boolean mIsHardwareBitmap;
Bitmap mLastDrawnIcon;
Bitmap mLastDrawnShadow;
ConstantState mChildState;
final int mLightColor;
final int mDarkColor;
final boolean mSupportsAnimation;
final Color mOvalBackgroundColor;
public ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor,
boolean animated, boolean horizontalFlip, Color ovalBackgroundColor) {
mLightColor = lightColor;
mDarkColor = darkColor;
mSupportsAnimation = animated;
mAlpha = 255;
mHorizontalFlip = horizontalFlip;
mOvalBackgroundColor = ovalBackgroundColor;
}
@Override
public Drawable newDrawable() {
return new KeyButtonDrawable(null, this);
}
@Override
public int getChangingConfigurations() {
return mChangingConfigurations;
}
@Override
public boolean canApplyTheme() {
return true;
}
}
/**
* Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
* {@link #create(Context, int, boolean, boolean)}.
*/
public static KeyButtonDrawable create(Context context, @ColorInt int lightColor,
@ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow,
Color ovalBackgroundColor) {
final Resources res = context.getResources();
boolean isRtl = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
Drawable d = context.getDrawable(iconResId);
final KeyButtonDrawable drawable = new KeyButtonDrawable(d, lightColor, darkColor,
isRtl && d.isAutoMirrored(), ovalBackgroundColor);
if (hasShadow) {
int offsetX = dpToPx(NAV_KEY_BUTTON_SHADOW_OFFSET_X, res);
int offsetY = dpToPx(NAV_KEY_BUTTON_SHADOW_OFFSET_Y, res);
int radius = dpToPx(NAV_KEY_BUTTON_SHADOW_RADIUS, res);
int color = NAV_KEY_BUTTON_SHADOW_COLOR;
drawable.setShadowProperties(offsetX, offsetY, radius, color);
}
return drawable;
}
}

View File

@@ -0,0 +1,525 @@
/*
* 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 android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.DimenRes;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RecordingCanvas;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Trace;
import android.view.RenderNodeAnimator;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import java.util.ArrayList;
import java.util.HashSet;
final class KeyButtonRipple extends Drawable {
private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
private static final float GLOW_MAX_ALPHA = 0.2f;
private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
private static final int ANIMATION_DURATION_SCALE = 350;
private static final int ANIMATION_DURATION_FADE = 450;
private static final Interpolator ALPHA_OUT_INTERPOLATOR =
new PathInterpolator(0f, 0f, 0.8f, 1f);
@DimenRes
private final int mMaxWidthResource;
private Paint mRipplePaint;
private CanvasProperty<Float> mLeftProp;
private CanvasProperty<Float> mTopProp;
private CanvasProperty<Float> mRightProp;
private CanvasProperty<Float> mBottomProp;
private CanvasProperty<Float> mRxProp;
private CanvasProperty<Float> mRyProp;
private CanvasProperty<Paint> mPaintProp;
private float mGlowAlpha = 0f;
private float mGlowScale = 1f;
private boolean mPressed;
private boolean mVisible;
private boolean mDrawingHardwareGlow;
private int mMaxWidth;
private boolean mLastDark;
private boolean mDark;
private boolean mDelayTouchFeedback;
private final Interpolator mInterpolator = new LogInterpolator();
private boolean mSupportHardware;
private final View mTargetView;
private final Handler mHandler = new Handler();
private final HashSet<Animator> mRunningAnimations = new HashSet<>();
private final ArrayList<Animator> mTmpArray = new ArrayList<>();
private final TraceAnimatorListener mExitHwTraceAnimator =
new TraceAnimatorListener("exitHardware");
private final TraceAnimatorListener mEnterHwTraceAnimator =
new TraceAnimatorListener("enterHardware");
public enum Type {
OVAL,
ROUNDED_RECT
}
private Type mType = Type.ROUNDED_RECT;
public KeyButtonRipple(Context ctx, View targetView, @DimenRes int maxWidthResource) {
mMaxWidthResource = maxWidthResource;
mMaxWidth = ctx.getResources().getDimensionPixelSize(maxWidthResource);
mTargetView = targetView;
}
public void updateResources() {
mMaxWidth = mTargetView.getContext().getResources()
.getDimensionPixelSize(mMaxWidthResource);
invalidateSelf();
}
public void setDarkIntensity(float darkIntensity) {
mDark = darkIntensity >= 0.5f;
}
public void setDelayTouchFeedback(boolean delay) {
mDelayTouchFeedback = delay;
}
public void setType(Type type) {
mType = type;
}
private Paint getRipplePaint() {
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
}
return mRipplePaint;
}
private void drawSoftware(Canvas canvas) {
if (mGlowAlpha > 0f) {
final Paint p = getRipplePaint();
p.setAlpha((int)(mGlowAlpha * 255f));
final float w = getBounds().width();
final float h = getBounds().height();
final boolean horizontal = w > h;
final float diameter = getRippleSize() * mGlowScale;
final float radius = diameter * .5f;
final float cx = w * .5f;
final float cy = h * .5f;
final float rx = horizontal ? radius : cx;
final float ry = horizontal ? cy : radius;
final float corner = horizontal ? cy : cx;
if (mType == Type.ROUNDED_RECT) {
canvas.drawRoundRect(cx - rx, cy - ry, cx + rx, cy + ry, corner, corner, p);
} else {
canvas.save();
canvas.translate(cx, cy);
float r = Math.min(rx, ry);
canvas.drawOval(-r, -r, r, r, p);
canvas.restore();
}
}
}
@Override
public void draw(Canvas canvas) {
mSupportHardware = canvas.isHardwareAccelerated();
if (mSupportHardware) {
drawHardware((RecordingCanvas) canvas);
} else {
drawSoftware(canvas);
}
}
@Override
public void setAlpha(int alpha) {
// Not supported.
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
// Not supported.
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
private boolean isHorizontal() {
return getBounds().width() > getBounds().height();
}
private void drawHardware(RecordingCanvas c) {
if (mDrawingHardwareGlow) {
if (mType == Type.ROUNDED_RECT) {
c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
mPaintProp);
} else {
CanvasProperty<Float> cx = CanvasProperty.createFloat(getBounds().width() / 2);
CanvasProperty<Float> cy = CanvasProperty.createFloat(getBounds().height() / 2);
int d = Math.min(getBounds().width(), getBounds().height());
CanvasProperty<Float> r = CanvasProperty.createFloat(1.0f * d / 2);
c.drawCircle(cx, cy, r, mPaintProp);
}
}
}
/** Gets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
public float getGlowAlpha() {
return mGlowAlpha;
}
/** Sets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
public void setGlowAlpha(float x) {
mGlowAlpha = x;
invalidateSelf();
}
/** Gets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
public float getGlowScale() {
return mGlowScale;
}
/** Sets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
public void setGlowScale(float x) {
mGlowScale = x;
invalidateSelf();
}
private float getMaxGlowAlpha() {
return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
}
@Override
protected boolean onStateChange(int[] state) {
boolean pressed = false;
for (int i = 0; i < state.length; i++) {
if (state[i] == android.R.attr.state_pressed) {
pressed = true;
break;
}
}
if (pressed != mPressed) {
setPressed(pressed);
mPressed = pressed;
return true;
} else {
return false;
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (changed) {
// End any existing animations when the visibility changes
jumpToCurrentState();
}
return changed;
}
@Override
public void jumpToCurrentState() {
endAnimations("jumpToCurrentState", false /* cancel */);
}
@Override
public boolean isStateful() {
return true;
}
@Override
public boolean hasFocusStateSpecified() {
return true;
}
public void setPressed(boolean pressed) {
if (mDark != mLastDark && pressed) {
mRipplePaint = null;
mLastDark = mDark;
}
if (mSupportHardware) {
setPressedHardware(pressed);
} else {
setPressedSoftware(pressed);
}
}
/**
* Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
* is enabled.
*/
public void abortDelayedRipple() {
mHandler.removeCallbacksAndMessages(null);
}
private void endAnimations(String reason, boolean cancel) {
Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
Trace.endSection();
mVisible = false;
mTmpArray.addAll(mRunningAnimations);
int size = mTmpArray.size();
for (int i = 0; i < size; i++) {
Animator a = mTmpArray.get(i);
if (cancel) {
a.cancel();
} else {
a.end();
}
}
mTmpArray.clear();
mRunningAnimations.clear();
mHandler.removeCallbacksAndMessages(null);
}
private void setPressedSoftware(boolean pressed) {
if (pressed) {
if (mDelayTouchFeedback) {
if (mRunningAnimations.isEmpty()) {
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
} else if (mVisible) {
enterSoftware();
}
} else {
enterSoftware();
}
} else {
exitSoftware();
}
}
private void enterSoftware() {
endAnimations("enterSoftware", true /* cancel */);
mVisible = true;
mGlowAlpha = getMaxGlowAlpha();
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
0f, GLOW_MAX_SCALE_FACTOR);
scaleAnimator.setInterpolator(mInterpolator);
scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
scaleAnimator.addListener(mAnimatorListener);
scaleAnimator.start();
mRunningAnimations.add(scaleAnimator);
// With the delay, it could eventually animate the enter animation with no pressed state,
// then immediately show the exit animation. If this is skipped there will be no ripple.
if (mDelayTouchFeedback && !mPressed) {
exitSoftware();
}
}
private void exitSoftware() {
ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
alphaAnimator.setInterpolator(ALPHA_OUT_INTERPOLATOR);
alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
alphaAnimator.addListener(mAnimatorListener);
alphaAnimator.start();
mRunningAnimations.add(alphaAnimator);
}
private void setPressedHardware(boolean pressed) {
if (pressed) {
if (mDelayTouchFeedback) {
if (mRunningAnimations.isEmpty()) {
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
} else if (mVisible) {
enterHardware();
}
} else {
enterHardware();
}
} else {
exitHardware();
}
}
/**
* Sets the left/top property for the round rect to {@code prop} depending on whether we are
* horizontal or vertical mode.
*/
private void setExtendStart(CanvasProperty<Float> prop) {
if (isHorizontal()) {
mLeftProp = prop;
} else {
mTopProp = prop;
}
}
private CanvasProperty<Float> getExtendStart() {
return isHorizontal() ? mLeftProp : mTopProp;
}
/**
* Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
* horizontal or vertical mode.
*/
private void setExtendEnd(CanvasProperty<Float> prop) {
if (isHorizontal()) {
mRightProp = prop;
} else {
mBottomProp = prop;
}
}
private CanvasProperty<Float> getExtendEnd() {
return isHorizontal() ? mRightProp : mBottomProp;
}
private int getExtendSize() {
return isHorizontal() ? getBounds().width() : getBounds().height();
}
private int getRippleSize() {
int size = isHorizontal() ? getBounds().width() : getBounds().height();
return Math.min(size, mMaxWidth);
}
private void enterHardware() {
endAnimations("enterHardware", true /* cancel */);
mVisible = true;
mDrawingHardwareGlow = true;
setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
startAnim.setDuration(ANIMATION_DURATION_SCALE);
startAnim.setInterpolator(mInterpolator);
startAnim.addListener(mAnimatorListener);
startAnim.setTarget(mTargetView);
setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
endAnim.setDuration(ANIMATION_DURATION_SCALE);
endAnim.setInterpolator(mInterpolator);
endAnim.addListener(mAnimatorListener);
endAnim.addListener(mEnterHwTraceAnimator);
endAnim.setTarget(mTargetView);
if (isHorizontal()) {
mTopProp = CanvasProperty.createFloat(0f);
mBottomProp = CanvasProperty.createFloat(getBounds().height());
mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
} else {
mLeftProp = CanvasProperty.createFloat(0f);
mRightProp = CanvasProperty.createFloat(getBounds().width());
mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
}
mGlowScale = GLOW_MAX_SCALE_FACTOR;
mGlowAlpha = getMaxGlowAlpha();
mRipplePaint = getRipplePaint();
mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
mPaintProp = CanvasProperty.createPaint(mRipplePaint);
startAnim.start();
endAnim.start();
mRunningAnimations.add(startAnim);
mRunningAnimations.add(endAnim);
invalidateSelf();
// With the delay, it could eventually animate the enter animation with no pressed state,
// then immediately show the exit animation. If this is skipped there will be no ripple.
if (mDelayTouchFeedback && !mPressed) {
exitHardware();
}
}
private void exitHardware() {
mPaintProp = CanvasProperty.createPaint(getRipplePaint());
final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
RenderNodeAnimator.PAINT_ALPHA, 0);
opacityAnim.setDuration(ANIMATION_DURATION_FADE);
opacityAnim.setInterpolator(ALPHA_OUT_INTERPOLATOR);
opacityAnim.addListener(mAnimatorListener);
opacityAnim.addListener(mExitHwTraceAnimator);
opacityAnim.setTarget(mTargetView);
opacityAnim.start();
mRunningAnimations.add(opacityAnim);
invalidateSelf();
}
private final AnimatorListenerAdapter mAnimatorListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRunningAnimations.remove(animation);
if (mRunningAnimations.isEmpty() && !mPressed) {
mVisible = false;
mDrawingHardwareGlow = false;
invalidateSelf();
}
}
};
private static final class TraceAnimatorListener extends AnimatorListenerAdapter {
private final String mName;
TraceAnimatorListener(String name) {
mName = name;
}
@Override
public void onAnimationStart(Animator animation) {
Trace.beginSection("KeyButtonRipple.start." + mName);
Trace.endSection();
}
@Override
public void onAnimationCancel(Animator animation) {
Trace.beginSection("KeyButtonRipple.cancel." + mName);
Trace.endSection();
}
@Override
public void onAnimationEnd(Animator animation) {
Trace.beginSection("KeyButtonRipple.end." + mName);
Trace.endSection();
}
}
/**
* Interpolator with a smooth log deceleration
*/
private static final class LogInterpolator implements Interpolator {
@Override
public float getInterpolation(float input) {
return 1 - (float) Math.pow(400, -input * 1.4);
}
}
}

View File

@@ -0,0 +1,370 @@
/*
* 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.view.Display.INVALID_DISPLAY;
import static android.view.KeyEvent.KEYCODE_BACK;
import static android.view.KeyEvent.KEYCODE_UNKNOWN;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.InputConnection;
import android.widget.ImageView;
import com.android.internal.annotations.VisibleForTesting;
/**
* @hide
*/
public class KeyButtonView extends ImageView implements ButtonInterface {
private static final String TAG = KeyButtonView.class.getSimpleName();
private final boolean mPlaySounds;
private long mDownTime;
private boolean mTracking;
private int mCode;
private int mTouchDownX;
private int mTouchDownY;
private AudioManager mAudioManager;
private boolean mGestureAborted;
@VisibleForTesting boolean mLongClicked;
private OnClickListener mOnClickListener;
private final KeyButtonRipple mRipple;
private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private float mDarkIntensity;
private boolean mHasOvalBg = false;
private final Runnable mCheckLongPress = new Runnable() {
public void run() {
if (isPressed()) {
// Log.d("KeyButtonView", "longpressed: " + this);
if (isLongClickable()) {
// Just an old-fashioned ImageView
performLongClick();
mLongClicked = true;
} else {
if (mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
}
mLongClicked = true;
}
}
}
};
public KeyButtonView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO(b/205803355): Figure out better place to set this.
switch (getId()) {
case com.android.internal.R.id.input_method_nav_back:
mCode = KEYCODE_BACK;
break;
default:
mCode = KEYCODE_UNKNOWN;
break;
}
mPlaySounds = true;
setClickable(true);
mAudioManager = context.getSystemService(AudioManager.class);
mRipple = new KeyButtonRipple(context, this,
com.android.internal.R.dimen.input_method_nav_key_button_ripple_max_width);
setBackground(mRipple);
setWillNotDraw(false);
forceHasOverlappingRendering(false);
}
@Override
public boolean isClickable() {
return mCode != KEYCODE_UNKNOWN || super.isClickable();
}
public void setCode(int code) {
mCode = code;
}
@Override
public void setOnClickListener(OnClickListener onClickListener) {
super.setOnClickListener(onClickListener);
mOnClickListener = onClickListener;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
if (mCode != KEYCODE_UNKNOWN) {
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null));
if (isLongClickable()) {
info.addAction(
new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null));
}
}
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility != View.VISIBLE) {
jumpDrawablesToCurrentState();
}
}
@Override
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
if (action == ACTION_CLICK && mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis());
sendEvent(KeyEvent.ACTION_UP, mTracking ? KeyEvent.FLAG_TRACKING : 0);
mTracking = false;
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
playSoundEffect(SoundEffectConstants.CLICK);
return true;
} else if (action == ACTION_LONG_CLICK && mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
sendEvent(KeyEvent.ACTION_UP, mTracking ? KeyEvent.FLAG_TRACKING : 0);
mTracking = false;
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
return true;
}
return super.performAccessibilityActionInternal(action, arguments);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final boolean showSwipeUI = false; // mOverviewProxyService.shouldShowSwipeUpUI();
final int action = ev.getAction();
int x, y;
if (action == MotionEvent.ACTION_DOWN) {
mGestureAborted = false;
}
if (mGestureAborted) {
setPressed(false);
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownTime = SystemClock.uptimeMillis();
mLongClicked = false;
setPressed(true);
// Use raw X and Y to detect gestures in case a parent changes the x and y values
mTouchDownX = (int) ev.getRawX();
mTouchDownY = (int) ev.getRawY();
if (mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
} else {
// Provide the same haptic feedback that the system offers for virtual keys.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
if (!showSwipeUI) {
playSoundEffect(SoundEffectConstants.CLICK);
}
removeCallbacks(mCheckLongPress);
postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE:
x = (int)ev.getRawX();
y = (int)ev.getRawY();
float slop = getQuickStepTouchSlopPx(getContext());
if (Math.abs(x - mTouchDownX) > slop || Math.abs(y - mTouchDownY) > slop) {
// When quick step is enabled, prevent animating the ripple triggered by
// setPressed and decide to run it on touch up
setPressed(false);
removeCallbacks(mCheckLongPress);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
if (mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
removeCallbacks(mCheckLongPress);
break;
case MotionEvent.ACTION_UP:
final boolean doIt = isPressed() && !mLongClicked;
setPressed(false);
final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
if (showSwipeUI) {
if (doIt) {
// Apply haptic feedback on touch up since there is none on touch down
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
playSoundEffect(SoundEffectConstants.CLICK);
}
} else if (doHapticFeedback && !mLongClicked) {
// Always send a release ourselves because it doesn't seem to be sent elsewhere
// and it feels weird to sometimes get a release haptic and other times not.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
}
if (mCode != KEYCODE_UNKNOWN) {
if (doIt) {
sendEvent(KeyEvent.ACTION_UP, mTracking ? KeyEvent.FLAG_TRACKING : 0);
mTracking = false;
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
} else {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
} else {
// no key code, just a regular ImageView
if (doIt && mOnClickListener != null) {
mOnClickListener.onClick(this);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
}
removeCallbacks(mCheckLongPress);
break;
}
return true;
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
if (drawable == null) {
return;
}
KeyButtonDrawable keyButtonDrawable = (KeyButtonDrawable) drawable;
keyButtonDrawable.setDarkIntensity(mDarkIntensity);
mHasOvalBg = keyButtonDrawable.hasOvalBg();
if (mHasOvalBg) {
mOvalBgPaint.setColor(keyButtonDrawable.getDrawableBackgroundColor());
}
mRipple.setType(keyButtonDrawable.hasOvalBg() ? KeyButtonRipple.Type.OVAL
: KeyButtonRipple.Type.ROUNDED_RECT);
}
public void playSoundEffect(int soundConstant) {
if (!mPlaySounds) return;
mAudioManager.playSoundEffect(soundConstant);
}
public void sendEvent(int action, int flags) {
sendEvent(action, flags, SystemClock.uptimeMillis());
}
private void sendEvent(int action, int flags, long when) {
if (mCode == KeyEvent.KEYCODE_BACK && flags != KeyEvent.FLAG_LONG_PRESS) {
if (action == MotionEvent.ACTION_UP) {
// TODO(b/205803355): Implement notifyBackAction();
}
}
// TODO(b/205803355): Consolidate this logic to somewhere else.
if (mContext instanceof InputMethodService) {
final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags | KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
InputDevice.SOURCE_KEYBOARD);
int displayId = INVALID_DISPLAY;
// Make KeyEvent work on multi-display environment
if (getDisplay() != null) {
displayId = getDisplay().getDisplayId();
}
if (displayId != INVALID_DISPLAY) {
ev.setDisplayId(displayId);
}
final InputMethodService ims = (InputMethodService) mContext;
final boolean handled;
switch (action) {
case KeyEvent.ACTION_DOWN:
handled = ims.onKeyDown(ev.getKeyCode(), ev);
mTracking = handled && ev.getRepeatCount() == 0 &&
(ev.getFlags() & KeyEvent.FLAG_START_TRACKING) != 0;
break;
case KeyEvent.ACTION_UP:
handled = ims.onKeyUp(ev.getKeyCode(), ev);
break;
default:
handled = false;
break;
}
if (!handled) {
final InputConnection ic = ims.getCurrentInputConnection();
if (ic != null) {
ic.sendKeyEvent(ev);
}
}
}
}
@Override
public void setDarkIntensity(float darkIntensity) {
mDarkIntensity = darkIntensity;
Drawable drawable = getDrawable();
if (drawable != null) {
((KeyButtonDrawable) drawable).setDarkIntensity(darkIntensity);
// Since we reuse the same drawable for multiple views, we need to invalidate the view
// manually.
invalidate();
}
mRipple.setDarkIntensity(darkIntensity);
}
@Override
public void setDelayTouchFeedback(boolean shouldDelay) {
mRipple.setDelayTouchFeedback(shouldDelay);
}
@Override
public void draw(Canvas canvas) {
if (mHasOvalBg) {
int d = Math.min(getWidth(), getHeight());
canvas.drawOval(0, 0, d, d, mOvalBgPaint);
}
super.draw(canvas);
}
/**
* Ratio of quickstep touch slop (when system takes over the touch) to view touch slop
*/
public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3;
/**
* Touch slop for quickstep gesture
*/
private static float getQuickStepTouchSlopPx(Context context) {
return QUICKSTEP_TOUCH_SLOP_RATIO * ViewConfiguration.get(context).getScaledTouchSlop();
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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 android.annotation.ColorInt;
final class NavigationBarConstants {
private NavigationBarConstants() {
// Not intended to be instantiated.
}
// Copied from "navbar_back_button_ime_offset"
// TODO(b/215443343): Handle this in the drawable then remove this constant.
static final float NAVBAR_BACK_BUTTON_IME_OFFSET = 2.0f;
// Copied from "light_mode_icon_color_single_tone" at packages/SettingsLib/res/values/colors.xml
@ColorInt
static final int LIGHT_MODE_ICON_COLOR_SINGLE_TONE = 0xffffffff;
// Copied from "dark_mode_icon_color_single_tone" at packages/SettingsLib/res/values/colors.xml
@ColorInt
static final int DARK_MODE_ICON_COLOR_SINGLE_TONE = 0x99000000;
// Copied from "navigation_bar_deadzone_hold"
static final int NAVIGATION_BAR_DEADZONE_HOLD = 333;
// Copied from "navigation_bar_deadzone_hold"
static final int NAVIGATION_BAR_DEADZONE_DECAY = 333;
// Copied from "navigation_bar_deadzone_size"
static final float NAVIGATION_BAR_DEADZONE_SIZE = 12.0f;
// Copied from "navigation_bar_deadzone_size_max"
static final float NAVIGATION_BAR_DEADZONE_SIZE_MAX = 32.0f;
// Copied from "nav_key_button_shadow_offset_x"
static final float NAV_KEY_BUTTON_SHADOW_OFFSET_X = 0.0f;
// Copied from "nav_key_button_shadow_offset_y"
static final float NAV_KEY_BUTTON_SHADOW_OFFSET_Y = 1.0f;
// Copied from "nav_key_button_shadow_radius"
static final float NAV_KEY_BUTTON_SHADOW_RADIUS = 0.5f;
// Copied from "nav_key_button_shadow_color"
@ColorInt
static final int NAV_KEY_BUTTON_SHADOW_COLOR = 0x30000000;
}

View File

@@ -0,0 +1,62 @@
/*
* 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.view.MotionEvent.ACTION_OUTSIDE;
import android.annotation.AttrRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;
/**
* @hide
*/
public final class NavigationBarFrame extends FrameLayout {
private DeadZone mDeadZone = null;
public NavigationBarFrame(@NonNull Context context) {
super(context);
}
public NavigationBarFrame(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NavigationBarFrame(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setDeadZone(@NonNull DeadZone deadZone) {
mDeadZone = deadZone;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == ACTION_OUTSIDE) {
if (mDeadZone != null) {
return mDeadZone.onTouchEvent(event);
}
}
return super.dispatchTouchEvent(event);
}
}

View File

@@ -0,0 +1,429 @@
/*
* 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.view.ViewGroup.LayoutParams.MATCH_PARENT;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.inputmethodservice.navigationbar.ReverseLinearLayout.ReverseRelativeLayout;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Space;
/**
* @hide
*/
public final class NavigationBarInflaterView extends FrameLayout {
private static final String TAG = "NavBarInflater";
public static final String NAV_BAR_VIEWS = "sysui_nav_bar";
public static final String NAV_BAR_LEFT = "sysui_nav_bar_left";
public static final String NAV_BAR_RIGHT = "sysui_nav_bar_right";
public static final String MENU_IME_ROTATE = "menu_ime";
public static final String BACK = "back";
public static final String HOME = "home";
public static final String RECENT = "recent";
public static final String NAVSPACE = "space";
public static final String CLIPBOARD = "clipboard";
public static final String HOME_HANDLE = "home_handle";
public static final String KEY = "key";
public static final String LEFT = "left";
public static final String RIGHT = "right";
public static final String CONTEXTUAL = "contextual";
public static final String IME_SWITCHER = "ime_switcher";
public static final String GRAVITY_SEPARATOR = ";";
public static final String BUTTON_SEPARATOR = ",";
public static final String SIZE_MOD_START = "[";
public static final String SIZE_MOD_END = "]";
public static final String KEY_CODE_START = "(";
public static final String KEY_IMAGE_DELIM = ":";
public static final String KEY_CODE_END = ")";
private static final String WEIGHT_SUFFIX = "W";
private static final String WEIGHT_CENTERED_SUFFIX = "WC";
private static final String ABSOLUTE_SUFFIX = "A";
private static final String ABSOLUTE_VERTICAL_CENTERED_SUFFIX = "C";
// Copied from "config_navBarLayoutHandle:
private static final String CONFIG_NAV_BAR_LAYOUT_HANDLE =
"back[70AC];home_handle;ime_switcher[70AC]";
protected LayoutInflater mLayoutInflater;
protected LayoutInflater mLandscapeInflater;
protected FrameLayout mHorizontal;
SparseArray<ButtonDispatcher> mButtonDispatchers;
private View mLastPortrait;
private View mLastLandscape;
private boolean mAlternativeOrder;
public NavigationBarInflaterView(Context context, AttributeSet attrs) {
super(context, attrs);
createInflaters();
}
void createInflaters() {
mLayoutInflater = LayoutInflater.from(mContext);
Configuration landscape = new Configuration();
landscape.setTo(mContext.getResources().getConfiguration());
landscape.orientation = Configuration.ORIENTATION_LANDSCAPE;
mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape));
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
inflateChildren();
clearViews();
inflateLayout(getDefaultLayout());
}
private void inflateChildren() {
removeAllViews();
mHorizontal = (FrameLayout) mLayoutInflater.inflate(
com.android.internal.R.layout.input_method_navigation_layout,
this /* root */, false /* attachToRoot */);
addView(mHorizontal);
updateAlternativeOrder();
}
String getDefaultLayout() {
return CONFIG_NAV_BAR_LAYOUT_HANDLE;
}
public void setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDispatchers) {
mButtonDispatchers = buttonDispatchers;
for (int i = 0; i < buttonDispatchers.size(); i++) {
initiallyFill(buttonDispatchers.valueAt(i));
}
}
void updateButtonDispatchersCurrentView() {
if (mButtonDispatchers != null) {
View view = mHorizontal;
for (int i = 0; i < mButtonDispatchers.size(); i++) {
final ButtonDispatcher dispatcher = mButtonDispatchers.valueAt(i);
dispatcher.setCurrentView(view);
}
}
}
void setAlternativeOrder(boolean alternativeOrder) {
if (alternativeOrder != mAlternativeOrder) {
mAlternativeOrder = alternativeOrder;
updateAlternativeOrder();
}
}
private void updateAlternativeOrder() {
updateAlternativeOrder(mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group));
updateAlternativeOrder(mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_center_group));
}
private void updateAlternativeOrder(View v) {
if (v instanceof ReverseLinearLayout) {
((ReverseLinearLayout) v).setAlternativeOrder(mAlternativeOrder);
}
}
private void initiallyFill(
ButtonDispatcher buttonDispatcher) {
addAll(buttonDispatcher, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group));
addAll(buttonDispatcher, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_center_group));
}
private void addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent) {
for (int i = 0; i < parent.getChildCount(); i++) {
// Need to manually search for each id, just in case each group has more than one
// of a single id. It probably mostly a waste of time, but shouldn't take long
// and will only happen once.
if (parent.getChildAt(i).getId() == buttonDispatcher.getId()) {
buttonDispatcher.addView(parent.getChildAt(i));
}
if (parent.getChildAt(i) instanceof ViewGroup) {
addAll(buttonDispatcher, (ViewGroup) parent.getChildAt(i));
}
}
}
protected void inflateLayout(String newLayout) {
if (newLayout == null) {
newLayout = getDefaultLayout();
}
String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
if (sets.length != 3) {
Log.d(TAG, "Invalid layout.");
newLayout = getDefaultLayout();
sets = newLayout.split(GRAVITY_SEPARATOR, 3);
}
String[] start = sets[0].split(BUTTON_SEPARATOR);
String[] center = sets[1].split(BUTTON_SEPARATOR);
String[] end = sets[2].split(BUTTON_SEPARATOR);
// Inflate these in start to end order or accessibility traversal will be messed up.
inflateButtons(start, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group),
false /* landscape */, true /* start */);
inflateButtons(center, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_center_group),
false /* landscape */, false /* start */);
addGravitySpacer(mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group));
inflateButtons(end, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group),
false /* landscape */, false /* start */);
updateButtonDispatchersCurrentView();
}
private void addGravitySpacer(LinearLayout layout) {
layout.addView(new Space(mContext), new LinearLayout.LayoutParams(0, 0, 1));
}
private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,
boolean start) {
for (int i = 0; i < buttons.length; i++) {
inflateButton(buttons[i], parent, landscape, start);
}
}
private ViewGroup.LayoutParams copy(ViewGroup.LayoutParams layoutParams) {
if (layoutParams instanceof LinearLayout.LayoutParams) {
return new LinearLayout.LayoutParams(layoutParams.width, layoutParams.height,
((LinearLayout.LayoutParams) layoutParams).weight);
}
return new LayoutParams(layoutParams.width, layoutParams.height);
}
@Nullable
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
boolean start) {
LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
View v = createView(buttonSpec, parent, inflater);
if (v == null) return null;
v = applySize(v, buttonSpec, landscape, start);
parent.addView(v);
addToDispatchers(v);
View lastView = landscape ? mLastLandscape : mLastPortrait;
View accessibilityView = v;
if (v instanceof ReverseRelativeLayout) {
accessibilityView = ((ReverseRelativeLayout) v).getChildAt(0);
}
if (lastView != null) {
accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
}
if (landscape) {
mLastLandscape = accessibilityView;
} else {
mLastPortrait = accessibilityView;
}
return v;
}
private View applySize(View v, String buttonSpec, boolean landscape, boolean start) {
String sizeStr = extractSize(buttonSpec);
if (sizeStr == null) return v;
if (sizeStr.contains(WEIGHT_SUFFIX) || sizeStr.contains(ABSOLUTE_SUFFIX)) {
// To support gravity, wrap in RelativeLayout and apply gravity to it.
// Children wanting to use gravity must be smaller than the frame.
ReverseRelativeLayout frame = new ReverseRelativeLayout(mContext);
LayoutParams childParams = new LayoutParams(v.getLayoutParams());
// Compute gravity to apply
int gravity = (landscape) ? (start ? Gravity.TOP : Gravity.BOTTOM)
: (start ? Gravity.START : Gravity.END);
if (sizeStr.endsWith(WEIGHT_CENTERED_SUFFIX)) {
gravity = Gravity.CENTER;
} else if (sizeStr.endsWith(ABSOLUTE_VERTICAL_CENTERED_SUFFIX)) {
gravity = Gravity.CENTER_VERTICAL;
}
// Set default gravity, flipped if needed in reversed layouts (270 RTL and 90 LTR)
frame.setDefaultGravity(gravity);
frame.setGravity(gravity); // Apply gravity to root
frame.addView(v, childParams);
if (sizeStr.contains(WEIGHT_SUFFIX)) {
// Use weighting to set the width of the frame
float weight = Float.parseFloat(
sizeStr.substring(0, sizeStr.indexOf(WEIGHT_SUFFIX)));
frame.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, weight));
} else {
int width = (int) convertDpToPx(mContext,
Float.parseFloat(sizeStr.substring(0, sizeStr.indexOf(ABSOLUTE_SUFFIX))));
frame.setLayoutParams(new LinearLayout.LayoutParams(width, MATCH_PARENT));
}
// Ensure ripples can be drawn outside bounds
frame.setClipChildren(false);
frame.setClipToPadding(false);
return frame;
}
float size = Float.parseFloat(sizeStr);
ViewGroup.LayoutParams params = v.getLayoutParams();
params.width = (int) (params.width * size);
return v;
}
View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
View v = null;
String button = extractButton(buttonSpec);
if (LEFT.equals(button)) {
button = extractButton(NAVSPACE);
} else if (RIGHT.equals(button)) {
button = extractButton(MENU_IME_ROTATE);
}
if (HOME.equals(button)) {
//v = inflater.inflate(R.layout.home, parent, false);
} else if (BACK.equals(button)) {
v = inflater.inflate(com.android.internal.R.layout.input_method_nav_back, parent,
false);
} else if (RECENT.equals(button)) {
//v = inflater.inflate(R.layout.recent_apps, parent, false);
} else if (MENU_IME_ROTATE.equals(button)) {
//v = inflater.inflate(R.layout.menu_ime, parent, false);
} else if (NAVSPACE.equals(button)) {
//v = inflater.inflate(R.layout.nav_key_space, parent, false);
} else if (CLIPBOARD.equals(button)) {
//v = inflater.inflate(R.layout.clipboard, parent, false);
} else if (CONTEXTUAL.equals(button)) {
//v = inflater.inflate(R.layout.contextual, parent, false);
} else if (HOME_HANDLE.equals(button)) {
v = inflater.inflate(com.android.internal.R.layout.input_method_nav_home_handle,
parent, false);
} else if (IME_SWITCHER.equals(button)) {
v = inflater.inflate(com.android.internal.R.layout.input_method_nav_ime_switcher,
parent, false);
} else if (button.startsWith(KEY)) {
/*
String uri = extractImage(button);
int code = extractKeycode(button);
v = inflater.inflate(R.layout.custom_key, parent, false);
((KeyButtonView) v).setCode(code);
if (uri != null) {
if (uri.contains(":")) {
((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri));
} else if (uri.contains("/")) {
int index = uri.indexOf('/');
String pkg = uri.substring(0, index);
int id = Integer.parseInt(uri.substring(index + 1));
((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id));
}
}
*/
}
return v;
}
/*
public static String extractImage(String buttonSpec) {
if (!buttonSpec.contains(KEY_IMAGE_DELIM)) {
return null;
}
final int start = buttonSpec.indexOf(KEY_IMAGE_DELIM);
String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_CODE_END));
return subStr;
}
public static int extractKeycode(String buttonSpec) {
if (!buttonSpec.contains(KEY_CODE_START)) {
return 1;
}
final int start = buttonSpec.indexOf(KEY_CODE_START);
String subStr = buttonSpec.substring(start + 1, buttonSpec.indexOf(KEY_IMAGE_DELIM));
return Integer.parseInt(subStr);
}
*/
public static String extractSize(String buttonSpec) {
if (!buttonSpec.contains(SIZE_MOD_START)) {
return null;
}
final int sizeStart = buttonSpec.indexOf(SIZE_MOD_START);
return buttonSpec.substring(sizeStart + 1, buttonSpec.indexOf(SIZE_MOD_END));
}
public static String extractButton(String buttonSpec) {
if (!buttonSpec.contains(SIZE_MOD_START)) {
return buttonSpec;
}
return buttonSpec.substring(0, buttonSpec.indexOf(SIZE_MOD_START));
}
private void addToDispatchers(View v) {
if (mButtonDispatchers != null) {
final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId());
if (indexOfKey >= 0) {
mButtonDispatchers.valueAt(indexOfKey).addView(v);
}
if (v instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup)v;
final int N = viewGroup.getChildCount();
for (int i = 0; i < N; i++) {
addToDispatchers(viewGroup.getChildAt(i));
}
}
}
}
private void clearViews() {
if (mButtonDispatchers != null) {
for (int i = 0; i < mButtonDispatchers.size(); i++) {
mButtonDispatchers.valueAt(i).clear();
}
}
clearAllChildren(mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_buttons));
}
private void clearAllChildren(ViewGroup group) {
for (int i = 0; i < group.getChildCount(); i++) {
((ViewGroup) group.getChildAt(i)).removeAllViews();
}
}
private static float convertDpToPx(Context context, float dp) {
return dp * context.getResources().getDisplayMetrics().density;
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.util.TypedValue.COMPLEX_UNIT_DIP;
import android.content.res.Resources;
import android.util.TypedValue;
final class NavigationBarUtils {
private NavigationBarUtils() {
// Not intended to be instantiated.
}
/**
* A utility method to convert "dp" to "pixel".
*
* <p>TODO(b/215443343): Remove this method by migrating DP values from
* {@link NavigationBarConstants} to resource files.</p>
*
* @param dpValue "dp" value to be converted to "pixel"
* @param res {@link Resources} to be used when dealing with "dp".
* @return the pixels for a given dp value.
*/
static int dpToPx(float dpValue, Resources res) {
return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, res.getDisplayMetrics());
}
}

View File

@@ -0,0 +1,380 @@
/*
* 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 {
final static boolean DEBUG = false;
final static 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 {@param 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();
}
}
public 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);
}
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();
}
public void setDisabledFlags(int disabledFlags) {
if (mDisabledFlags == disabledFlags) return;
mDisabledFlags = disabledFlags;
updateNavButtonIcons();
}
public 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_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);
}
public 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();
}
}
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);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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 android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/**
* TODO(b/215443343): Remove this file, as IME actually doesn't use this.
*
* @hide
*/
public class NavigationHandle extends View implements ButtonInterface {
public NavigationHandle(Context context) {
this(context, null);
}
public NavigationHandle(Context context, AttributeSet attr) {
super(context, attr);
setFocusable(false);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return false;
}
@Override
public void setImageDrawable(Drawable drawable) {
}
@Override
public void setDarkIntensity(float intensity) {
}
@Override
public void setDelayTouchFeedback(boolean shouldDelay) {
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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 android.annotation.Nullable;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import java.util.ArrayList;
/**
* Automatically reverses the order of children as they are added.
* Also reverse the width and height values of layout params
* @hide
*/
public class ReverseLinearLayout extends LinearLayout {
/** If true, the layout is reversed vs. a regular linear layout */
private boolean mIsLayoutReverse;
/** If true, the layout is opposite to it's natural reversity from the layout direction */
private boolean mIsAlternativeOrder;
public ReverseLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
updateOrder();
}
@Override
public void addView(View child) {
reverseParams(child.getLayoutParams(), child, mIsLayoutReverse);
if (mIsLayoutReverse) {
super.addView(child, 0);
} else {
super.addView(child);
}
}
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
reverseParams(params, child, mIsLayoutReverse);
if (mIsLayoutReverse) {
super.addView(child, 0, params);
} else {
super.addView(child, params);
}
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
updateOrder();
}
public void setAlternativeOrder(boolean alternative) {
mIsAlternativeOrder = alternative;
updateOrder();
}
/**
* In landscape, the LinearLayout is not auto mirrored since it is vertical. Therefore we
* have to do it manually
*/
private void updateOrder() {
boolean isLayoutRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
boolean isLayoutReverse = isLayoutRtl ^ mIsAlternativeOrder;
if (mIsLayoutReverse != isLayoutReverse) {
// reversity changed, swap the order of all views.
int childCount = getChildCount();
ArrayList<View> childList = new ArrayList<>(childCount);
for (int i = 0; i < childCount; i++) {
childList.add(getChildAt(i));
}
removeAllViews();
for (int i = childCount - 1; i >= 0; i--) {
final View child = childList.get(i);
super.addView(child);
}
mIsLayoutReverse = isLayoutReverse;
}
}
private static void reverseParams(ViewGroup.LayoutParams params, View child,
boolean isLayoutReverse) {
if (child instanceof Reversible) {
((Reversible) child).reverse(isLayoutReverse);
}
if (child.getPaddingLeft() == child.getPaddingRight()
&& child.getPaddingTop() == child.getPaddingBottom()) {
child.setPadding(child.getPaddingTop(), child.getPaddingLeft(),
child.getPaddingTop(), child.getPaddingLeft());
}
if (params == null) {
return;
}
int width = params.width;
params.width = params.height;
params.height = width;
}
interface Reversible {
void reverse(boolean isLayoutReverse);
}
public static class ReverseRelativeLayout extends RelativeLayout implements Reversible {
public ReverseRelativeLayout(Context context) {
super(context);
}
@Override
public void reverse(boolean isLayoutReverse) {
updateGravity(isLayoutReverse);
reverseGroup(this, isLayoutReverse);
}
private int mDefaultGravity = Gravity.NO_GRAVITY;
public void setDefaultGravity(int gravity) {
mDefaultGravity = gravity;
}
public void updateGravity(boolean isLayoutReverse) {
// Flip gravity if top of bottom is used
if (mDefaultGravity != Gravity.TOP && mDefaultGravity != Gravity.BOTTOM) return;
// Use the default (intended for 270 LTR and 90 RTL) unless layout is otherwise
int gravityToApply = mDefaultGravity;
if (isLayoutReverse) {
gravityToApply = mDefaultGravity == Gravity.TOP ? Gravity.BOTTOM : Gravity.TOP;
}
if (getGravity() != gravityToApply) setGravity(gravityToApply);
}
}
private static void reverseGroup(ViewGroup group, boolean isLayoutReverse) {
for (int i = 0; i < group.getChildCount(); i++) {
final View child = group.getChildAt(i);
reverseParams(child.getLayoutParams(), child, isLayoutReverse);
// Recursively reverse all children
if (child instanceof ViewGroup) {
reverseGroup((ViewGroup) child, isLayoutReverse);
}
}
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:autoMirrored="true"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M16.78,10.03l-3.97,3.97l3.97,3.97l-1.06,1.06l-5.03,-5.03l5.03,-5.03z"
android:fillColor="#FFFFFFFF" />
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,7h2v2h-2V7zM15,7h2v2h-2V7zM3,7h2v2H3V7zM7,7h2v2H7V7zM11,7h2v2h-2V7zM19,11h2v2h-2V11zM15,11h2v2h-2V11zM3,11h2v2H3V11zM7,11h2v2H7V11zM11,11h2v2h-2V11zM7,15h10v2H7V15z"
android:fillColor="#FFFFFFFF"/>
</vector>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<android.inputmethodservice.navigationbar.KeyButtonView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/input_method_nav_back"
android:layout_width="@dimen/input_method_navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
android:scaleType="center"
android:contentDescription="@string/input_method_nav_back_button_desc"
android:paddingStart="@dimen/input_method_navigation_key_padding"
android:paddingEnd="@dimen/input_method_navigation_key_padding"
/>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<android.inputmethodservice.navigationbar.NavigationHandle
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/input_method_nav_home_handle"
android:layout_width="72dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:paddingStart="@dimen/input_method_navigation_key_padding"
android:paddingEnd="@dimen/input_method_navigation_key_padding"
/>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<android.inputmethodservice.navigationbar.KeyButtonView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/input_method_nav_ime_switcher"
android:layout_width="@dimen/input_method_navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/input_method_ime_switch_button_desc"
android:scaleType="center"
android:paddingStart="@dimen/input_method_navigation_key_padding"
android:paddingEnd="@dimen/input_method_navigation_key_padding"
/>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<android.inputmethodservice.navigationbar.NavigationBarView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/input_method_navigation_bar_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
<android.inputmethodservice.navigationbar.NavigationBarInflaterView
android:id="@+id/input_method_nav_inflater"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
</android.inputmethodservice.navigationbar.NavigationBarView>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/input_method_rounded_corner_content_padding"
android:layout_marginEnd="@dimen/input_method_rounded_corner_content_padding"
android:paddingStart="@dimen/input_method_nav_content_padding"
android:paddingEnd="@dimen/input_method_nav_content_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:id="@+id/input_method_nav_horizontal">
<FrameLayout
android:id="@+id/input_method_nav_buttons"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout
android:id="@+id/input_method_nav_ends_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:clipToPadding="false"
android:clipChildren="false" />
<LinearLayout
android:id="@+id/input_method_nav_center_group"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"
android:clipToPadding="false"
android:clipChildren="false" />
</FrameLayout>
</FrameLayout>

View File

@@ -18,4 +18,8 @@
-->
<resources>
<dimen name="chooser_grid_padding">16dp</dimen>
<!-- Copied from SysUI's @dimen/navigation_key_width for the embedded nav bar in the IME -->
<dimen name="input_method_navigation_key_width">80dip</dimen>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<!-- Copied from SysUI's @dimen/nav_content_padding for the embedded nav bar in the IME -->
<dimen name="input_method_nav_content_padding">8dp</dimen>
</resources>

View File

@@ -41,6 +41,11 @@
<!-- Size of lockscreen outerring on unsecure unlock LockScreen -->
<dimen name="keyguard_lockscreen_outerring_diameter">364dp</dimen>
<!-- Copied from SysUI's @dimen/navigation_key_width for the embedded nav bar in the IME -->
<dimen name="input_method_navigation_key_width">128dp</dimen>
<!-- Copied from SysUI's @dimen/navigation_key_padding for the embedded nav bar in the IME -->
<dimen name="input_method_navigation_key_padding">25dp</dimen>
<!-- Height of FaceUnlock view in keyguard -->
<dimen name="face_unlock_height">430dip</dimen>

View File

@@ -24,4 +24,12 @@
the same as @dimen/navigation_bar_height -->
<dimen name="navigation_bar_height_landscape">56dp</dimen>
<!-- Copied from SysUI's @dimen/navigation_key_width for the embedded nav bar in the IME. -->
<dimen name="input_method_navigation_key_width">80dp</dimen>
<!-- Copied from SysUI's @dimen/navigation_key_padding for the embedded nav bar in the IME. -->
<dimen name="input_method_navigation_key_padding">0dp</dimen>
<!-- Copied from SysUI's @dimen/key_button_ripple_max_width for the embedded nav bar in the
IME. -->
<dimen name="input_method_nav_key_button_ripple_max_width">76dp</dimen>
</resources>

View File

@@ -131,6 +131,19 @@
corners. -->
<dimen name="rounded_corner_radius_bottom_adjustment">0px</dimen>
<!-- Copied from SysUI's @dimen/navigation_key_width for the embedded nav bar in the IME. -->
<dimen name="input_method_navigation_key_width">70dp</dimen>
<!-- Copied from SysUI's @dimen/navigation_key_padding for the embedded nav bar in the IME. -->
<dimen name="input_method_navigation_key_padding">0dp</dimen>
<!-- Copied from SysUI's @dimen/nav_content_padding for the embedded nav bar in the IME. -->
<dimen name="input_method_nav_content_padding">0px</dimen>
<!-- Copied from SysUI's @dimen/rounded_corner_content_padding for the embedded nav bar in the
IME. -->
<dimen name="input_method_rounded_corner_content_padding">0px</dimen>
<!-- Copied from SysUI's @dimen/key_button_ripple_max_width for the embedded nav bar in the
IME. -->
<dimen name="input_method_nav_key_button_ripple_max_width">95dp</dimen>
<!-- Width of the window of the divider bar used to resize docked stacks. -->
<dimen name="docked_stack_divider_thickness">48dp</dimen>

View File

@@ -3277,6 +3277,11 @@
<!-- Title for EditText context menu [CHAR LIMIT=20] -->
<string name="editTextMenuTitle">Text actions</string>
<!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="input_method_nav_back_button_desc">Back</string>
<!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="input_method_ime_switch_button_desc">Switch input method</string>
<!-- If the device is getting low on internal storage, a notification is shown to the user. This is the title of that notification. -->
<string name="low_internal_storage_view_title">Storage space running out</string>
<!-- If the device is getting low on internal storage, a notification is shown to the user. This is the message of that notification. -->

View File

@@ -2438,6 +2438,24 @@
<!-- From PinyinIME(!!!) -->
<java-symbol type="string" name="inputMethod" />
<!-- Gestural Nav buttons within InputMethodService -->
<java-symbol type="dimen" name="input_method_nav_key_button_ripple_max_width" />
<java-symbol type="drawable" name="ic_ime_nav_back" />
<java-symbol type="drawable" name="ic_ime_switcher" />
<java-symbol type="id" name="input_method_nav_back" />
<java-symbol type="id" name="input_method_nav_buttons" />
<java-symbol type="id" name="input_method_nav_center_group" />
<java-symbol type="id" name="input_method_nav_ends_group" />
<java-symbol type="id" name="input_method_nav_home_handle" />
<java-symbol type="id" name="input_method_nav_horizontal" />
<java-symbol type="id" name="input_method_nav_ime_switcher" />
<java-symbol type="id" name="input_method_nav_inflater" />
<java-symbol type="layout" name="input_method_navigation_bar" />
<java-symbol type="layout" name="input_method_navigation_layout" />
<java-symbol type="layout" name="input_method_nav_back" />
<java-symbol type="layout" name="input_method_nav_home_handle" />
<java-symbol type="layout" name="input_method_nav_ime_switcher" />
<!-- From Chromium-WebView -->
<java-symbol type="attr" name="actionModeWebSearchDrawable" />
<java-symbol type="string" name="websearch" />