1 /* <lambda>null2 * Copyright (C) 2024 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.shared.bubbles 18 19 import android.graphics.Point 20 import android.graphics.RectF 21 import android.view.View 22 import androidx.annotation.VisibleForTesting 23 import androidx.core.animation.Animator 24 import androidx.core.animation.AnimatorListenerAdapter 25 import androidx.core.animation.ObjectAnimator 26 import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener 27 import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT 28 import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT 29 30 /** 31 * Base class for common logic shared between different bubble views to support pinning bubble bar 32 * to left or right edge of screen. 33 * 34 * Handles drag events and allows a [LocationChangeListener] to be registered that is notified when 35 * location of the bubble bar should change. 36 * 37 * Shows a drop target when releasing a view would update the [BubbleBarLocation]. 38 */ 39 abstract class BaseBubblePinController(private val screenSizeProvider: () -> Point) { 40 41 private var initialLocationOnLeft = false 42 private var onLeft = false 43 private var dismissZone: RectF? = null 44 private var stuckToDismissTarget = false 45 private var screenCenterX = 0 46 private var listener: LocationChangeListener? = null 47 private var dropTargetAnimator: ObjectAnimator? = null 48 49 /** 50 * Signal the controller that dragging interaction has started. 51 * 52 * @param initialLocationOnLeft side of the screen where bubble bar is pinned to 53 */ 54 fun onDragStart(initialLocationOnLeft: Boolean) { 55 this.initialLocationOnLeft = initialLocationOnLeft 56 onLeft = initialLocationOnLeft 57 screenCenterX = screenSizeProvider.invoke().x / 2 58 dismissZone = getExclusionRect() 59 listener?.onStart(if (initialLocationOnLeft) LEFT else RIGHT) 60 } 61 62 /** View has moved to [x] and [y] screen coordinates */ 63 fun onDragUpdate(x: Float, y: Float) { 64 if (dismissZone?.contains(x, y) == true) return 65 66 val wasOnLeft = onLeft 67 onLeft = x < screenCenterX 68 if (wasOnLeft != onLeft) { 69 onLocationChange(if (onLeft) LEFT else RIGHT) 70 } else if (stuckToDismissTarget) { 71 // Moved out of the dismiss view back to initial side, if we have a drop target, show it 72 getDropTargetView()?.apply { animateIn() } 73 } 74 // Make sure this gets cleared 75 stuckToDismissTarget = false 76 } 77 78 /** Signal the controller that view has been dragged to dismiss view. */ 79 fun onStuckToDismissTarget() { 80 stuckToDismissTarget = true 81 // Notify that location may be reset 82 val shouldResetLocation = onLeft != initialLocationOnLeft 83 if (shouldResetLocation) { 84 onLeft = initialLocationOnLeft 85 listener?.onChange(if (onLeft) LEFT else RIGHT) 86 } 87 getDropTargetView()?.apply { 88 animateOut { 89 if (shouldResetLocation) { 90 updateLocation(if (onLeft) LEFT else RIGHT) 91 } 92 } 93 } 94 } 95 96 /** Signal the controller that dragging interaction has finished. */ 97 fun onDragEnd() { 98 getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } 99 dismissZone = null 100 listener?.onRelease(if (onLeft) LEFT else RIGHT) 101 } 102 103 /** 104 * [LocationChangeListener] that is notified when dragging interaction has resulted in bubble 105 * bar to be pinned on the other edge 106 */ 107 fun setListener(listener: LocationChangeListener?) { 108 this.listener = listener 109 } 110 111 /** Get width for exclusion rect where dismiss takes over drag */ 112 protected abstract fun getExclusionRectWidth(): Float 113 114 /** Get height for exclusion rect where dismiss takes over drag */ 115 protected abstract fun getExclusionRectHeight(): Float 116 117 /** Create the drop target view and attach it to the parent */ 118 protected abstract fun createDropTargetView(): View 119 120 /** Get the drop target view if it exists */ 121 protected abstract fun getDropTargetView(): View? 122 123 /** Remove the drop target view */ 124 protected abstract fun removeDropTargetView(view: View) 125 126 /** Update size and location of the drop target view */ 127 protected abstract fun updateLocation(location: BubbleBarLocation) 128 129 private fun onLocationChange(location: BubbleBarLocation) { 130 showDropTarget(location) 131 listener?.onChange(location) 132 } 133 134 private fun getExclusionRect(): RectF { 135 val rect = RectF(0f, 0f, getExclusionRectWidth(), getExclusionRectHeight()) 136 // Center it around the bottom center of the screen 137 val screenBottom = screenSizeProvider.invoke().y 138 rect.offsetTo(screenCenterX - rect.width() / 2, screenBottom - rect.height()) 139 return rect 140 } 141 142 private fun showDropTarget(location: BubbleBarLocation) { 143 val targetView = getDropTargetView() ?: createDropTargetView().apply { alpha = 0f } 144 if (targetView.alpha > 0) { 145 targetView.animateOut { 146 updateLocation(location) 147 targetView.animateIn() 148 } 149 } else { 150 updateLocation(location) 151 targetView.animateIn() 152 } 153 } 154 155 private fun View.animateIn() { 156 dropTargetAnimator?.cancel() 157 dropTargetAnimator = 158 ObjectAnimator.ofFloat(this, View.ALPHA, 1f) 159 .setDuration(DROP_TARGET_ALPHA_IN_DURATION) 160 .addEndAction { dropTargetAnimator = null } 161 dropTargetAnimator?.start() 162 } 163 164 private fun View.animateOut(endAction: Runnable? = null) { 165 dropTargetAnimator?.cancel() 166 dropTargetAnimator = 167 ObjectAnimator.ofFloat(this, View.ALPHA, 0f) 168 .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) 169 .addEndAction { 170 endAction?.run() 171 dropTargetAnimator = null 172 } 173 dropTargetAnimator?.start() 174 } 175 176 private fun <T : Animator> T.addEndAction(runnable: Runnable): T { 177 addListener( 178 object : AnimatorListenerAdapter() { 179 override fun onAnimationEnd(animation: Animator) { 180 runnable.run() 181 } 182 } 183 ) 184 return this 185 } 186 187 /** Receive updates on location changes */ 188 interface LocationChangeListener { 189 /** Bubble bar dragging has started. Includes the initial location of the bar */ 190 fun onStart(location: BubbleBarLocation) {} 191 192 /** 193 * Bubble bar has been dragged to a new [BubbleBarLocation]. And the drag is still in 194 * progress. 195 * 196 * Triggered when drag gesture passes the middle of the screen and before touch up. Can be 197 * triggered multiple times per gesture. 198 * 199 * @param location new location as a result of the ongoing drag operation 200 */ 201 fun onChange(location: BubbleBarLocation) {} 202 203 /** 204 * Bubble bar has been released in the [BubbleBarLocation]. 205 * 206 * @param location final location of the bubble bar once drag is released 207 */ 208 fun onRelease(location: BubbleBarLocation) 209 } 210 211 companion object { 212 @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L 213 @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L 214 } 215 } 216