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.util.Size 26 import android.view.LayoutInflater 27 import android.view.MotionEvent 28 import android.view.View 29 import android.view.View.LAYOUT_DIRECTION_RTL 30 import android.view.View.MeasureSpec.UNSPECIFIED 31 import android.view.WindowManager 32 import android.widget.ImageView 33 import android.widget.LinearLayout 34 import android.widget.TextView 35 import android.window.DisplayAreaInfo 36 import android.window.WindowContainerTransaction 37 import androidx.core.graphics.drawable.DrawableCompat 38 import androidx.dynamicanimation.animation.DynamicAnimation 39 import androidx.dynamicanimation.animation.SpringForce 40 import com.android.wm.shell.R 41 import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener 42 import com.android.wm.shell.common.DisplayController 43 import com.android.wm.shell.shared.animation.PhysicsAnimator 44 import com.android.wm.shell.windowdecor.WindowManagerWrapper 45 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer 46 47 /** 48 * Controls the lifecycle of an education tooltip, including showing and hiding it. Ensures that 49 * only one tooltip is displayed at a time. 50 */ 51 class DesktopWindowingEducationTooltipController( 52 private val context: Context, 53 private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory, 54 private val displayController: DisplayController, 55 ) : OnDisplayChangingListener { 56 private var tooltipView: View? = null 57 private var animator: PhysicsAnimator<View>? = null 58 private val springConfig by lazy { 59 PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY) 60 } 61 private var popupWindow: AdditionalSystemViewContainer? = null 62 63 override fun onDisplayChange( 64 displayId: Int, 65 fromRotation: Int, 66 toRotation: Int, 67 newDisplayAreaInfo: DisplayAreaInfo?, 68 t: WindowContainerTransaction? 69 ) { 70 // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and 71 // [toRotation] can be one of the [@Surface.Rotation] values. 72 if ((fromRotation % 2 == toRotation % 2)) return 73 hideEducationTooltip() 74 // TODO: b/370820018 - Update tooltip position on orientation change instead of dismissing 75 } 76 77 /** 78 * Shows education tooltip. 79 * 80 * @param tooltipViewConfig features of tooltip. 81 * @param taskId is used in the title of popup window created for the tooltip view. 82 */ 83 fun showEducationTooltip(tooltipViewConfig: TooltipEducationViewConfig, taskId: Int) { 84 hideEducationTooltip() 85 tooltipView = createEducationTooltipView(tooltipViewConfig, taskId) 86 animator = createAnimator() 87 animateShowTooltipTransition() 88 displayController.addDisplayChangingController(this) 89 } 90 91 /** Hide the current education view if visible */ 92 fun hideEducationTooltip() = animateHideTooltipTransition { cleanUp() } 93 94 /** Create education view by inflating layout provided. */ 95 private fun createEducationTooltipView( 96 tooltipViewConfig: TooltipEducationViewConfig, 97 taskId: Int, 98 ): View { 99 val tooltipView = 100 LayoutInflater.from(context) 101 .inflate( 102 tooltipViewConfig.tooltipViewLayout, /* root= */ null, /* attachToRoot= */ false) 103 .apply { 104 alpha = 0f 105 scaleX = 0f 106 scaleY = 0f 107 108 requireViewById<TextView>(R.id.tooltip_text).apply { 109 text = tooltipViewConfig.tooltipText 110 } 111 112 setOnTouchListener { _, motionEvent -> 113 if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) { 114 hideEducationTooltip() 115 tooltipViewConfig.onDismissAction() 116 true 117 } else { 118 false 119 } 120 } 121 setOnClickListener { 122 hideEducationTooltip() 123 tooltipViewConfig.onEducationClickAction() 124 } 125 setTooltipColorScheme(tooltipViewConfig.tooltipColorScheme) 126 } 127 128 val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection) 129 val tooltipViewGlobalCoordinates = 130 tooltipViewGlobalCoordinates( 131 tooltipViewGlobalCoordinates = tooltipViewConfig.tooltipViewGlobalCoordinates, 132 arrowDirection = tooltipViewConfig.arrowDirection, 133 tooltipDimen = tooltipDimens) 134 createTooltipPopupWindow( 135 taskId, tooltipViewGlobalCoordinates, tooltipDimens, tooltipView = tooltipView) 136 137 return tooltipView 138 } 139 140 /** Create animator for education transitions */ 141 private fun createAnimator(): PhysicsAnimator<View>? = 142 tooltipView?.let { 143 PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) } 144 } 145 146 /** Animate show transition for the education view */ 147 private fun animateShowTooltipTransition() { 148 animator 149 ?.spring(DynamicAnimation.ALPHA, 1f) 150 ?.spring(DynamicAnimation.SCALE_X, 1f) 151 ?.spring(DynamicAnimation.SCALE_Y, 1f) 152 ?.start() 153 } 154 155 /** Animate hide transition for the education view */ 156 private fun animateHideTooltipTransition(endActions: () -> Unit) { 157 animator 158 ?.spring(DynamicAnimation.ALPHA, 0f) 159 ?.spring(DynamicAnimation.SCALE_X, 0f) 160 ?.spring(DynamicAnimation.SCALE_Y, 0f) 161 ?.start() 162 endActions() 163 } 164 165 /** Remove education tooltip and clean up all relative properties */ 166 private fun cleanUp() { 167 tooltipView = null 168 animator = null 169 popupWindow?.releaseView() 170 popupWindow = null 171 displayController.removeDisplayChangingController(this) 172 } 173 174 private fun createTooltipPopupWindow( 175 taskId: Int, 176 tooltipViewGlobalCoordinates: Point, 177 tooltipDimen: Size, 178 tooltipView: View, 179 ) { 180 popupWindow = 181 additionalSystemViewContainerFactory.create( 182 windowManagerWrapper = 183 WindowManagerWrapper(context.getSystemService(WindowManager::class.java)), 184 taskId = taskId, 185 x = tooltipViewGlobalCoordinates.x, 186 y = tooltipViewGlobalCoordinates.y, 187 width = tooltipDimen.width, 188 height = tooltipDimen.height, 189 flags = 190 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 191 WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, 192 view = tooltipView) 193 } 194 195 private fun View.setTooltipColorScheme(tooltipColorScheme: TooltipColorScheme) { 196 requireViewById<LinearLayout>(R.id.tooltip_container).apply { 197 background.setTint(tooltipColorScheme.container) 198 } 199 requireViewById<ImageView>(R.id.arrow_icon).apply { 200 val wrappedDrawable = DrawableCompat.wrap(this.drawable) 201 DrawableCompat.setTint(wrappedDrawable, tooltipColorScheme.container) 202 if (isRtl()) scaleX = -1f 203 } 204 requireViewById<TextView>(R.id.tooltip_text).apply { setTextColor(tooltipColorScheme.text) } 205 requireViewById<ImageView>(R.id.tooltip_icon).apply { 206 val wrappedDrawable = DrawableCompat.wrap(this.drawable) 207 DrawableCompat.setTint(wrappedDrawable, tooltipColorScheme.icon) 208 } 209 } 210 211 private fun tooltipViewGlobalCoordinates( 212 tooltipViewGlobalCoordinates: Point, 213 arrowDirection: TooltipArrowDirection, 214 tooltipDimen: Size, 215 ): Point { 216 var tooltipX = tooltipViewGlobalCoordinates.x 217 var tooltipY = tooltipViewGlobalCoordinates.y 218 219 // Current values of [tooltipX]/[tooltipY] are the coordinates of tip of the arrow. 220 // Parameter x and y passed to [AdditionalSystemViewContainer] is the top left position of 221 // the window to be created. Hence we will need to move the coordinates left/up in order 222 // to position the tooltip correctly. 223 if (arrowDirection == TooltipArrowDirection.UP) { 224 // Arrow is placed at horizontal center on top edge of the tooltip. Hence decrement 225 // half of tooltip width from [tooltipX] to horizontally position the tooltip. 226 tooltipX -= tooltipDimen.width / 2 227 } else { 228 // Arrow is placed at vertical center on the left edge of the tooltip. Hence decrement 229 // half of tooltip height from [tooltipY] to vertically position the tooltip. 230 tooltipY -= tooltipDimen.height / 2 231 if (isRtl()) { 232 tooltipX -= tooltipDimen.width 233 } 234 } 235 return Point(tooltipX, tooltipY) 236 } 237 238 private fun tooltipDimens(tooltipView: View, arrowDirection: TooltipArrowDirection): Size { 239 val tooltipBackground = tooltipView.requireViewById<LinearLayout>(R.id.tooltip_container) 240 val arrowView = tooltipView.requireViewById<ImageView>(R.id.arrow_icon) 241 tooltipBackground.measure( 242 /* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED) 243 arrowView.measure(/* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED) 244 245 var desiredWidth = 246 tooltipBackground.measuredWidth + 247 2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding) 248 var desiredHeight = 249 tooltipBackground.measuredHeight + 250 2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding) 251 if (arrowDirection == TooltipArrowDirection.UP) { 252 // desiredHeight currently does not account for the height of arrow, hence adding it. 253 desiredHeight += arrowView.height 254 } else { 255 // desiredWidth currently does not account for the width of arrow, hence adding it. 256 desiredWidth += arrowView.width 257 } 258 259 return Size(desiredWidth, desiredHeight) 260 } 261 262 private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { 263 if (resourceId == Resources.ID_NULL) return 0 264 return context.resources.getDimensionPixelSize(resourceId) 265 } 266 267 private fun isRtl() = context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL 268 269 /** 270 * The configuration for education view features: 271 * 272 * @property tooltipViewLayout Layout resource ID of the view to be used for education tooltip. 273 * @property tooltipViewGlobalCoordinates Global (screen) coordinates of the tip of the tooltip 274 * arrow. 275 * @property tooltipText Text to be added to the TextView of tooltip. 276 * @property arrowDirection Direction of arrow of the tooltip. 277 * @property onEducationClickAction Lambda to be executed when the tooltip is clicked. 278 * @property onDismissAction Lambda to be executed when the tooltip is dismissed. 279 */ 280 data class TooltipEducationViewConfig( 281 @LayoutRes val tooltipViewLayout: Int, 282 val tooltipColorScheme: TooltipColorScheme, 283 val tooltipViewGlobalCoordinates: Point, 284 val tooltipText: String, 285 val arrowDirection: TooltipArrowDirection, 286 val onEducationClickAction: () -> Unit, 287 val onDismissAction: () -> Unit, 288 ) 289 290 /** 291 * Color scheme of education view: 292 * 293 * @property container Color of the container of the tooltip. 294 * @property text Text color of the [TextView] of education tooltip. 295 * @property icon Color to be filled in tooltip's icon. 296 */ 297 data class TooltipColorScheme( 298 @ColorInt val container: Int, 299 @ColorInt val text: Int, 300 @ColorInt val icon: Int, 301 ) 302 303 /** Direction of arrow of the tooltip */ 304 enum class TooltipArrowDirection { 305 UP, 306 HORIZONTAL 307 } 308 }