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