• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.LayoutRes
19 import android.content.Context
20 import android.graphics.Point
21 import android.graphics.Rect
22 import android.util.Log
23 import android.view.Gravity
24 import android.view.LayoutInflater
25 import android.view.View
26 import android.view.ViewGroup
27 import android.widget.FrameLayout
28 import androidx.core.view.doOnLayout
29 import androidx.dynamicanimation.animation.DynamicAnimation
30 import androidx.dynamicanimation.animation.SpringForce
31 import com.android.wm.shell.R
32 import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION
33 import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES
34 import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME
35 import com.android.wm.shell.bubbles.BubbleEducationController
36 import com.android.wm.shell.bubbles.BubbleViewProvider
37 import com.android.wm.shell.bubbles.setup
38 import com.android.wm.shell.shared.TypefaceUtils
39 import com.android.wm.shell.shared.animation.PhysicsAnimator
40 import com.android.wm.shell.shared.bubbles.BubblePopupDrawable
41 import com.android.wm.shell.shared.bubbles.BubblePopupView
42 import kotlin.math.roundToInt
43 
44 /** Manages bubble education presentation and animation */
45 class BubbleEducationViewController(private val context: Context, private val listener: Listener) {
46     interface Listener {
onEducationVisibilityChangednull47         fun onEducationVisibilityChanged(isVisible: Boolean)
48     }
49 
50     private var rootView: ViewGroup? = null
51     private var educationView: BubblePopupView? = null
52     private var animator: PhysicsAnimator<BubblePopupView>? = null
53 
54     private val springConfig by lazy {
55         PhysicsAnimator.SpringConfig(
56             SpringForce.STIFFNESS_MEDIUM,
57             SpringForce.DAMPING_RATIO_LOW_BOUNCY
58         )
59     }
60 
<lambda>null61     private val scrimView by lazy {
62         View(context).apply {
63             importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
64             setOnClickListener { hideEducation(animated = true) }
65         }
66     }
67 
<lambda>null68     private val controller by lazy { BubbleEducationController(context) }
69 
70     /** Whether the education view is visible or being animated */
71     val isEducationVisible: Boolean
72         get() = educationView != null && rootView != null
73 
74     /**
75      * Hide the current education view if visible
76      *
77      * @param animated whether should hide with animation
78      */
79     @JvmOverloads
<lambda>null80     fun hideEducation(animated: Boolean, endActions: () -> Unit = {}) {
<lambda>null81         log { "hideEducation animated: $animated" }
82 
83         if (animated) {
<lambda>null84             animateTransition(show = false) {
85                 cleanUp()
86                 endActions()
87                 listener.onEducationVisibilityChanged(isVisible = false)
88             }
89         } else {
90             cleanUp()
91             endActions()
92             listener.onEducationVisibilityChanged(isVisible = false)
93         }
94     }
95 
96     /**
97      * Show bubble bar stack user education.
98      *
99      * @param position the reference position for the user education in Screen coordinates.
100      * @param root the view to show user education in.
101      * @param educationClickHandler the on click handler for the user education view
102      */
showStackEducationnull103     fun showStackEducation(position: Point, root: ViewGroup, educationClickHandler: () -> Unit) {
104         hideEducation(animated = false)
105         log { "showStackEducation at: $position" }
106 
107         val rootBounds = Rect()
108         // Get root bounds on screen as position is in screen coordinates
109         root.getBoundsOnScreen(rootBounds)
110         educationView =
111             createEducationView(R.layout.bubble_bar_stack_education, root).apply {
112                 TypefaceUtils.setTypeface(findViewById(R.id.education_title),
113                     TypefaceUtils.FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED)
114                 TypefaceUtils.setTypeface(findViewById(R.id.education_text),
115                     TypefaceUtils.FontFamily.GSF_BODY_MEDIUM)
116                 setArrowDirection(BubblePopupDrawable.ArrowDirection.DOWN)
117                 updateEducationPosition(view = this, position, rootBounds)
118                 val arrowToEdgeOffset = popupDrawable?.config?.cornerRadius ?: 0f
119                 doOnLayout {
120                     it.pivotX = if (position.x < rootBounds.centerX())
121                         arrowToEdgeOffset else it.width - arrowToEdgeOffset
122                     it.pivotY = it.height.toFloat()
123                 }
124                 setOnClickListener { educationClickHandler() }
125             }
126 
127         rootView = root
128         animator = createAnimator()
129 
130         root.addView(scrimView)
131         root.addView(educationView)
132         animateTransition(show = true) {
133             controller.hasSeenStackEducation = true
134             listener.onEducationVisibilityChanged(isVisible = true)
135         }
136     }
137 
138     /**
139      * Show manage bubble education if hasn't been shown before
140      *
141      * @param bubble the bubble used for the manage education check
142      * @param root the view to show manage education in
143      */
maybeShowManageEducationnull144     fun maybeShowManageEducation(bubble: BubbleViewProvider, root: ViewGroup) {
145         log { "maybeShowManageEducation bubble: $bubble" }
146         if (!controller.shouldShowManageEducation(bubble)) return
147         showManageEducation(root)
148     }
149 
150     /**
151      * Show manage education with animation
152      *
153      * @param root the view to show manage education in
154      */
showManageEducationnull155     private fun showManageEducation(root: ViewGroup) {
156         hideEducation(animated = false)
157         log { "showManageEducation" }
158 
159         educationView =
160             createEducationView(R.layout.bubble_bar_manage_education, root).apply {
161                 TypefaceUtils.setTypeface(findViewById(R.id.education_manage_title),
162                     TypefaceUtils.FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED)
163                 TypefaceUtils.setTypeface(findViewById(R.id.education_manage_text),
164                     TypefaceUtils.FontFamily.GSF_BODY_MEDIUM)
165                 pivotY = 0f
166                 doOnLayout { it.pivotX = it.width / 2f }
167                 setOnClickListener { hideEducation(animated = true) }
168             }
169 
170         rootView = root
171         animator = createAnimator()
172 
173         root.addView(scrimView)
174         root.addView(educationView)
175         animateTransition(show = true) {
176             controller.hasSeenManageEducation = true
177             listener.onEducationVisibilityChanged(isVisible = true)
178         }
179     }
180 
181     /**
182      * Animate show/hide transition for the education view
183      *
184      * @param show whether to show or hide the view
185      * @param endActions a closure to be called when the animation completes
186      */
animateTransitionnull187     private fun animateTransition(show: Boolean, endActions: () -> Unit) {
188         animator
189             ?.spring(DynamicAnimation.ALPHA, if (show) 1f else 0f)
190             ?.spring(DynamicAnimation.SCALE_X, if (show) 1f else EDU_SCALE_HIDDEN)
191             ?.spring(DynamicAnimation.SCALE_Y, if (show) 1f else EDU_SCALE_HIDDEN)
192             ?.withEndActions(endActions)
193             ?.start()
194             ?: endActions()
195     }
196 
197     /** Remove education view from the root and clean up all relative properties */
cleanUpnull198     private fun cleanUp() {
199         log { "cleanUp" }
200         rootView?.removeView(educationView)
201         rootView?.removeView(scrimView)
202         educationView = null
203         rootView = null
204         animator = null
205     }
206 
207     /**
208      * Create education view by inflating layout provided.
209      *
210      * @param layout layout resource id to inflate. The root view should be [BubblePopupView]
211      * @param root view group to use as root for inflation, is not attached to root
212      */
createEducationViewnull213     private fun createEducationView(@LayoutRes layout: Int, root: ViewGroup): BubblePopupView {
214         val view = LayoutInflater.from(context).inflate(layout, root, false) as BubblePopupView
215         view.setup()
216         view.alpha = 0f
217         view.scaleX = EDU_SCALE_HIDDEN
218         view.scaleY = EDU_SCALE_HIDDEN
219         return view
220     }
221 
222     /** Create animator for the user education transitions */
createAnimatornull223     private fun createAnimator(): PhysicsAnimator<BubblePopupView>? {
224         return educationView?.let {
225             PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) }
226         }
227     }
228 
229     /**
230      * Update user education view position relative to the reference position
231      *
232      * @param view the user education view to layout
233      * @param position the reference position in Screen coordinates
234      * @param rootBounds bounds of the parent the education view is placed in
235      */
updateEducationPositionnull236     private fun updateEducationPosition(view: BubblePopupView, position: Point, rootBounds: Rect) {
237         // Get the offset to the arrow from the edge of the education view
238         val arrowToEdgeOffset =
239             view.popupDrawable?.config?.let { it.cornerRadius + it.arrowWidth / 2f }?.roundToInt()
240                 ?: 0
241         // Calculate education view margins
242         val params = view.layoutParams as FrameLayout.LayoutParams
243         params.bottomMargin = rootBounds.bottom - position.y
244         if (position.x < rootBounds.centerX()) {
245             params.leftMargin = position.x - arrowToEdgeOffset
246             params.gravity = Gravity.LEFT or Gravity.BOTTOM
247             view.setArrowPosition(BubblePopupDrawable.ArrowPosition.Start)
248         } else {
249             params.rightMargin = rootBounds.right - position.x - arrowToEdgeOffset
250             params.gravity = Gravity.RIGHT or Gravity.BOTTOM
251             view.setArrowPosition(BubblePopupDrawable.ArrowPosition.End)
252         }
253         view.layoutParams = params
254     }
255 
lognull256     private fun log(msg: () -> String) {
257         if (DEBUG_USER_EDUCATION) Log.d(TAG, msg())
258     }
259 
260     companion object {
261         private val TAG = if (TAG_WITH_CLASS_NAME) "BubbleEducationViewController" else TAG_BUBBLES
262         private const val EDU_SCALE_HIDDEN = 0.5f
263     }
264 }
265