Merge "Minimum impl of nav buttons rendered by IME"
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
203
core/java/android/inputmethodservice/navigationbar/DeadZone.java
Normal file
203
core/java/android/inputmethodservice/navigationbar/DeadZone.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
core/res/res/drawable/ic_ime_nav_back.xml
Normal file
26
core/res/res/drawable/ic_ime_nav_back.xml
Normal 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>
|
||||
25
core/res/res/drawable/ic_ime_switcher.xml
Normal file
25
core/res/res/drawable/ic_ime_switcher.xml
Normal 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>
|
||||
28
core/res/res/layout/input_method_nav_back.xml
Normal file
28
core/res/res/layout/input_method_nav_back.xml
Normal 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"
|
||||
/>
|
||||
25
core/res/res/layout/input_method_nav_home_handle.xml
Normal file
25
core/res/res/layout/input_method_nav_home_handle.xml
Normal 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"
|
||||
/>
|
||||
27
core/res/res/layout/input_method_nav_ime_switcher.xml
Normal file
27
core/res/res/layout/input_method_nav_ime_switcher.xml
Normal 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"
|
||||
/>
|
||||
32
core/res/res/layout/input_method_navigation_bar.xml
Normal file
32
core/res/res/layout/input_method_navigation_bar.xml
Normal 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>
|
||||
57
core/res/res/layout/input_method_navigation_layout.xml
Normal file
57
core/res/res/layout/input_method_navigation_layout.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
20
core/res/res/values-sw372dp/dimens.xml
Normal file
20
core/res/res/values-sw372dp/dimens.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -2439,6 +2439,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" />
|
||||
|
||||
Reference in New Issue
Block a user