1 /*
2  * 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.launcher3.taskbar.bubbles
18 
19 import android.animation.ValueAnimator
20 import android.content.Context
21 import androidx.annotation.VisibleForTesting
22 import androidx.core.animation.doOnEnd
23 import androidx.dynamicanimation.animation.SpringForce
24 import com.android.launcher3.anim.AnimatedFloat
25 import com.android.launcher3.anim.SpringAnimationBuilder
26 import com.android.launcher3.taskbar.TaskbarActivityContext
27 import com.android.launcher3.taskbar.TaskbarThresholdUtils
28 import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.COLLAPSED
29 import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.EXPANDED
30 import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.STASHED
31 import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.UNKNOWN
32 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
33 import com.android.launcher3.touch.OverScroll
34 
35 /** Handle swipe events on the bubble bar and handle */
36 class BubbleBarSwipeController {
37 
38     private val context: Context
39 
40     private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
41     private lateinit var bubbleBarViewController: BubbleBarViewController
42     private lateinit var bubbleStashController: BubbleStashController
43 
44     private var springAnimation: ValueAnimator? = null
45     private val animatedSwipeTranslation = AnimatedFloat(this::onSwipeUpdate)
46 
47     private val unstashThreshold: Int
48     private val maxOverscroll: Int
49 
50     private var swipeState: SwipeState = SwipeState(startState = UNKNOWN)
51 
52     constructor(tac: TaskbarActivityContext) : this(tac, DefaultDimensionProvider(tac))
53 
54     @VisibleForTesting
55     constructor(context: Context, dimensionProvider: DimensionProvider) {
56         this.context = context
57         unstashThreshold = dimensionProvider.unstashThreshold
58         maxOverscroll = dimensionProvider.maxOverscroll
59     }
60 
initnull61     fun init(bubbleControllers: BubbleControllers) {
62         bubbleStashedHandleViewController =
63             bubbleControllers.bubbleStashedHandleViewController.orElse(null)
64         bubbleBarViewController = bubbleControllers.bubbleBarViewController
65         bubbleStashController = bubbleControllers.bubbleStashController
66     }
67 
68     /** Start tracking a new swipe gesture */
startnull69     fun start() {
70         if (springAnimation != null) reset()
71         val startState =
72             when {
73                 bubbleStashController.isStashed -> STASHED
74                 bubbleBarViewController.isExpanded -> EXPANDED
75                 bubbleStashController.isBubbleBarVisible() -> COLLAPSED
76                 else -> UNKNOWN
77             }
78         swipeState = SwipeState(startState = startState, currentState = startState)
79     }
80 
81     /** Update swipe distance to [dy] */
swipeTonull82     fun swipeTo(dy: Float) {
83         if (!canHandleSwipe(dy)) {
84             return
85         }
86         animatedSwipeTranslation.updateValue(dy)
87 
88         swipeState.passedUnstash = isUnstash(dy)
89         // Tracking swipe gesture if we pass unstash threshold at least once during gesture
90         swipeState.isSwipe = swipeState.isSwipe || swipeState.passedUnstash
91         when {
92             canUnstash() && swipeState.passedUnstash -> {
93                 swipeState.currentState = COLLAPSED
94                 bubbleStashController.showBubbleBar(expandBubbles = false, bubbleBarGesture = true)
95             }
96             canStash() && !swipeState.passedUnstash -> {
97                 swipeState.currentState = STASHED
98                 bubbleStashController.stashBubbleBar()
99             }
100         }
101     }
102 
103     /** Finish tracking swipe gesture. Animate views back to resting state */
finishnull104     fun finish() {
105         if (swipeState.passedUnstash && swipeState.startState in setOf(STASHED, COLLAPSED)) {
106             bubbleStashController.showBubbleBar(expandBubbles = true, bubbleBarGesture = true)
107         }
108         if (animatedSwipeTranslation.value == 0f) {
109             reset()
110         } else {
111             springToRest()
112         }
113     }
114 
115     /** Returns `true` if we are tracking a swipe gesture */
isSwipeGesturenull116     fun isSwipeGesture(): Boolean {
117         return swipeState.isSwipe
118     }
119 
canHandleSwipenull120     private fun canHandleSwipe(dy: Float): Boolean {
121         return when (swipeState.startState) {
122             STASHED -> {
123                 if (swipeState.currentState == COLLAPSED) {
124                     // if we have unstashed the bar, allow swipe in both directions
125                     true
126                 } else {
127                     // otherwise, only allow swipe up on stash handle
128                     dy < 0
129                 }
130             }
131             COLLAPSED -> dy < 0 // collapsed bar can only be swiped up
132             UNKNOWN,
133             EXPANDED -> false // expanded bar can't be swiped
134         }
135     }
136 
isUnstashnull137     private fun isUnstash(dy: Float): Boolean {
138         return dy < -unstashThreshold
139     }
140 
canStashnull141     private fun canStash(): Boolean {
142         // Only allow stashing if we started from stashed state
143         return swipeState.startState == STASHED && swipeState.currentState == COLLAPSED
144     }
145 
canUnstashnull146     private fun canUnstash(): Boolean {
147         return swipeState.currentState == STASHED
148     }
149 
resetnull150     private fun reset() {
151         springAnimation?.let {
152             if (it.isRunning) {
153                 it.removeAllListeners()
154                 it.cancel()
155                 animatedSwipeTranslation.updateValue(0f)
156             }
157         }
158         springAnimation = null
159         swipeState = SwipeState(startState = UNKNOWN)
160     }
161 
onSwipeUpdatenull162     private fun onSwipeUpdate(value: Float) {
163         val dampedSwipe = -OverScroll.dampedScroll(-value, maxOverscroll).toFloat()
164         bubbleStashedHandleViewController?.setTranslationYForSwipe(dampedSwipe)
165         bubbleBarViewController.setTranslationYForSwipe(dampedSwipe)
166     }
167 
springToRestnull168     private fun springToRest() {
169         springAnimation =
170             SpringAnimationBuilder(context)
171                 .setStartValue(animatedSwipeTranslation.value)
172                 .setEndValue(0f)
173                 .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
174                 .setStiffness(SpringForce.STIFFNESS_LOW)
175                 .build(animatedSwipeTranslation, AnimatedFloat.VALUE)
176                 .also { it.doOnEnd { reset() } }
177         springAnimation?.start()
178     }
179 
180     internal data class SwipeState(
181         val startState: BarState,
182         var currentState: BarState = UNKNOWN,
183         var passedUnstash: Boolean = false,
184         var isSwipe: Boolean = false,
185     )
186 
187     internal enum class BarState {
188         UNKNOWN,
189         STASHED,
190         COLLAPSED,
191         EXPANDED,
192     }
193 
194     /** Allows overriding the dimension provider for testing */
195     @VisibleForTesting
196     interface DimensionProvider {
197         val unstashThreshold: Int
198         val maxOverscroll: Int
199     }
200 
201     private class DefaultDimensionProvider(taskbarActivityContext: TaskbarActivityContext) :
202         DimensionProvider {
203         override val unstashThreshold: Int
204         override val maxOverscroll: Int
205 
206         init {
207             val resources = taskbarActivityContext.resources
208             unstashThreshold =
209                 TaskbarThresholdUtils.getFromNavThreshold(
210                     resources,
211                     taskbarActivityContext.deviceProfile,
212                 )
213             maxOverscroll = taskbarActivityContext.deviceProfile.heightPx - unstashThreshold
214         }
215     }
216 }
217