• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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