/* * Copyright (C) 2019 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.assist; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.os.Handler; import android.util.Log; import android.util.MathUtils; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.CornerHandleView; import com.android.systemui.R; import com.android.systemui.statusbar.phone.NavigationBarTransitions; /** * A class for managing Assistant handle show, hide and animation. */ public class AssistHandleViewController implements NavigationBarTransitions.DarkIntensityListener { private static final boolean DEBUG = false; private static final String TAG = "AssistHandleViewController"; private Handler mHandler; private CornerHandleView mAssistHintLeft; private CornerHandleView mAssistHintRight; private int mBottomOffset; @VisibleForTesting boolean mAssistHintVisible; @VisibleForTesting boolean mAssistHintBlocked = false; public AssistHandleViewController(Handler handler, View navBar) { mHandler = handler; mAssistHintLeft = navBar.findViewById(R.id.assist_hint_left); mAssistHintRight = navBar.findViewById(R.id.assist_hint_right); } @Override public void onDarkIntensity(float darkIntensity) { mAssistHintLeft.updateDarkness(darkIntensity); mAssistHintRight.updateDarkness(darkIntensity); } /** * Set the bottom offset. * * @param bottomOffset the bottom offset to translate. */ public void setBottomOffset(int bottomOffset) { if (mBottomOffset != bottomOffset) { mBottomOffset = bottomOffset; if (mAssistHintVisible) { // If assist handles are visible, hide them without animation and then make them // show once again (with corrected bottom offset). hideAssistHandles(); setAssistHintVisible(true); } } } /** * Controls the visibility of the assist gesture handles. * * @param visible whether the handles should be shown */ public void setAssistHintVisible(boolean visible) { if (!mHandler.getLooper().isCurrentThread()) { mHandler.post(() -> setAssistHintVisible(visible)); return; } if (mAssistHintBlocked && visible) { if (DEBUG) { Log.v(TAG, "Assist hint blocked, cannot make it visible"); } return; } if (mAssistHintVisible != visible) { mAssistHintVisible = visible; fade(mAssistHintLeft, mAssistHintVisible, /* isLeft = */ true); fade(mAssistHintRight, mAssistHintVisible, /* isLeft = */ false); } } /** * Prevents the assist hint from becoming visible even if `mAssistHintVisible` is true. */ public void setAssistHintBlocked(boolean blocked) { if (!mHandler.getLooper().isCurrentThread()) { mHandler.post(() -> setAssistHintBlocked(blocked)); return; } mAssistHintBlocked = blocked; if (mAssistHintVisible && mAssistHintBlocked) { hideAssistHandles(); } } private void hideAssistHandles() { mAssistHintLeft.setVisibility(View.GONE); mAssistHintRight.setVisibility(View.GONE); mAssistHintVisible = false; } /** * Returns an animator that animates the given view from start to end over durationMs. Start and * end represent total animation progress: 0 is the start, 1 is the end, 1.1 would be an * overshoot. */ Animator getHandleAnimator(View view, float start, float end, boolean isLeft, long durationMs, Interpolator interpolator) { // Note that lerp does allow overshoot, in cases where start and end are outside of [0,1]. float scaleStart = MathUtils.lerp(2f, 1f, start); float scaleEnd = MathUtils.lerp(2f, 1f, end); Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, scaleStart, scaleEnd); Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, scaleStart, scaleEnd); float translationStart = MathUtils.lerp(0.2f, 0f, start); float translationEnd = MathUtils.lerp(0.2f, 0f, end); int xDirection = isLeft ? -1 : 1; Animator translateX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, xDirection * translationStart * view.getWidth(), xDirection * translationEnd * view.getWidth()); Animator translateY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, translationStart * view.getHeight() + mBottomOffset, translationEnd * view.getHeight() + mBottomOffset); AnimatorSet set = new AnimatorSet(); set.play(scaleX).with(scaleY); set.play(scaleX).with(translateX); set.play(scaleX).with(translateY); set.setDuration(durationMs); set.setInterpolator(interpolator); return set; } private void fade(View view, boolean fadeIn, boolean isLeft) { if (fadeIn) { view.animate().cancel(); view.setAlpha(1f); view.setVisibility(View.VISIBLE); // A piecewise spring-like interpolation. // End value in one animator call must match the start value in the next, otherwise // there will be a discontinuity. AnimatorSet anim = new AnimatorSet(); Animator first = getHandleAnimator(view, 0, 1.1f, isLeft, 750, new PathInterpolator(0, 0.45f, .67f, 1f)); Interpolator secondInterpolator = new PathInterpolator(0.33f, 0, 0.67f, 1f); Animator second = getHandleAnimator(view, 1.1f, 0.97f, isLeft, 400, secondInterpolator); Animator third = getHandleAnimator(view, 0.97f, 1.02f, isLeft, 400, secondInterpolator); Animator fourth = getHandleAnimator(view, 1.02f, 1f, isLeft, 400, secondInterpolator); anim.play(first).before(second); anim.play(second).before(third); anim.play(third).before(fourth); anim.start(); } else { view.animate().cancel(); view.animate() .setInterpolator(new AccelerateInterpolator(1.5f)) .setDuration(250) .alpha(0f); } } }