1 /* <lambda>null2 * Copyright (C) 2024 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 17 package com.android.wm.shell.windowdecor.education 18 19 import android.annotation.ColorInt 20 import android.annotation.DimenRes 21 import android.annotation.LayoutRes 22 import android.content.Context 23 import android.content.res.Resources 24 import android.graphics.Point 25 import android.view.LayoutInflater 26 import android.view.MotionEvent 27 import android.view.View 28 import android.view.WindowManager 29 import android.widget.LinearLayout 30 import android.widget.TextView 31 import android.window.DisplayAreaInfo 32 import android.window.WindowContainerTransaction 33 import androidx.dynamicanimation.animation.DynamicAnimation 34 import androidx.dynamicanimation.animation.SpringForce 35 import com.android.wm.shell.R 36 import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener 37 import com.android.wm.shell.common.DisplayController 38 import com.android.wm.shell.shared.animation.PhysicsAnimator 39 import com.android.wm.shell.windowdecor.WindowManagerWrapper 40 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer 41 42 /** 43 * Controls the lifecycle of an education promo, including showing and hiding it. 44 */ 45 class DesktopWindowingEducationPromoController( 46 private val context: Context, 47 private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory, 48 private val displayController: DisplayController, 49 ) : OnDisplayChangingListener { 50 private var educationView: View? = null 51 private var animator: PhysicsAnimator<View>? = null 52 private val springConfig by lazy { 53 PhysicsAnimator.SpringConfig( 54 SpringForce.STIFFNESS_MEDIUM, 55 SpringForce.DAMPING_RATIO_LOW_BOUNCY 56 ) 57 } 58 private var popupWindow: AdditionalSystemViewContainer? = null 59 60 override fun onDisplayChange( 61 displayId: Int, 62 fromRotation: Int, 63 toRotation: Int, 64 newDisplayAreaInfo: DisplayAreaInfo?, 65 t: WindowContainerTransaction? 66 ) { 67 // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and 68 // [toRotation] can be one of the [@Surface.Rotation] values. 69 if ((fromRotation % 2 == toRotation % 2)) return 70 hideEducation() 71 } 72 73 /** 74 * Shows education promo. 75 * 76 * @param viewConfig features of the education. 77 * @param taskId is used in the title of popup window created for the education view. 78 */ 79 fun showEducation( 80 viewConfig: EducationViewConfig, 81 taskId: Int 82 ) { 83 hideEducation() 84 educationView = createEducationView(viewConfig, taskId) 85 animator = createAnimator() 86 animateShowEducationTransition() 87 displayController.addDisplayChangingController(this) 88 } 89 90 /** Hide the current education view if visible */ 91 private fun hideEducation() = animateHideEducationTransition { cleanUp() } 92 93 /** Create education view by inflating layout provided. */ 94 private fun createEducationView( 95 viewConfig: EducationViewConfig, 96 taskId: Int 97 ): View { 98 val educationView = 99 LayoutInflater.from(context) 100 .inflate( 101 viewConfig.viewLayout, /* root= */ null, /* attachToRoot= */ false) 102 .apply { 103 alpha = 0f 104 scaleX = 0f 105 scaleY = 0f 106 107 requireViewById<TextView>(R.id.education_text).apply { 108 text = viewConfig.educationText 109 } 110 setOnTouchListener { _, motionEvent -> 111 if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) { 112 hideEducation() 113 true 114 } else { 115 false 116 } 117 } 118 setOnClickListener { 119 hideEducation() 120 } 121 setEducationColorScheme(viewConfig.educationColorScheme) 122 } 123 124 createEducationPopupWindow( 125 taskId, 126 viewConfig.viewGlobalCoordinates, 127 loadDimensionPixelSize(viewConfig.widthId), 128 loadDimensionPixelSize(viewConfig.heightId), 129 educationView = educationView) 130 131 return educationView 132 } 133 134 /** Create animator for education transitions */ 135 private fun createAnimator(): PhysicsAnimator<View>? = 136 educationView?.let { 137 PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) } 138 } 139 140 /** Animate show transition for the education view */ 141 private fun animateShowEducationTransition() { 142 animator 143 ?.spring(DynamicAnimation.ALPHA, 1f) 144 ?.spring(DynamicAnimation.SCALE_X, 1f) 145 ?.spring(DynamicAnimation.SCALE_Y, 1f) 146 ?.start() 147 } 148 149 /** Animate hide transition for the education view */ 150 private fun animateHideEducationTransition(endActions: () -> Unit) { 151 animator 152 ?.spring(DynamicAnimation.ALPHA, 0f) 153 ?.spring(DynamicAnimation.SCALE_X, 0f) 154 ?.spring(DynamicAnimation.SCALE_Y, 0f) 155 ?.start() 156 endActions() 157 } 158 159 /** Remove education promo and clean up all relative properties */ 160 private fun cleanUp() { 161 educationView = null 162 animator = null 163 popupWindow?.releaseView() 164 popupWindow = null 165 displayController.removeDisplayChangingController(this) 166 } 167 168 private fun createEducationPopupWindow( 169 taskId: Int, 170 educationViewGlobalCoordinates: Point, 171 width: Int, 172 height: Int, 173 educationView: View, 174 ) { 175 popupWindow = 176 additionalSystemViewContainerFactory.create( 177 windowManagerWrapper = 178 WindowManagerWrapper(context.getSystemService(WindowManager::class.java)), 179 taskId = taskId, 180 x = educationViewGlobalCoordinates.x, 181 y = educationViewGlobalCoordinates.y, 182 width = width, 183 height = height, 184 flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 185 WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, 186 view = educationView) 187 } 188 189 private fun View.setEducationColorScheme(educationColorScheme: EducationColorScheme) { 190 requireViewById<LinearLayout>(R.id.education_container).apply { 191 background.setTint(educationColorScheme.container) 192 } 193 requireViewById<TextView>(R.id.education_text).apply { 194 setTextColor(educationColorScheme.text) 195 } 196 } 197 198 private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { 199 if (resourceId == Resources.ID_NULL) return 0 200 return context.resources.getDimensionPixelSize(resourceId) 201 } 202 203 /** 204 * The configuration for education view features: 205 * 206 * @property viewLayout Layout resource ID of the view to be used for education promo. 207 * @property viewGlobalCoordinates Global (screen) coordinates of the education. 208 * @property educationText Text to be added to the TextView of the promo. 209 * @property widthId res Id for education width 210 * @property heightId res Id for education height 211 */ 212 data class EducationViewConfig( 213 @LayoutRes val viewLayout: Int, 214 val educationColorScheme: EducationColorScheme, 215 val viewGlobalCoordinates: Point, 216 val educationText: String, 217 @DimenRes val widthId: Int, 218 @DimenRes val heightId: Int 219 ) 220 221 /** 222 * Color scheme of education view: 223 * 224 * @property container Color of the container of the education. 225 * @property text Text color of the [TextView] of education promo. 226 */ 227 data class EducationColorScheme( 228 @ColorInt val container: Int, 229 @ColorInt val text: Int, 230 ) 231 } 232