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