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:
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -293,7 +293,7 @@ class MenuViewAppearance {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
private boolean isMenuOnLeftSide() {
|
||||
boolean isMenuOnLeftSide() {
|
||||
return mPercentagePosition.getPercentageX() < 0.5f;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user