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