1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility.floatingmenu; 18 19 import static android.R.id.empty; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.ValueAnimator; 24 import android.util.ArrayMap; 25 import android.util.Pair; 26 import android.view.MotionEvent; 27 28 import androidx.annotation.NonNull; 29 import androidx.dynamicanimation.animation.DynamicAnimation; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.systemui.Flags; 33 import com.android.wm.shell.common.bubbles.DismissCircleView; 34 import com.android.wm.shell.common.bubbles.DismissView; 35 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 36 37 import java.util.Map; 38 import java.util.Objects; 39 40 /** 41 * Controls the interaction between {@link MagnetizedObject} and 42 * {@link MagnetizedObject.MagneticTarget}. 43 */ 44 class DragToInteractAnimationController { 45 private static final float COMPLETELY_OPAQUE = 1.0f; 46 private static final float COMPLETELY_TRANSPARENT = 0.0f; 47 private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f; 48 private static final float ANIMATING_MAX_ALPHA = 0.7f; 49 50 private final DragToInteractView mInteractView; 51 private final DismissView mDismissView; 52 private final MenuView mMenuView; 53 54 /** 55 * MagnetizedObject cannot differentiate between its MagnetizedTargets, 56 * so we need an object & an animator for every interactable. 57 */ 58 private final ArrayMap<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> mInteractMap; 59 60 private float mMinInteractSize; 61 private float mSizePercent; 62 DragToInteractAnimationController(DragToInteractView interactView, MenuView menuView)63 DragToInteractAnimationController(DragToInteractView interactView, MenuView menuView) { 64 mDismissView = null; 65 mInteractView = interactView; 66 mInteractView.setPivotX(interactView.getWidth() / 2.0f); 67 mInteractView.setPivotY(interactView.getHeight() / 2.0f); 68 mMenuView = menuView; 69 70 updateResources(); 71 72 mInteractMap = new ArrayMap<>(); 73 interactView.getInteractMap().forEach((viewId, pair) -> { 74 DismissCircleView circleView = pair.getFirst(); 75 createMagnetizedObjectAndAnimator(circleView); 76 }); 77 } 78 DragToInteractAnimationController(DismissView dismissView, MenuView menuView)79 DragToInteractAnimationController(DismissView dismissView, MenuView menuView) { 80 mDismissView = dismissView; 81 mInteractView = null; 82 mDismissView.setPivotX(dismissView.getWidth() / 2.0f); 83 mDismissView.setPivotY(dismissView.getHeight() / 2.0f); 84 mMenuView = menuView; 85 86 updateResources(); 87 88 mInteractMap = new ArrayMap<>(); 89 createMagnetizedObjectAndAnimator(dismissView.getCircle()); 90 } 91 showInteractView(boolean show)92 void showInteractView(boolean show) { 93 if (Flags.floatingMenuDragToEdit() && mInteractView != null) { 94 if (show) { 95 mInteractView.show(); 96 } else { 97 mInteractView.hide(); 98 } 99 } else if (mDismissView != null) { 100 if (show) { 101 mDismissView.show(); 102 } else { 103 mDismissView.hide(); 104 } 105 } 106 } 107 setMagnetListener(MagnetizedObject.MagnetListener magnetListener)108 void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) { 109 mInteractMap.forEach((viewId, pair) -> { 110 MagnetizedObject<?> magnetizedObject = pair.first; 111 magnetizedObject.setMagnetListener(magnetListener); 112 }); 113 } 114 115 @VisibleForTesting getMagnetListener(int id)116 MagnetizedObject.MagnetListener getMagnetListener(int id) { 117 return Objects.requireNonNull(mInteractMap.get(id)).first.getMagnetListener(); 118 } 119 maybeConsumeDownMotionEvent(MotionEvent event)120 void maybeConsumeDownMotionEvent(MotionEvent event) { 121 mInteractMap.forEach((viewId, pair) -> { 122 MagnetizedObject<?> magnetizedObject = pair.first; 123 magnetizedObject.maybeConsumeMotionEvent(event); 124 }); 125 } 126 maybeConsumeMotionEvent(MotionEvent event)127 private int maybeConsumeMotionEvent(MotionEvent event) { 128 for (Map.Entry<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> set: 129 mInteractMap.entrySet()) { 130 MagnetizedObject<MenuView> magnetizedObject = set.getValue().first; 131 if (magnetizedObject.maybeConsumeMotionEvent(event)) { 132 return set.getKey(); 133 } 134 } 135 return empty; 136 } 137 138 /** 139 * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized objects 140 * to check if it was within a magnetic field. 141 * It should be used in the {@link MenuListViewTouchHandler}. 142 * 143 * @param event that move the magnetized object which is also the menu list view. 144 * @return id of a target if the location of the motion events moves 145 * within the field of the target, otherwise it returns{@link android.R.id#empty}. 146 * <p> 147 * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. 148 */ maybeConsumeMoveMotionEvent(MotionEvent event)149 int maybeConsumeMoveMotionEvent(MotionEvent event) { 150 return maybeConsumeMotionEvent(event); 151 } 152 153 /** 154 * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was 155 * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. 156 * 157 * @param event that move the magnetized object which is also the menu list view. 158 * @return id of a target if the location of the motion events moves 159 * within the field of the target, otherwise it returns{@link android.R.id#empty}. 160 * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. 161 */ maybeConsumeUpMotionEvent(MotionEvent event)162 int maybeConsumeUpMotionEvent(MotionEvent event) { 163 return maybeConsumeMotionEvent(event); 164 } 165 animateInteractMenu(int targetViewId, boolean scaleUp)166 void animateInteractMenu(int targetViewId, boolean scaleUp) { 167 Pair<MagnetizedObject<MenuView>, ValueAnimator> value = mInteractMap.get(targetViewId); 168 if (value == null) { 169 return; 170 } 171 ValueAnimator animator = value.second; 172 if (scaleUp) { 173 animator.start(); 174 } else { 175 animator.reverse(); 176 } 177 } 178 updateResources()179 void updateResources() { 180 final float maxInteractSize = mMenuView.getResources().getDimensionPixelSize( 181 com.android.wm.shell.R.dimen.dismiss_circle_size); 182 mMinInteractSize = mMenuView.getResources().getDimensionPixelSize( 183 com.android.wm.shell.R.dimen.dismiss_circle_small); 184 mSizePercent = mMinInteractSize / maxInteractSize; 185 } 186 187 /** 188 * Creates a magnetizedObject & valueAnimator pair for the provided circleView, 189 * and adds them to the interactMap. 190 * 191 * @param circleView circleView to create objects for. 192 */ createMagnetizedObjectAndAnimator(DismissCircleView circleView)193 private void createMagnetizedObjectAndAnimator(DismissCircleView circleView) { 194 MagnetizedObject<MenuView> magnetizedObject = new MagnetizedObject<MenuView>( 195 mMenuView.getContext(), mMenuView, 196 new MenuAnimationController.MenuPositionProperty( 197 DynamicAnimation.TRANSLATION_X), 198 new MenuAnimationController.MenuPositionProperty( 199 DynamicAnimation.TRANSLATION_Y)) { 200 @Override 201 public void getLocationOnScreen(MenuView underlyingObject, @NonNull int[] loc) { 202 underlyingObject.getLocationOnScreen(loc); 203 } 204 205 @Override 206 public float getHeight(MenuView underlyingObject) { 207 return underlyingObject.getHeight(); 208 } 209 210 @Override 211 public float getWidth(MenuView underlyingObject) { 212 return underlyingObject.getWidth(); 213 } 214 }; 215 // Avoid unintended selection of an object / option 216 magnetizedObject.setFlingToTargetEnabled(false); 217 magnetizedObject.addTarget(new MagnetizedObject.MagneticTarget( 218 circleView, (int) mMinInteractSize)); 219 220 final ValueAnimator animator = 221 ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); 222 223 animator.addUpdateListener(dismissAnimation -> { 224 final float animatedValue = (float) dismissAnimation.getAnimatedValue(); 225 final float scaleValue = Math.max(animatedValue, mSizePercent); 226 circleView.setScaleX(scaleValue); 227 circleView.setScaleY(scaleValue); 228 229 mMenuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); 230 }); 231 232 animator.addListener(new AnimatorListenerAdapter() { 233 @Override 234 public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { 235 super.onAnimationEnd(animation, isReverse); 236 237 if (isReverse) { 238 circleView.setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); 239 circleView.setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); 240 mMenuView.setAlpha(COMPLETELY_OPAQUE); 241 } 242 } 243 }); 244 245 mInteractMap.put(circleView.getId(), new Pair<>(magnetizedObject, animator)); 246 } 247 } 248