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