Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 20faab26 authored by PETER LIANG's avatar PETER LIANG Committed by Android (Google) Code Review
Browse files

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).
parents 3ed5cc4c 403730de
Loading
Loading
Loading
Loading
+59 −6
Original line number Diff line number Diff line
@@ -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(),
                            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"));
    }
+222 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+41 −0
Original line number Diff line number Diff line
@@ -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);
+9 −0
Original line number Diff line number Diff line
@@ -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.
     */
+1 −1
Original line number Diff line number Diff line
@@ -293,7 +293,7 @@ class MenuViewAppearance {
        return bounds;
    }

    private boolean isMenuOnLeftSide() {
    boolean isMenuOnLeftSide() {
        return mPercentagePosition.getPercentageX() < 0.5f;
    }

Loading