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