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