blob: 60633762b19b9b3c8e777372ae449c8d978a6a5d [file] [log] [blame]
/*
* Copyright (C) 2023 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.launcher3.taskbar.bubbles;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.res.Resources;
import android.os.SystemProperties;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import com.android.launcher3.R;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarDragLayer;
import com.android.wm.shell.common.bubbles.DismissView;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
/**
* Controls dismiss view presentation for the bubble bar dismiss functionality.
* Provides the dragged view snapping to the target dismiss area and animates it.
* When the dragged bubble/bubble stack is realised inside of the target area, it gets dismissed.
*
* @see BubbleDragController
*/
public class BubbleDismissController {
private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
private final TaskbarActivityContext mActivity;
private final TaskbarDragLayer mDragLayer;
@Nullable
private BubbleBarViewController mBubbleBarViewController;
// Dismiss view that's attached to drag layer. It consists of the scrim view and the circular
// dismiss view used as a dismiss target.
@Nullable
private DismissView mDismissView;
// The currently magnetized object, which is being dragged and will be attracted to the magnetic
// dismiss target. This is either the stack itself, or an individual bubble.
@Nullable
private MagnetizedObject<View> mMagnetizedObject;
// The MagneticTarget instance for our circular dismiss view. This is added to the
// MagnetizedObject instances for the stack and any dragged-out bubbles.
@Nullable
private MagnetizedObject.MagneticTarget mMagneticTarget;
@Nullable
private ValueAnimator mDismissAnimator;
public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
mActivity = activity;
mDragLayer = dragLayer;
}
/**
* Initializes dependencies when bubble controllers are created.
* Should be careful to only access things that were created in constructors for now, as some
* controllers may still be waiting for init().
*/
public void init(@NonNull BubbleControllers bubbleControllers) {
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
}
/**
* Setup the dismiss view and magnetized object that will be attracted to magnetic target.
* Should be called before handling events or showing/hiding dismiss view.
* @param magnetizedView the view to be pulled into target dismiss area
*/
public void setupDismissView(@NonNull View magnetizedView) {
setupDismissView();
setupMagnetizedObject(magnetizedView);
}
/**
* Handle the touch event and pass it to the magnetized object.
* It should be called after {@code setupDismissView}
*/
public boolean handleTouchEvent(@NonNull MotionEvent event) {
return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
}
/**
* Show dismiss view with animation
* It should be called after {@code setupDismissView}
*/
public void showDismissView() {
if (mDismissView == null) return;
mDismissView.show();
}
/**
* Hide dismiss view with animation
* It should be called after {@code setupDismissView}
*/
public void hideDismissView() {
if (mDismissView == null) return;
mDismissView.hide();
}
/**
* Dismiss magnetized object when it's released in the dismiss target area
*/
private void dismissMagnetizedObject() {
if (mMagnetizedObject == null || mBubbleBarViewController == null) return;
if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
if (bubbleView.getBubble() != null) {
mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
}
} else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
mBubbleBarViewController.onDismissAllBubblesWhileDragging();
}
cleanUpAnimatedViews();
}
/**
* Animate dismiss view when magnetized object gets stuck in the magnetic target
* @param view captured view
*/
private void animateDismissCaptured(@NonNull View view) {
cancelAnimations();
mDismissAnimator = createDismissAnimator(view);
mDismissAnimator.start();
}
/**
* Animate dismiss view when magnetized object gets unstuck from the magnetic target
*/
private void animateDismissReleased() {
if (mDismissAnimator == null) return;
mDismissAnimator.removeAllListeners();
mDismissAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
cancelAnimations();
}
@Override
public void onAnimationEnd(Animator animation) {
cancelAnimations();
}
});
mDismissAnimator.reverse();
}
/**
* Cancel and clear dismiss animations and reset view states
*/
private void cancelAnimations() {
if (mDismissAnimator == null) return;
ValueAnimator animator = mDismissAnimator;
mDismissAnimator = null;
animator.cancel();
}
/**
* Clean up views changed during animation
*/
private void cleanUpAnimatedViews() {
// Cancel animations
cancelAnimations();
// Reset dismiss view
if (mDismissView != null) {
mDismissView.getCircle().setScaleX(1f);
mDismissView.getCircle().setScaleY(1f);
}
// Reset magnetized view
if (mMagnetizedObject != null) {
mMagnetizedObject.getUnderlyingObject().setAlpha(1f);
mMagnetizedObject.getUnderlyingObject().setScaleX(1f);
mMagnetizedObject.getUnderlyingObject().setScaleY(1f);
}
}
private void setupDismissView() {
if (mDismissView != null) return;
mDismissView = new DismissView(mActivity.getApplicationContext());
BubbleDismissViewUtils.setup(mDismissView);
mDragLayer.addView(mDismissView, /* index = */ 0,
new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
setupMagneticTarget(mDismissView.getCircle());
}
private void setupMagneticTarget(@NonNull View view) {
int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize(
R.dimen.bubblebar_dismiss_target_size);
mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius);
}
private void setupMagnetizedObject(@NonNull View magnetizedView) {
mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
@Override
public float getWidth(@NonNull View underlyingObject) {
return underlyingObject.getWidth();
}
@Override
public float getHeight(@NonNull View underlyingObject) {
return underlyingObject.getHeight();
}
@Override
public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
underlyingObject.getLocationOnScreen(loc);
}
};
mMagnetizedObject.setHapticsEnabled(true);
mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
if (mMagneticTarget != null) {
mMagnetizedObject.addTarget(mMagneticTarget);
}
mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() {
@Override
public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
animateDismissCaptured(magnetizedView);
}
@Override
public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
float velX, float velY, boolean wasFlungOut) {
animateDismissReleased();
}
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
dismissMagnetizedObject();
}
});
}
private ValueAnimator createDismissAnimator(@NonNull View magnetizedView) {
Resources resources = mActivity.getResources();
int expandedSize = resources.getDimensionPixelSize(R.dimen.bubblebar_dismiss_target_size);
int collapsedSize = resources.getDimensionPixelSize(
R.dimen.bubblebar_dismiss_target_small_size);
float minScale = (float) collapsedSize / expandedSize;
ValueAnimator animator = ValueAnimator.ofFloat(1f, minScale);
animator.addUpdateListener(animation -> {
if (mDismissView == null) return;
final float animatedValue = (float) animation.getAnimatedValue();
mDismissView.getCircle().setScaleX(animatedValue);
mDismissView.getCircle().setScaleY(animatedValue);
magnetizedView.setAlpha(animatedValue);
if (magnetizedView instanceof BubbleBarView) {
magnetizedView.setScaleX(animatedValue);
magnetizedView.setScaleY(animatedValue);
}
});
return animator;
}
}