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 hideDropTarget() 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 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 fun hideDropTarget() { 156 getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } 157 } 158 159 private fun View.animateIn() { 160 dropTargetAnimator?.cancel() 161 dropTargetAnimator = 162 ObjectAnimator.ofFloat(this, View.ALPHA, 1f) 163 .setDuration(DROP_TARGET_ALPHA_IN_DURATION) 164 .addEndAction { dropTargetAnimator = null } 165 dropTargetAnimator?.start() 166 } 167 168 private fun View.animateOut(endAction: Runnable? = null) { 169 dropTargetAnimator?.cancel() 170 dropTargetAnimator = 171 ObjectAnimator.ofFloat(this, View.ALPHA, 0f) 172 .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) 173 .addEndAction { 174 endAction?.run() 175 dropTargetAnimator = null 176 } 177 dropTargetAnimator?.start() 178 } 179 180 private fun <T : Animator> T.addEndAction(runnable: Runnable): T { 181 addListener( 182 object : AnimatorListenerAdapter() { 183 override fun onAnimationEnd(animation: Animator) { 184 runnable.run() 185 } 186 } 187 ) 188 return this 189 } 190 191 /** Receive updates on location changes */ 192 interface LocationChangeListener { 193 /** Bubble bar dragging has started. Includes the initial location of the bar */ 194 fun onStart(location: BubbleBarLocation) {} 195 196 /** 197 * Bubble bar has been dragged to a new [BubbleBarLocation]. And the drag is still in 198 * progress. 199 * 200 * Triggered when drag gesture passes the middle of the screen and before touch up. Can be 201 * triggered multiple times per gesture. 202 * 203 * @param location new location as a result of the ongoing drag operation 204 */ 205 fun onChange(location: BubbleBarLocation) {} 206 207 /** 208 * Bubble bar has been released in the [BubbleBarLocation]. 209 * 210 * @param location final location of the bubble bar once drag is released 211 */ 212 fun onRelease(location: BubbleBarLocation) 213 } 214 215 companion object { 216 @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L 217 @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L 218 } 219 } 220