1 /*
2  * Copyright (C) 2023 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 package com.android.wm.shell.bubbles.bar
17 
18 import android.content.Context
19 import android.view.LayoutInflater
20 import android.view.ViewGroup
21 import androidx.core.view.doOnLayout
22 import androidx.dynamicanimation.animation.DynamicAnimation
23 import androidx.dynamicanimation.animation.SpringForce
24 import com.android.wm.shell.R
25 import com.android.wm.shell.animation.PhysicsAnimator
26 import com.android.wm.shell.bubbles.BubbleEducationController
27 import com.android.wm.shell.bubbles.BubbleViewProvider
28 import com.android.wm.shell.bubbles.setup
29 import com.android.wm.shell.common.bubbles.BubblePopupView
30 
31 /** Manages bubble education presentation and animation */
32 class BubbleEducationViewController(private val context: Context, private val listener: Listener) {
33     interface Listener {
34         fun onManageEducationVisibilityChanged(isVisible: Boolean)
35     }
36 
37     private var rootView: ViewGroup? = null
38     private var educationView: BubblePopupView? = null
39     private var animator: PhysicsAnimator<BubblePopupView>? = null
40 
41     private val springConfig by lazy {
42         PhysicsAnimator.SpringConfig(
43             SpringForce.STIFFNESS_MEDIUM,
44             SpringForce.DAMPING_RATIO_LOW_BOUNCY
45         )
46     }
47 
48     private val controller by lazy { BubbleEducationController(context) }
49 
50     /** Whether the education view is visible or being animated */
51     val isManageEducationVisible: Boolean
52         get() = educationView != null && rootView != null
53 
54     /**
55      * Show manage bubble education if hasn't been shown before
56      *
57      * @param bubble the bubble used for the manage education check
58      * @param root the view to show manage education in
59      */
60     fun maybeShowManageEducation(bubble: BubbleViewProvider, root: ViewGroup) {
61         if (!controller.shouldShowManageEducation(bubble)) return
62         showManageEducation(root)
63     }
64 
65     /**
66      * Hide the manage education view if visible
67      *
68      * @param animated whether should hide with animation
69      */
70     fun hideManageEducation(animated: Boolean) {
71         rootView?.let {
72             fun cleanUp() {
73                 it.removeView(educationView)
74                 rootView = null
75                 listener.onManageEducationVisibilityChanged(isVisible = false)
76             }
77 
78             if (animated) {
79                 animateTransition(show = false, ::cleanUp)
80             } else {
81                 cleanUp()
82             }
83         }
84     }
85 
86     /**
87      * Show manage education with animation
88      *
89      * @param root the view to show manage education in
90      */
91     private fun showManageEducation(root: ViewGroup) {
92         hideManageEducation(animated = false)
93         if (educationView == null) {
94             val eduView = createEducationView(root)
95             educationView = eduView
96             animator = createAnimation(eduView)
97         }
98         root.addView(educationView)
99         rootView = root
100         animateTransition(show = true) {
101             controller.hasSeenManageEducation = true
102             listener.onManageEducationVisibilityChanged(isVisible = true)
103         }
104     }
105 
106     /**
107      * Animate show/hide transition for the education view
108      *
109      * @param show whether to show or hide the view
110      * @param endActions a closure to be called when the animation completes
111      */
112     private fun animateTransition(show: Boolean, endActions: () -> Unit) {
113         animator?.let { animator ->
114             animator
115                 .spring(DynamicAnimation.ALPHA, if (show) 1f else 0f)
116                 .spring(DynamicAnimation.SCALE_X, if (show) 1f else EDU_SCALE_HIDDEN)
117                 .spring(DynamicAnimation.SCALE_Y, if (show) 1f else EDU_SCALE_HIDDEN)
118                 .withEndActions(endActions)
119                 .start()
120         } ?: endActions()
121     }
122 
123     private fun createEducationView(root: ViewGroup): BubblePopupView {
124         val view =
125             LayoutInflater.from(context).inflate(R.layout.bubble_bar_manage_education, root, false)
126                 as BubblePopupView
127 
128         return view.apply {
129             setup()
130             alpha = 0f
131             pivotY = 0f
132             scaleX = EDU_SCALE_HIDDEN
133             scaleY = EDU_SCALE_HIDDEN
134             doOnLayout { it.pivotX = it.width / 2f }
135             setOnClickListener { hideManageEducation(animated = true) }
136         }
137     }
138 
139     private fun createAnimation(view: BubblePopupView): PhysicsAnimator<BubblePopupView> {
140         val animator = PhysicsAnimator.getInstance(view)
141         animator.setDefaultSpringConfig(springConfig)
142         return animator
143     }
144 
145     companion object {
146         private const val EDU_SCALE_HIDDEN = 0.5f
147     }
148 }
149