Merge changes Idd0d85f7,I53ea56b8

* changes:
  Refactor the design and improve the animations of Accessibility Floating Menu(11/n).
  Refactor the design and improve the animations of Accessibility Floating Menu(10/n).
This commit is contained in:
PETER LIANG
2022-12-10 02:00:24 +00:00
committed by Android (Google) Code Review
10 changed files with 730 additions and 16 deletions

View File

@@ -25,6 +25,9 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FlingAnimation;
@@ -33,6 +36,7 @@ import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.util.HashMap;
@@ -55,7 +59,11 @@ class MenuAnimationController {
private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
private static final float SPRING_STIFFNESS = 700f;
private static final float ESCAPE_VELOCITY = 750f;
// Make tucked animation by using translation X relative to the view itself.
private static final float ANIMATION_TO_X_VALUE = 0.5f;
private static final int ANIMATION_START_OFFSET_MS = 600;
private static final int ANIMATION_DURATION_MS = 600;
private static final int FADE_OUT_DURATION_MS = 1000;
private static final int FADE_EFFECT_DURATION_MS = 3000;
@@ -64,10 +72,12 @@ class MenuAnimationController {
private final Handler mHandler;
private boolean mIsFadeEffectEnabled;
private DismissAnimationController.DismissCallback mDismissCallback;
private Runnable mSpringAnimationsEndAction;
// Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
// DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler
private final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
@VisibleForTesting
final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
new HashMap<>();
MenuAnimationController(MenuView menuView) {
@@ -102,6 +112,13 @@ class MenuAnimationController {
}
}
/**
* Sets the action to be called when the all dynamic animations are completed.
*/
void setSpringAnimationsEndAction(Runnable runnable) {
mSpringAnimationsEndAction = runnable;
}
void setDismissCallback(
DismissAnimationController.DismissCallback dismissCallback) {
mDismissCallback = dismissCallback;
@@ -192,7 +209,7 @@ class MenuAnimationController {
? bounds.right
: bounds.bottom;
final FlingAnimation flingAnimation = new FlingAnimation(mMenuView, menuPositionProperty);
final FlingAnimation flingAnimation = createFlingAnimation(mMenuView, menuPositionProperty);
flingAnimation.setFriction(friction)
.setStartVelocity(velocity)
.setMinValue(Math.min(currentValue, min))
@@ -217,7 +234,14 @@ class MenuAnimationController {
flingAnimation.start();
}
private void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
@VisibleForTesting
FlingAnimation createFlingAnimation(MenuView menuView,
MenuPositionProperty menuPositionProperty) {
return new FlingAnimation(menuView, menuPositionProperty);
}
@VisibleForTesting
void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
float velocity, float finalPosition) {
final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
final SpringAnimation springAnimation =
@@ -228,8 +252,13 @@ class MenuAnimationController {
return;
}
onSpringAnimationEnd(new PointF(mMenuView.getTranslationX(),
mMenuView.getTranslationY()));
final boolean areAnimationsRunning =
mPositionAnimations.values().stream().anyMatch(
DynamicAnimation::isRunning);
if (!areAnimationsRunning) {
onSpringAnimationsEnd(new PointF(mMenuView.getTranslationX(),
mMenuView.getTranslationY()));
}
})
.setStartVelocity(velocity);
@@ -332,11 +361,15 @@ class MenuAnimationController {
.start();
}
private void onSpringAnimationEnd(PointF position) {
private void onSpringAnimationsEnd(PointF position) {
mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
constrainPositionAndUpdate(position);
fadeOutIfEnabled();
if (mSpringAnimationsEndAction != null) {
mSpringAnimationsEndAction.run();
}
}
private void constrainPositionAndUpdate(PointF position) {
@@ -387,6 +420,26 @@ class MenuAnimationController {
mHandler.removeCallbacksAndMessages(/* token= */ null);
}
void startTuckedAnimationPreview() {
fadeInNowIfEnabled();
final float toXValue = isOnLeftSide()
? -ANIMATION_TO_X_VALUE
: ANIMATION_TO_X_VALUE;
final TranslateAnimation animation =
new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, toXValue,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0);
animation.setDuration(ANIMATION_DURATION_MS);
animation.setRepeatMode(Animation.REVERSE);
animation.setInterpolator(new OvershootInterpolator());
animation.setRepeatCount(Animation.INFINITE);
animation.setStartOffset(ANIMATION_START_OFFSET_MS);
mMenuView.startAnimation(animation);
}
private Handler createUiHandler() {
return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
}

View File

@@ -0,0 +1,222 @@
/*
* 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 com.android.systemui.accessibility.floatingmenu;
import static android.util.TypedValue.COMPLEX_UNIT_PX;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.UNSPECIFIED;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.recents.TriangleShape;
/**
* The tooltip view shows the information about the operation of the anchor view {@link MenuView}
* . It's just shown on the left or right of the anchor view.
*/
@SuppressLint("ViewConstructor")
class MenuEduTooltipView extends FrameLayout {
private int mFontSize;
private int mTextViewMargin;
private int mTextViewPadding;
private int mTextViewCornerRadius;
private int mArrowMargin;
private int mArrowWidth;
private int mArrowHeight;
private int mArrowCornerRadius;
private int mColorAccentPrimary;
private View mArrowLeftView;
private View mArrowRightView;
private TextView mMessageView;
private final MenuViewAppearance mMenuViewAppearance;
MenuEduTooltipView(@NonNull Context context, MenuViewAppearance menuViewAppearance) {
super(context);
mMenuViewAppearance = menuViewAppearance;
updateResources();
initViews();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateResources();
updateMessageView();
updateArrowView();
updateLocationAndVisibility();
}
void show(CharSequence message) {
mMessageView.setText(message);
updateLocationAndVisibility();
}
void updateLocationAndVisibility() {
final boolean isTooltipOnRightOfAnchor = mMenuViewAppearance.isMenuOnLeftSide();
updateArrowVisibilityWith(isTooltipOnRightOfAnchor);
updateLocationWith(getMenuBoundsInParent(), isTooltipOnRightOfAnchor);
}
/**
* Gets the bounds of the {@link MenuView}. Besides, its parent view {@link MenuViewLayer} is
* also the root view of the tooltip view.
*
* @return The menu bounds based on its parent view.
*/
private Rect getMenuBoundsInParent() {
final Rect bounds = new Rect();
final PointF position = mMenuViewAppearance.getMenuPosition();
bounds.set((int) position.x, (int) position.y,
(int) position.x + mMenuViewAppearance.getMenuWidth(),
(int) position.y + mMenuViewAppearance.getMenuHeight());
return bounds;
}
private void updateResources() {
final Resources res = getResources();
mArrowWidth =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width);
mArrowHeight =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_height);
mArrowMargin =
res.getDimensionPixelSize(
R.dimen.accessibility_floating_tooltip_arrow_margin);
mArrowCornerRadius =
res.getDimensionPixelSize(
R.dimen.accessibility_floating_tooltip_arrow_corner_radius);
mFontSize =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_font_size);
mTextViewMargin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_margin);
mTextViewPadding =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_padding);
mTextViewCornerRadius =
res.getDimensionPixelSize(
R.dimen.accessibility_floating_tooltip_text_corner_radius);
mColorAccentPrimary = Utils.getColorAttrDefaultColor(getContext(),
com.android.internal.R.attr.colorAccentPrimary);
}
private void updateLocationWith(Rect anchorBoundsInParent, boolean isTooltipOnRightOfAnchor) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
getAvailableTextViewWidth(isTooltipOnRightOfAnchor), AT_MOST);
final int heightSpec = MeasureSpec.makeMeasureSpec(/* size= */ 0, UNSPECIFIED);
mMessageView.measure(widthSpec, heightSpec);
final LinearLayout.LayoutParams textViewParams =
(LinearLayout.LayoutParams) mMessageView.getLayoutParams();
textViewParams.width = mMessageView.getMeasuredWidth();
mMessageView.setLayoutParams(textViewParams);
final int layoutWidth = mMessageView.getMeasuredWidth() + mArrowWidth + mArrowMargin;
setTranslationX(isTooltipOnRightOfAnchor
? anchorBoundsInParent.right
: anchorBoundsInParent.left - layoutWidth);
setTranslationY(anchorBoundsInParent.centerY() - (mMessageView.getMeasuredHeight() / 2.0f));
}
private void updateMessageView() {
mMessageView.setTextSize(COMPLEX_UNIT_PX, mFontSize);
mMessageView.setPadding(mTextViewPadding, mTextViewPadding, mTextViewPadding,
mTextViewPadding);
final GradientDrawable gradientDrawable = (GradientDrawable) mMessageView.getBackground();
gradientDrawable.setCornerRadius(mTextViewCornerRadius);
gradientDrawable.setColor(mColorAccentPrimary);
}
private void updateArrowView() {
drawArrow(mArrowLeftView, /* isPointingLeft= */ true);
drawArrow(mArrowRightView, /* isPointingLeft= */ false);
}
private void updateArrowVisibilityWith(boolean isTooltipOnRightOfAnchor) {
if (isTooltipOnRightOfAnchor) {
mArrowLeftView.setVisibility(VISIBLE);
mArrowRightView.setVisibility(GONE);
} else {
mArrowLeftView.setVisibility(GONE);
mArrowRightView.setVisibility(VISIBLE);
}
}
private void drawArrow(View arrowView, boolean isPointingLeft) {
final TriangleShape triangleShape =
TriangleShape.createHorizontal(mArrowWidth, mArrowHeight, isPointingLeft);
final ShapeDrawable arrowDrawable = new ShapeDrawable(triangleShape);
final Paint arrowPaint = arrowDrawable.getPaint();
arrowPaint.setColor(mColorAccentPrimary);
final CornerPathEffect effect = new CornerPathEffect(mArrowCornerRadius);
arrowPaint.setPathEffect(effect);
arrowView.setBackground(arrowDrawable);
}
private void initViews() {
final View contentView = LayoutInflater.from(getContext()).inflate(
R.layout.accessibility_floating_menu_tooltip, /* root= */ this, /* attachToRoot= */
false);
mMessageView = contentView.findViewById(R.id.text);
mMessageView.setMovementMethod(LinkMovementMethod.getInstance());
mArrowLeftView = contentView.findViewById(R.id.arrow_left);
mArrowRightView = contentView.findViewById(R.id.arrow_right);
updateMessageView();
updateArrowView();
addView(contentView);
}
private int getAvailableTextViewWidth(boolean isOnRightOfAnchor) {
final PointF position = mMenuViewAppearance.getMenuPosition();
final int availableWidth = isOnRightOfAnchor
? mMenuViewAppearance.getMenuDraggableBounds().width() - (int) position.x
: (int) position.x;
return availableWidth - mArrowWidth - mArrowMargin - mTextViewMargin;
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.accessibility.floatingmenu;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
@@ -28,6 +29,7 @@ import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfo
import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
@@ -40,6 +42,8 @@ import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Prefs;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -52,12 +56,24 @@ class MenuInfoRepository {
@FloatRange(from = 0.0, to = 1.0)
private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.77f;
private static final boolean DEFAULT_MOVE_TO_TUCKED_VALUE = false;
private static final boolean DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE = false;
private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED;
private final Context mContext;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final OnSettingsContentsChanged mSettingsContentsCallback;
private Position mPercentagePosition;
@IntDef({
MigrationPrompt.DISABLED,
MigrationPrompt.ENABLED,
})
@Retention(RetentionPolicy.SOURCE)
@interface MigrationPrompt {
int DISABLED = 0;
int ENABLED = 1;
}
private final ContentObserver mMenuTargetFeaturesContentObserver =
new ContentObserver(mHandler) {
@Override
@@ -99,6 +115,19 @@ class MenuInfoRepository {
DEFAULT_MOVE_TO_TUCKED_VALUE));
}
void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) {
callback.onReady(Prefs.getBoolean(mContext,
Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE));
}
void loadMigrationTooltipVisibility(OnInfoReady<Boolean> callback) {
callback.onReady(Settings.Secure.getIntForUser(mContext.getContentResolver(),
ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT, UserHandle.USER_CURRENT)
== MigrationPrompt.ENABLED);
}
void loadMenuPosition(OnInfoReady<Position> callback) {
callback.onReady(mPercentagePosition);
}
@@ -131,6 +160,18 @@ class MenuInfoRepository {
percentagePosition.toString());
}
void updateDockTooltipVisibility(boolean hasSeen) {
Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
hasSeen);
}
void updateMigrationTooltipVisibility(boolean visible) {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
visible ? MigrationPrompt.ENABLED : MigrationPrompt.DISABLED,
UserHandle.USER_CURRENT);
}
private Position getStartPosition() {
final String absolutePositionString = Prefs.getString(mContext,
Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);

View File

@@ -25,6 +25,8 @@ import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Optional;
/**
* Controls the all touch events of the accessibility target features view{@link RecyclerView} in
* the {@link MenuView}. And then compute the gestures' velocity for fling and spring
@@ -39,6 +41,7 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
private boolean mIsDragging = false;
private float mTouchSlop;
private final DismissAnimationController mDismissAnimationController;
private Optional<Runnable> mOnActionDownEnd = Optional.empty();
MenuListViewTouchHandler(MenuAnimationController menuAnimationController,
DismissAnimationController dismissAnimationController) {
@@ -65,6 +68,8 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
mMenuAnimationController.cancelAnimations();
mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent);
mOnActionDownEnd.ifPresent(Runnable::run);
break;
case MotionEvent.ACTION_MOVE:
if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) {
@@ -125,6 +130,10 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
// Do nothing
}
void setOnActionDownEndListener(Runnable onActionDownEndListener) {
mOnActionDownEnd = Optional.ofNullable(onActionDownEndListener);
}
/**
* Adds a movement to the velocity tracker using raw screen coordinates.
*/

View File

@@ -293,7 +293,7 @@ class MenuViewAppearance {
return bounds;
}
private boolean isMenuOnLeftSide() {
boolean isMenuOnLeftSide() {
return mPercentagePosition.getPercentageX() < 0.5f;
}

View File

@@ -20,6 +20,7 @@ import static android.view.WindowInsets.Type.ime;
import static androidx.core.view.WindowInsetsCompat.Type;
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_BUTTON_COMPONENT_NAME;
import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE;
import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
@@ -27,8 +28,10 @@ import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.In
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.IntDef;
import android.annotation.StringDef;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Handler;
@@ -37,6 +40,8 @@ import android.os.UserHandle;
import android.provider.Settings;
import android.util.PluralsMessageFormatter;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
@@ -45,6 +50,7 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.Observer;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
@@ -58,6 +64,7 @@ import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* The basic interactions with the child views {@link MenuView}, {@link DismissView}, and
@@ -66,11 +73,13 @@ import java.util.Map;
* message view would be shown and allowed users to undo it.
*/
@SuppressLint("ViewConstructor")
class MenuViewLayer extends FrameLayout {
class MenuViewLayer extends FrameLayout implements
ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener {
private static final int SHOW_MESSAGE_DELAY_MS = 3000;
private final WindowManager mWindowManager;
private final MenuView mMenuView;
private final MenuListViewTouchHandler mMenuListViewTouchHandler;
private final MenuMessageView mMessageView;
private final DismissView mDismissView;
private final MenuViewAppearance mMenuViewAppearance;
@@ -79,18 +88,38 @@ class MenuViewLayer extends FrameLayout {
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final IAccessibilityFloatingMenu mFloatingMenu;
private final DismissAnimationController mDismissAnimationController;
private final MenuViewModel mMenuViewModel;
private final Observer<Boolean> mDockTooltipObserver =
this::onDockTooltipVisibilityChanged;
private final Observer<Boolean> mMigrationTooltipObserver =
this::onMigrationTooltipVisibilityChanged;
private final Rect mImeInsetsRect = new Rect();
private boolean mIsMigrationTooltipShowing;
private boolean mShouldShowDockTooltip;
private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty();
@IntDef({
LayerIndex.MENU_VIEW,
LayerIndex.DISMISS_VIEW,
LayerIndex.MESSAGE_VIEW,
LayerIndex.TOOLTIP_VIEW,
})
@Retention(RetentionPolicy.SOURCE)
@interface LayerIndex {
int MENU_VIEW = 0;
int DISMISS_VIEW = 1;
int MESSAGE_VIEW = 2;
int TOOLTIP_VIEW = 3;
}
@StringDef({
TooltipType.MIGRATION,
TooltipType.DOCK,
})
@Retention(RetentionPolicy.SOURCE)
@interface TooltipType {
String MIGRATION = "migration";
String DOCK = "dock";
}
@VisibleForTesting
@@ -125,12 +154,12 @@ class MenuViewLayer extends FrameLayout {
mAccessibilityManager = accessibilityManager;
mFloatingMenu = floatingMenu;
final MenuViewModel menuViewModel = new MenuViewModel(context);
mMenuViewModel = new MenuViewModel(context);
mMenuViewAppearance = new MenuViewAppearance(context, windowManager);
mMenuView = new MenuView(context, menuViewModel, mMenuViewAppearance);
mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance);
mMenuAnimationController = mMenuView.getMenuAnimationController();
mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
mDismissView = new DismissView(context);
mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
@@ -153,9 +182,9 @@ class MenuViewLayer extends FrameLayout {
}
});
final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler(
mMenuAnimationController, mDismissAnimationController);
mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler);
mMenuListViewTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
mDismissAnimationController);
mMenuView.addOnItemTouchListenerToList(mMenuListViewTouchHandler);
mMessageView = new MenuMessageView(context);
@@ -210,7 +239,12 @@ class MenuViewLayer extends FrameLayout {
super.onAttachedToWindow();
mMenuView.show();
setOnClickListener(this);
setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
mMenuViewModel.getDockTooltipVisibilityData().observeForever(mDockTooltipObserver);
mMenuViewModel.getMigrationTooltipVisibilityData().observeForever(
mMigrationTooltipObserver);
mMessageView.setUndoListener(view -> undo());
mContext.registerComponentCallbacks(mDismissAnimationController);
}
@@ -220,11 +254,32 @@ class MenuViewLayer extends FrameLayout {
super.onDetachedFromWindow();
mMenuView.hide();
setOnClickListener(null);
setOnApplyWindowInsetsListener(null);
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
mMenuViewModel.getDockTooltipVisibilityData().removeObserver(mDockTooltipObserver);
mMenuViewModel.getMigrationTooltipVisibilityData().removeObserver(
mMigrationTooltipObserver);
mHandler.removeCallbacksAndMessages(/* token= */ null);
mContext.unregisterComponentCallbacks(mDismissAnimationController);
}
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
if (mEduTooltipView.isPresent()) {
final int x = (int) getX();
final int y = (int) getY();
inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight()));
}
}
@Override
public void onClick(View v) {
mEduTooltipView.ifPresent(this::removeTooltip);
}
private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
final WindowInsets windowInsets = windowMetrics.getWindowInsets();
@@ -249,6 +304,78 @@ class MenuViewLayer extends FrameLayout {
return insets;
}
private void onMigrationTooltipVisibilityChanged(boolean visible) {
mIsMigrationTooltipShowing = visible;
if (mIsMigrationTooltipShowing) {
mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
mEduTooltipView.ifPresent(
view -> addTooltipView(view, getMigrationMessage(), TooltipType.MIGRATION));
}
}
private void onDockTooltipVisibilityChanged(boolean hasSeenTooltip) {
mShouldShowDockTooltip = !hasSeenTooltip;
}
private void onSpringAnimationsEndAction() {
if (mShouldShowDockTooltip) {
mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
mEduTooltipView.ifPresent(view -> addTooltipView(view,
getContext().getText(R.string.accessibility_floating_button_docking_tooltip),
TooltipType.DOCK));
mMenuAnimationController.startTuckedAnimationPreview();
}
}
private CharSequence getMigrationMessage() {
final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_COMPONENT_NAME,
ACCESSIBILITY_BUTTON_COMPONENT_NAME.flattenToShortString());
final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
v -> {
getContext().startActivity(intent);
mEduTooltipView.ifPresent(this::removeTooltip);
});
final int textResId = R.string.accessibility_floating_button_migration_tooltip;
return AnnotationLinkSpan.linkify(getContext().getText(textResId), linkInfo);
}
private void addTooltipView(MenuEduTooltipView tooltipView, CharSequence message,
CharSequence tag) {
addView(tooltipView, LayerIndex.TOOLTIP_VIEW);
tooltipView.show(message);
tooltipView.setTag(tag);
mMenuListViewTouchHandler.setOnActionDownEndListener(
() -> mEduTooltipView.ifPresent(this::removeTooltip));
}
private void removeTooltip(View tooltipView) {
if (tooltipView.getTag().equals(TooltipType.MIGRATION)) {
mMenuViewModel.updateMigrationTooltipVisibility(/* visible= */ false);
mIsMigrationTooltipShowing = false;
}
if (tooltipView.getTag().equals(TooltipType.DOCK)) {
mMenuViewModel.updateDockTooltipVisibility(/* hasSeen= */ true);
mMenuView.clearAnimation();
mShouldShowDockTooltip = false;
}
removeView(tooltipView);
mMenuListViewTouchHandler.setOnActionDownEndListener(null);
mEduTooltipView = Optional.empty();
}
private void hideMenuAndShowMessage() {
final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis(
SHOW_MESSAGE_DELAY_MS,

View File

@@ -36,6 +36,8 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged {
private final MutableLiveData<MenuFadeEffectInfo> mFadeEffectInfoData =
new MutableLiveData<>();
private final MutableLiveData<Boolean> mMoveToTuckedData = new MutableLiveData<>();
private final MutableLiveData<Boolean> mDockTooltipData = new MutableLiveData<>();
private final MutableLiveData<Boolean> mMigrationTooltipData = new MutableLiveData<>();
private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>();
private final MenuInfoRepository mInfoRepository;
@@ -66,11 +68,29 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged {
mInfoRepository.updateMenuSavingPosition(percentagePosition);
}
void updateDockTooltipVisibility(boolean hasSeen) {
mInfoRepository.updateDockTooltipVisibility(hasSeen);
}
void updateMigrationTooltipVisibility(boolean visible) {
mInfoRepository.updateMigrationTooltipVisibility(visible);
}
LiveData<Boolean> getMoveToTuckedData() {
mInfoRepository.loadMenuMoveToTucked(mMoveToTuckedData::setValue);
return mMoveToTuckedData;
}
LiveData<Boolean> getDockTooltipVisibilityData() {
mInfoRepository.loadDockTooltipVisibility(mDockTooltipData::setValue);
return mDockTooltipData;
}
LiveData<Boolean> getMigrationTooltipVisibilityData() {
mInfoRepository.loadMigrationTooltipVisibility(mMigrationTooltipData::setValue);
return mMigrationTooltipData;
}
LiveData<Position> getPercentagePositionData() {
mInfoRepository.loadMenuPosition(mPercentagePositionData::setValue);
return mPercentagePositionData;

View File

@@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import android.graphics.PointF;
import android.testing.AndroidTestingRunner;
@@ -30,6 +32,10 @@ import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.WindowManager;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FlingAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.test.filters.SmallTest;
import com.android.systemui.Prefs;
@@ -39,6 +45,9 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.Optional;
/** Tests for {@link MenuAnimationController}. */
@RunWith(AndroidTestingRunner.class)
@@ -47,9 +56,10 @@ import org.junit.runner.RunWith;
public class MenuAnimationControllerTest extends SysuiTestCase {
private boolean mLastIsMoveToTucked;
private ArgumentCaptor<DynamicAnimation.OnAnimationEndListener> mEndListenerCaptor;
private ViewPropertyAnimator mViewPropertyAnimator;
private MenuView mMenuView;
private MenuAnimationController mMenuAnimationController;
private TestMenuAnimationController mMenuAnimationController;
@Before
public void setUp() throws Exception {
@@ -62,15 +72,17 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
mViewPropertyAnimator = spy(mMenuView.animate());
doReturn(mViewPropertyAnimator).when(mMenuView).animate();
mMenuAnimationController = new MenuAnimationController(mMenuView);
mMenuAnimationController = new TestMenuAnimationController(mMenuView);
mLastIsMoveToTucked = Prefs.getBoolean(mContext,
Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, /* defaultValue= */ false);
mEndListenerCaptor = ArgumentCaptor.forClass(DynamicAnimation.OnAnimationEndListener.class);
}
@After
public void tearDown() throws Exception {
Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
mLastIsMoveToTucked);
mEndListenerCaptor.getAllValues().clear();
}
@Test
@@ -122,4 +134,115 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
assertThat(isMoveToTucked).isFalse();
}
@Test
public void startTuckedAnimationPreview_hasAnimation() {
mMenuView.clearAnimation();
mMenuAnimationController.startTuckedAnimationPreview();
assertThat(mMenuView.getAnimation()).isNotNull();
}
@Test
public void startSpringAnimationsAndEndOneAnimation_notTriggerEndAction() {
final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);
setupAndRunSpringAnimations();
final Optional<DynamicAnimation> anyAnimation =
mMenuAnimationController.mPositionAnimations.values().stream().findAny();
anyAnimation.ifPresent(this::skipAnimationToEnd);
verifyZeroInteractions(onSpringAnimationsEndCallback);
}
@Test
public void startAndEndSpringAnimations_triggerEndAction() {
final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);
setupAndRunSpringAnimations();
mMenuAnimationController.mPositionAnimations.values().forEach(this::skipAnimationToEnd);
verify(onSpringAnimationsEndCallback).run();
}
@Test
public void flingThenSpringAnimationsAreEnded_triggerEndAction() {
final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);
mMenuAnimationController.flingMenuThenSpringToEdge(/* x= */ 0, /* velocityX= */
100, /* velocityY= */ 100);
mMenuAnimationController.mPositionAnimations.values()
.forEach(animation -> verify((FlingAnimation) animation).addEndListener(
mEndListenerCaptor.capture()));
mEndListenerCaptor.getAllValues()
.forEach(listener -> listener.onAnimationEnd(mock(DynamicAnimation.class),
/* canceled */ false, /* endValue */ 0, /* endVelocity */ 0));
mMenuAnimationController.mPositionAnimations.values().forEach(this::skipAnimationToEnd);
verify(onSpringAnimationsEndCallback).run();
}
@Test
public void existFlingIsRunningAndTheOtherAreEnd_notTriggerEndAction() {
final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);
mMenuAnimationController.flingMenuThenSpringToEdge(/* x= */ 0, /* velocityX= */
200, /* velocityY= */ 200);
mMenuAnimationController.mPositionAnimations.values()
.forEach(animation -> verify((FlingAnimation) animation).addEndListener(
mEndListenerCaptor.capture()));
final Optional<DynamicAnimation.OnAnimationEndListener> anyAnimation =
mEndListenerCaptor.getAllValues().stream().findAny();
anyAnimation.ifPresent(
listener -> listener.onAnimationEnd(mock(DynamicAnimation.class), /* canceled */
false, /* endValue */ 0, /* endVelocity */ 0));
mMenuAnimationController.mPositionAnimations.values()
.stream()
.filter(animation -> animation instanceof SpringAnimation)
.forEach(this::skipAnimationToEnd);
verifyZeroInteractions(onSpringAnimationsEndCallback);
}
private void setupAndRunSpringAnimations() {
final float stiffness = 700f;
final float dampingRatio = 0.85f;
final float velocity = 100f;
final float finalPosition = 300f;
mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_X, new SpringForce()
.setStiffness(stiffness)
.setDampingRatio(dampingRatio), velocity, finalPosition);
mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_Y, new SpringForce()
.setStiffness(stiffness)
.setDampingRatio(dampingRatio), velocity, finalPosition);
}
private void skipAnimationToEnd(DynamicAnimation animation) {
final SpringAnimation springAnimation = ((SpringAnimation) animation);
// The doAnimationFrame function is used for skipping animation to the end.
springAnimation.doAnimationFrame(100);
springAnimation.skipToEnd();
springAnimation.doAnimationFrame(200);
}
/**
* Wrapper class for testing.
*/
private static class TestMenuAnimationController extends MenuAnimationController {
TestMenuAnimationController(MenuView menuView) {
super(menuView);
}
@Override
FlingAnimation createFlingAnimation(MenuView menuView,
MenuPositionProperty menuPositionProperty) {
return spy(super.createFlingAnimation(menuView, menuPositionProperty));
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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 com.android.systemui.accessibility.floatingmenu;
import static com.google.common.truth.Truth.assertThat;
import android.content.res.Resources;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link MenuEduTooltipView}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class MenuEduTooltipViewTest extends SysuiTestCase {
private MenuViewAppearance mMenuViewAppearance;
private MenuEduTooltipView mMenuEduTooltipView;
@Before
public void setUp() throws Exception {
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
mMenuViewAppearance = new MenuViewAppearance(mContext, windowManager);
mMenuEduTooltipView = new MenuEduTooltipView(mContext, mMenuViewAppearance);
}
@Test
public void show_matchMessageText() {
final CharSequence text = "Message";
mMenuEduTooltipView.show(text);
final TextView messageView = mMenuEduTooltipView.findViewById(R.id.text);
assertThat(messageView.getText().toString().contentEquals(text)).isTrue();
}
@Test
public void show_menuOnLeft_onRightOfAnchor() {
mMenuViewAppearance.setPercentagePosition(
new Position(/* percentageX= */ 0.0f, /* percentageY= */ 0.0f));
final CharSequence text = "Message";
mMenuEduTooltipView.show(text);
final int tooltipViewX = (int) (mMenuViewAppearance.getMenuPosition().x
+ mMenuViewAppearance.getMenuWidth());
assertThat(mMenuEduTooltipView.getX()).isEqualTo(tooltipViewX);
}
@Test
public void show_menuCloseToLeftOfCenter_onLeftOfAnchor() {
mMenuViewAppearance.setPercentagePosition(
new Position(/* percentageX= */ 0.4f, /* percentageY= */ 0.0f));
final CharSequence text = "Message";
mMenuEduTooltipView.show(text);
final int tooltipViewX = (int) (mMenuViewAppearance.getMenuPosition().x
+ mMenuViewAppearance.getMenuWidth());
assertThat(mMenuEduTooltipView.getX()).isEqualTo(tooltipViewX);
}
@Test
public void show_menuOnRight_onLeftOfAnchor() {
mMenuViewAppearance.setPercentagePosition(
new Position(/* percentageX= */ 1.0f, /* percentageY= */ 0.0f));
final Resources res = getContext().getResources();
final int arrowWidth =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width);
final int arrowMargin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_margin);
final CharSequence text = "Message";
mMenuEduTooltipView.show(text);
final TextView messageView = mMenuEduTooltipView.findViewById(R.id.text);
final int layoutWidth = messageView.getMeasuredWidth() + arrowWidth + arrowMargin;
assertThat(mMenuEduTooltipView.getX()).isEqualTo(
mMenuViewAppearance.getMenuPosition().x - layoutWidth);
}
}

View File

@@ -181,6 +181,20 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase {
verify(mMenuAnimationController).moveToEdgeAndHide();
}
@Test
public void receiveActionDownMotionEvent_verifyOnActionDownEnd() {
final Runnable onActionDownEnd = mock(Runnable.class);
mTouchHandler.setOnActionDownEndListener(onActionDownEnd);
final MotionEvent stubDownEvent =
mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
mStubMenuView.getTranslationY());
mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
verify(onActionDownEnd).run();
}
@After
public void tearDown() {
mMotionEventHelper.recycleEvents();