1 /* 2 * Copyright (C) 2020 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.wm.shell.pip.phone; 18 19 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Insets; 24 import android.graphics.PixelFormat; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.graphics.drawable.TransitionDrawable; 28 import android.view.Gravity; 29 import android.view.MotionEvent; 30 import android.view.SurfaceControl; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.ViewTreeObserver; 34 import android.view.WindowInsets; 35 import android.view.WindowManager; 36 import android.widget.FrameLayout; 37 38 import androidx.annotation.NonNull; 39 import androidx.dynamicanimation.animation.DynamicAnimation; 40 import androidx.dynamicanimation.animation.SpringForce; 41 42 import com.android.wm.shell.R; 43 import com.android.wm.shell.animation.PhysicsAnimator; 44 import com.android.wm.shell.common.DismissCircleView; 45 import com.android.wm.shell.common.ShellExecutor; 46 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 47 import com.android.wm.shell.pip.PipUiEventLogger; 48 49 import kotlin.Unit; 50 51 /** 52 * Handler of all Magnetized Object related code for PiP. 53 */ 54 public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListener { 55 56 /* The multiplier to apply scale the target size by when applying the magnetic field radius */ 57 private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; 58 59 /** Duration of the dismiss scrim fading in/out. */ 60 private static final int DISMISS_TRANSITION_DURATION_MS = 200; 61 62 /** 63 * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move 64 * PIP. 65 */ 66 private MagnetizedObject<Rect> mMagnetizedPip; 67 68 /** 69 * Container for the dismiss circle, so that it can be animated within the container via 70 * translation rather than within the WindowManager via slow layout animations. 71 */ 72 private ViewGroup mTargetViewContainer; 73 74 /** Circle view used to render the dismiss target. */ 75 private DismissCircleView mTargetView; 76 77 /** 78 * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius. 79 */ 80 private MagnetizedObject.MagneticTarget mMagneticTarget; 81 82 /** 83 * PhysicsAnimator instance for animating the dismiss target in/out. 84 */ 85 private PhysicsAnimator<View> mMagneticTargetAnimator; 86 87 /** Default configuration to use for springing the dismiss target in/out. */ 88 private final PhysicsAnimator.SpringConfig mTargetSpringConfig = 89 new PhysicsAnimator.SpringConfig( 90 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); 91 92 // Allow dragging the PIP to a location to close it 93 private boolean mEnableDismissDragToEdge; 94 95 private int mTargetSize; 96 private int mDismissAreaHeight; 97 private float mMagneticFieldRadiusPercent = 1f; 98 private WindowInsets mWindowInsets; 99 100 private SurfaceControl mTaskLeash; 101 private boolean mHasDismissTargetSurface; 102 103 private final Context mContext; 104 private final PipMotionHelper mMotionHelper; 105 private final PipUiEventLogger mPipUiEventLogger; 106 private final WindowManager mWindowManager; 107 private final ShellExecutor mMainExecutor; 108 PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, PipMotionHelper motionHelper, ShellExecutor mainExecutor)109 public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, 110 PipMotionHelper motionHelper, ShellExecutor mainExecutor) { 111 mContext = context; 112 mPipUiEventLogger = pipUiEventLogger; 113 mMotionHelper = motionHelper; 114 mMainExecutor = mainExecutor; 115 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 116 } 117 init()118 public void init() { 119 Resources res = mContext.getResources(); 120 mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge); 121 mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); 122 123 if (mTargetViewContainer != null) { 124 // init can be called multiple times, remove the old one from view hierarchy first. 125 cleanUpDismissTarget(); 126 } 127 128 mTargetView = new DismissCircleView(mContext); 129 mTargetViewContainer = new FrameLayout(mContext); 130 mTargetViewContainer.setBackgroundDrawable( 131 mContext.getDrawable(R.drawable.floating_dismiss_gradient_transition)); 132 mTargetViewContainer.setClipChildren(false); 133 mTargetViewContainer.addView(mTargetView); 134 mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> { 135 if (!windowInsets.equals(mWindowInsets)) { 136 mWindowInsets = windowInsets; 137 updateMagneticTargetSize(); 138 } 139 return windowInsets; 140 }); 141 142 mMagnetizedPip = mMotionHelper.getMagnetizedPip(); 143 mMagnetizedPip.clearAllTargets(); 144 mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); 145 updateMagneticTargetSize(); 146 147 mMagnetizedPip.setAnimateStuckToTarget( 148 (target, velX, velY, flung, after) -> { 149 if (mEnableDismissDragToEdge) { 150 mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after); 151 } 152 return Unit.INSTANCE; 153 }); 154 mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { 155 @Override 156 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { 157 // Show the dismiss target, in case the initial touch event occurred within 158 // the magnetic field radius. 159 if (mEnableDismissDragToEdge) { 160 showDismissTargetMaybe(); 161 } 162 } 163 164 @Override 165 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 166 float velX, float velY, boolean wasFlungOut) { 167 if (wasFlungOut) { 168 mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */); 169 hideDismissTargetMaybe(); 170 } else { 171 mMotionHelper.setSpringingToTouch(true); 172 } 173 } 174 175 @Override 176 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { 177 if (mEnableDismissDragToEdge) { 178 mMainExecutor.executeDelayed(() -> { 179 mMotionHelper.notifyDismissalPending(); 180 mMotionHelper.animateDismiss(); 181 hideDismissTargetMaybe(); 182 183 mPipUiEventLogger.log( 184 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); 185 }, 0); 186 } 187 } 188 }); 189 190 mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView); 191 } 192 193 @Override onPreDraw()194 public boolean onPreDraw() { 195 mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); 196 mHasDismissTargetSurface = true; 197 updateDismissTargetLayer(); 198 return true; 199 } 200 201 /** 202 * Potentially start consuming future motion events if PiP is currently near the magnetized 203 * object. 204 */ maybeConsumeMotionEvent(MotionEvent ev)205 public boolean maybeConsumeMotionEvent(MotionEvent ev) { 206 return mMagnetizedPip.maybeConsumeMotionEvent(ev); 207 } 208 209 /** 210 * Update the magnet size. 211 */ updateMagneticTargetSize()212 public void updateMagneticTargetSize() { 213 if (mTargetView == null) { 214 return; 215 } 216 217 final Resources res = mContext.getResources(); 218 mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); 219 mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); 220 final WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); 221 final Insets navInset = insets.getInsetsIgnoringVisibility( 222 WindowInsets.Type.navigationBars()); 223 final FrameLayout.LayoutParams newParams = 224 new FrameLayout.LayoutParams(mTargetSize, mTargetSize); 225 newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 226 newParams.bottomMargin = navInset.bottom + mContext.getResources().getDimensionPixelSize( 227 R.dimen.floating_dismiss_bottom_margin); 228 mTargetView.setLayoutParams(newParams); 229 230 // Set the magnetic field radius equal to the target size from the center of the target 231 setMagneticFieldRadiusPercent(mMagneticFieldRadiusPercent); 232 } 233 234 /** 235 * Increase or decrease the field radius of the magnet object, e.g. with larger percent, 236 * PiP will magnetize to the field sooner. 237 */ setMagneticFieldRadiusPercent(float percent)238 public void setMagneticFieldRadiusPercent(float percent) { 239 mMagneticFieldRadiusPercent = percent; 240 mMagneticTarget.setMagneticFieldRadiusPx((int) (mMagneticFieldRadiusPercent * mTargetSize 241 * MAGNETIC_FIELD_RADIUS_MULTIPLIER)); 242 } 243 setTaskLeash(SurfaceControl taskLeash)244 public void setTaskLeash(SurfaceControl taskLeash) { 245 mTaskLeash = taskLeash; 246 } 247 updateDismissTargetLayer()248 private void updateDismissTargetLayer() { 249 if (!mHasDismissTargetSurface || mTaskLeash == null) { 250 // No dismiss target surface, can just return 251 return; 252 } 253 254 // Put the dismiss target behind the task 255 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 256 t.setRelativeLayer(mTargetViewContainer.getViewRootImpl().getSurfaceControl(), 257 mTaskLeash, -1); 258 t.apply(); 259 } 260 261 /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ createOrUpdateDismissTarget()262 public void createOrUpdateDismissTarget() { 263 if (!mTargetViewContainer.isAttachedToWindow()) { 264 mMagneticTargetAnimator.cancel(); 265 266 mTargetViewContainer.setVisibility(View.INVISIBLE); 267 mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); 268 mHasDismissTargetSurface = false; 269 270 try { 271 mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); 272 } catch (IllegalStateException e) { 273 // This shouldn't happen, but if the target is already added, just update its layout 274 // params. 275 mWindowManager.updateViewLayout( 276 mTargetViewContainer, getDismissTargetLayoutParams()); 277 } 278 } else { 279 mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); 280 } 281 } 282 283 /** Returns layout params for the dismiss target, using the latest display metrics. */ getDismissTargetLayoutParams()284 private WindowManager.LayoutParams getDismissTargetLayoutParams() { 285 final Point windowSize = new Point(); 286 mWindowManager.getDefaultDisplay().getRealSize(windowSize); 287 288 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 289 WindowManager.LayoutParams.MATCH_PARENT, 290 mDismissAreaHeight, 291 0, windowSize.y - mDismissAreaHeight, 292 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 293 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 294 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 295 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 296 PixelFormat.TRANSLUCENT); 297 298 lp.setTitle("pip-dismiss-overlay"); 299 lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 300 lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 301 lp.setFitInsetsTypes(0 /* types */); 302 303 return lp; 304 } 305 306 /** Makes the dismiss target visible and animates it in, if it isn't already visible. */ showDismissTargetMaybe()307 public void showDismissTargetMaybe() { 308 if (!mEnableDismissDragToEdge) { 309 return; 310 } 311 312 createOrUpdateDismissTarget(); 313 314 if (mTargetViewContainer.getVisibility() != View.VISIBLE) { 315 mTargetView.setTranslationY(mTargetViewContainer.getHeight()); 316 mTargetViewContainer.setVisibility(View.VISIBLE); 317 mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this); 318 319 // Cancel in case we were in the middle of animating it out. 320 mMagneticTargetAnimator.cancel(); 321 mMagneticTargetAnimator 322 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig) 323 .start(); 324 325 ((TransitionDrawable) mTargetViewContainer.getBackground()).startTransition( 326 DISMISS_TRANSITION_DURATION_MS); 327 } 328 } 329 330 /** Animates the magnetic dismiss target out and then sets it to GONE. */ hideDismissTargetMaybe()331 public void hideDismissTargetMaybe() { 332 if (!mEnableDismissDragToEdge) { 333 return; 334 } 335 336 mMagneticTargetAnimator 337 .spring(DynamicAnimation.TRANSLATION_Y, 338 mTargetViewContainer.getHeight(), 339 mTargetSpringConfig) 340 .withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE)) 341 .start(); 342 343 ((TransitionDrawable) mTargetViewContainer.getBackground()).reverseTransition( 344 DISMISS_TRANSITION_DURATION_MS); 345 } 346 347 /** 348 * Removes the dismiss target and cancels any pending callbacks to show it. 349 */ cleanUpDismissTarget()350 public void cleanUpDismissTarget() { 351 if (mTargetViewContainer.isAttachedToWindow()) { 352 mWindowManager.removeViewImmediate(mTargetViewContainer); 353 } 354 } 355 } 356