1 package com.android.systemui.statusbar 2 3 import android.animation.Animator 4 import android.animation.ValueAnimator 5 import android.content.Context 6 import android.content.res.Configuration 7 import android.util.MathUtils 8 import android.view.animation.PathInterpolator 9 import com.android.internal.annotations.VisibleForTesting 10 import com.android.systemui.R 11 import com.android.app.animation.Interpolators 12 import com.android.systemui.dump.DumpManager 13 import com.android.systemui.plugins.qs.QS 14 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 15 import com.android.systemui.statusbar.phone.ScrimController 16 import com.android.systemui.statusbar.policy.ConfigurationController 17 import dagger.assisted.Assisted 18 import dagger.assisted.AssistedFactory 19 import dagger.assisted.AssistedInject 20 import java.io.PrintWriter 21 22 class SplitShadeLockScreenOverScroller 23 @AssistedInject 24 constructor( 25 configurationController: ConfigurationController, 26 dumpManager: DumpManager, 27 private val context: Context, 28 private val scrimController: ScrimController, 29 private val statusBarStateController: SysuiStatusBarStateController, 30 @Assisted private val qSProvider: () -> QS, 31 @Assisted private val nsslControllerProvider: () -> NotificationStackScrollLayoutController 32 ) : LockScreenShadeOverScroller { 33 34 private var releaseOverScrollAnimator: Animator? = null 35 private var transitionToFullShadeDistance = 0 36 private var releaseOverScrollDuration = 0L 37 private var maxOverScrollAmount = 0 38 private var previousOverscrollAmount = 0 39 40 private val qS: QS 41 get() = qSProvider() 42 43 private val nsslController: NotificationStackScrollLayoutController 44 get() = nsslControllerProvider() 45 46 init { 47 updateResources() 48 configurationController.addCallback( 49 object : ConfigurationController.ConfigurationListener { 50 override fun onConfigChanged(newConfig: Configuration?) { 51 updateResources() 52 } 53 }) 54 dumpManager.registerCriticalDumpable("SplitShadeLockscreenOverScroller") { pw, _ -> 55 dump(pw) 56 } 57 } 58 59 private fun updateResources() { 60 val resources = context.resources 61 transitionToFullShadeDistance = 62 resources.getDimensionPixelSize(R.dimen.lockscreen_shade_full_transition_distance) 63 maxOverScrollAmount = 64 resources.getDimensionPixelSize(R.dimen.lockscreen_shade_max_over_scroll_amount) 65 releaseOverScrollDuration = 66 resources.getInteger(R.integer.lockscreen_shade_over_scroll_release_duration).toLong() 67 } 68 69 override var expansionDragDownAmount: Float = 0f 70 set(dragDownAmount) { 71 if (field == dragDownAmount) { 72 return 73 } 74 field = dragDownAmount 75 if (shouldOverscroll()) { 76 overScroll(dragDownAmount) 77 } else if (shouldReleaseOverscroll()) { 78 releaseOverScroll() 79 } 80 } 81 82 private fun shouldOverscroll() = statusBarStateController.state == StatusBarState.KEYGUARD 83 84 private fun shouldReleaseOverscroll() = !shouldOverscroll() && previousOverscrollAmount != 0 85 86 private fun overScroll(dragDownAmount: Float) { 87 val overscrollAmount: Int = calculateOverscrollAmount(dragDownAmount) 88 applyOverscroll(overscrollAmount) 89 previousOverscrollAmount = overscrollAmount 90 } 91 92 private fun applyOverscroll(overscrollAmount: Int) { 93 qS.setOverScrollAmount(overscrollAmount) 94 scrimController.setNotificationsOverScrollAmount(overscrollAmount) 95 nsslController.setOverScrollAmount(overscrollAmount) 96 } 97 98 private fun calculateOverscrollAmount(dragDownAmount: Float): Int { 99 val fullHeight: Int = nsslController.height 100 val fullHeightProgress: Float = MathUtils.saturate(dragDownAmount / fullHeight) 101 val overshootStart: Float = transitionToFullShadeDistance / fullHeight.toFloat() 102 val overShootTransitionProgress: Float = 103 Interpolators.getOvershootInterpolation( 104 fullHeightProgress, OVER_SHOOT_AMOUNT, overshootStart) 105 return (overShootTransitionProgress * maxOverScrollAmount).toInt() 106 } 107 108 private fun releaseOverScroll() { 109 val animator = ValueAnimator.ofInt(previousOverscrollAmount, 0) 110 animator.addUpdateListener { 111 val overScrollAmount = it.animatedValue as Int 112 qS.setOverScrollAmount(overScrollAmount) 113 scrimController.setNotificationsOverScrollAmount(overScrollAmount) 114 nsslController.setOverScrollAmount(overScrollAmount) 115 } 116 animator.interpolator = RELEASE_OVER_SCROLL_INTERPOLATOR 117 animator.duration = releaseOverScrollDuration 118 animator.start() 119 releaseOverScrollAnimator = animator 120 previousOverscrollAmount = 0 121 } 122 123 @VisibleForTesting 124 internal fun finishAnimations() { 125 releaseOverScrollAnimator?.end() 126 releaseOverScrollAnimator = null 127 } 128 129 private fun dump(printWriter: PrintWriter) { 130 printWriter.println( 131 """ 132 SplitShadeLockScreenOverScroller: 133 Resources: 134 transitionToFullShadeDistance: $transitionToFullShadeDistance 135 maxOverScrollAmount: $maxOverScrollAmount 136 releaseOverScrollDuration: $releaseOverScrollDuration 137 State: 138 previousOverscrollAmount: $previousOverscrollAmount 139 expansionDragDownAmount: $expansionDragDownAmount 140 """.trimIndent()) 141 } 142 143 @AssistedFactory 144 fun interface Factory { 145 fun create( 146 qSProvider: () -> QS, 147 nsslControllerProvider: () -> NotificationStackScrollLayoutController 148 ): SplitShadeLockScreenOverScroller 149 } 150 151 companion object { 152 private const val OVER_SHOOT_AMOUNT = 0.6f 153 private val RELEASE_OVER_SCROLL_INTERPOLATOR = PathInterpolator(0.17f, 0f, 0f, 1f) 154 } 155 } 156