1 /* <lambda>null2 * Copyright (C) 2018 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.quickstep.views 17 18 import android.animation.Animator 19 import android.animation.AnimatorSet 20 import android.animation.ObjectAnimator 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.graphics.Outline 24 import android.graphics.Rect 25 import android.graphics.drawable.ShapeDrawable 26 import android.graphics.drawable.shapes.RectShape 27 import android.util.AttributeSet 28 import android.view.Gravity 29 import android.view.KeyEvent 30 import android.view.MotionEvent 31 import android.view.View 32 import android.view.ViewOutlineProvider 33 import android.widget.LinearLayout 34 import android.widget.TextView 35 import androidx.core.content.res.ResourcesCompat 36 import com.android.app.animation.Interpolators 37 import com.android.launcher3.AbstractFloatingView 38 import com.android.launcher3.Flags.enableOverviewIconMenu 39 import com.android.launcher3.Flags.enableRefactorTaskThumbnail 40 import com.android.launcher3.R 41 import com.android.launcher3.anim.AnimationSuccessListener 42 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider 43 import com.android.launcher3.popup.SystemShortcut 44 import com.android.launcher3.util.MultiPropertyFactory 45 import com.android.launcher3.util.SplitConfigurationOptions 46 import com.android.launcher3.views.BaseDragLayer 47 import com.android.quickstep.TaskOverlayFactory 48 import com.android.quickstep.TaskUtils 49 import com.android.quickstep.util.TaskCornerRadius 50 import java.util.function.Consumer 51 import kotlin.math.max 52 53 /** Contains options for a recent task when long-pressing its icon. */ 54 class TaskMenuView 55 @JvmOverloads 56 constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int = 0) : 57 AbstractFloatingView(context, attrs, defStyleAttr) { 58 private val recentsViewContainer: RecentsViewContainer = 59 RecentsViewContainer.containerFromContext(context) 60 private val tempRect = Rect() 61 private val taskName: TextView by lazy { findViewById(R.id.task_name) } 62 private val optionLayout: LinearLayout by lazy { findViewById(R.id.menu_option_layout) } 63 private var openCloseAnimator: AnimatorSet? = null 64 private var revealAnimator: ValueAnimator? = null 65 private var onClosingStartCallback: Runnable? = null 66 private lateinit var taskView: TaskView 67 private lateinit var taskContainer: TaskContainer 68 private var menuTranslationXBeforeOpen = 0f 69 private var menuTranslationYBeforeOpen = 0f 70 71 // Spaced claimed below Overview (taskbar and insets) 72 private val taskbarTop by lazy { 73 recentsViewContainer.deviceProfile.heightPx - 74 recentsViewContainer.deviceProfile.overviewActionsClaimedSpaceBelow 75 } 76 private val minMenuTop by lazy { taskContainer.iconView.height.toFloat() } 77 // TODO(b/401476868): Replace overviewRowSpacing with correct margin to the taskbarTop. 78 private val maxMenuBottom by lazy { 79 (taskbarTop - recentsViewContainer.deviceProfile.overviewRowSpacing).toFloat() 80 } 81 82 init { 83 clipToOutline = true 84 } 85 86 override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { 87 if (ev.action == MotionEvent.ACTION_DOWN) { 88 if (!recentsViewContainer.dragLayer.isEventOverView(this, ev)) { 89 // TODO: log this once we have a new container type for it? 90 animateOpenOrClosed(true) 91 return true 92 } 93 } 94 return false 95 } 96 97 override fun handleClose(animate: Boolean) { 98 animateOpenOrClosed(true, animated = false) 99 } 100 101 override fun isOfType(type: Int): Boolean = (type and TYPE_TASK_MENU) != 0 102 103 override fun getOutlineProvider(): ViewOutlineProvider = 104 object : ViewOutlineProvider() { 105 override fun getOutline(view: View, outline: Outline) { 106 outline.setRoundRect( 107 0, 108 0, 109 view.width, 110 view.height, 111 TaskCornerRadius.get(view.context), 112 ) 113 } 114 } 115 116 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 117 var heightMeasure = heightMeasureSpec 118 val maxMenuHeight = calculateMaxHeight() 119 if (MeasureSpec.getSize(heightMeasure) > maxMenuHeight) { 120 heightMeasure = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST) 121 } 122 super.onMeasure(widthMeasureSpec, heightMeasure) 123 } 124 125 fun onRotationChanged() { 126 openCloseAnimator?.let { if (it.isRunning) it.end() } 127 if (mIsOpen) { 128 optionLayout.removeAllViews() 129 if (enableOverviewIconMenu() || !populateAndLayoutMenu()) { 130 close(false) 131 } 132 } 133 } 134 135 private fun populateAndShowForTask(taskContainer: TaskContainer): Boolean { 136 if (isAttachedToWindow) return false 137 recentsViewContainer.dragLayer.addView(this) 138 taskView = taskContainer.taskView 139 this.taskContainer = taskContainer 140 if (!populateAndLayoutMenu()) return false 141 post { this.animateOpen() } 142 return true 143 } 144 145 /** @return true if successfully able to populate task view menu, false otherwise */ 146 private fun populateAndLayoutMenu(): Boolean { 147 addMenuOptions(taskContainer) 148 orientAroundTaskView(taskContainer) 149 return true 150 } 151 152 private fun addMenuOptions(taskContainer: TaskContainer) { 153 if (enableOverviewIconMenu()) { 154 removeView(taskName) 155 } else { 156 taskName.text = TaskUtils.getTitle(context, taskContainer.task) 157 taskName.setOnClickListener { close(true) } 158 } 159 TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer) 160 .forEach(Consumer { menuOption: SystemShortcut<*> -> this.addMenuOption(menuOption) }) 161 } 162 163 private fun addMenuOption(menuOption: SystemShortcut<*>) { 164 val menuOptionView = 165 recentsViewContainer.layoutInflater.inflate(R.layout.task_view_menu_option, this, false) 166 as LinearLayout 167 if (enableOverviewIconMenu()) { 168 menuOptionView.background = 169 ResourcesCompat.getDrawable( 170 resources, 171 R.drawable.app_chip_menu_item_bg, 172 context.theme, 173 ) 174 menuOptionView.foreground = 175 ResourcesCompat.getDrawable( 176 resources, 177 R.drawable.app_chip_menu_item_fg, 178 context.theme, 179 ) 180 } 181 menuOption.setIconAndLabelFor( 182 menuOptionView.findViewById(R.id.icon), 183 menuOptionView.findViewById(R.id.text), 184 ) 185 val lp = menuOptionView.layoutParams as LayoutParams 186 taskView.pagedOrientationHandler.setLayoutParamsForTaskMenuOptionItem( 187 lp, 188 menuOptionView, 189 recentsViewContainer.deviceProfile, 190 ) 191 // Set an onClick listener on each menu option. The onClick method is responsible for 192 // ending LiveTile mode on the thumbnail if needed. 193 menuOptionView.setOnClickListener { v: View? -> menuOption.onClick(v) } 194 optionLayout.addView(menuOptionView) 195 } 196 197 private fun orientAroundTaskView(taskContainer: TaskContainer) { 198 val recentsView = recentsViewContainer.getOverviewPanel<RecentsView<*, *>>() 199 val orientationHandler = recentsView.pagedOrientationHandler 200 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) 201 202 // Get Position 203 val deviceProfile = recentsViewContainer.deviceProfile 204 recentsViewContainer.dragLayer.getDescendantRectRelativeToSelf( 205 if (enableOverviewIconMenu()) iconView.findViewById(R.id.icon_view_menu_anchor) 206 else taskContainer.snapshotView, 207 tempRect, 208 ) 209 val insets = recentsViewContainer.dragLayer.getInsets() 210 val params = layoutParams as BaseDragLayer.LayoutParams 211 params.width = 212 orientationHandler.getTaskMenuWidth( 213 taskContainer.snapshotView, 214 deviceProfile, 215 taskContainer.stagePosition, 216 ) 217 // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start 218 params.gravity = Gravity.LEFT 219 layoutParams = params 220 scaleX = taskView.scaleX 221 scaleY = taskView.scaleY 222 223 // Set divider spacing 224 val divider = ShapeDrawable(RectShape()) 225 divider.paint.color = resources.getColor(android.R.color.transparent) 226 val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt() 227 optionLayout.showDividers = 228 if (enableOverviewIconMenu()) SHOW_DIVIDER_NONE else SHOW_DIVIDER_MIDDLE 229 230 optionLayout.background = 231 if (enableOverviewIconMenu()) { 232 ResourcesCompat.getDrawable(resources, R.drawable.app_chip_menu_bg, context.theme) 233 } else { 234 null 235 } 236 237 orientationHandler.setTaskOptionsMenuLayoutOrientation( 238 deviceProfile, 239 optionLayout, 240 dividerSpacing, 241 divider, 242 ) 243 val thumbnailAlignedX = (tempRect.left - insets.left).toFloat() 244 val thumbnailAlignedY = (tempRect.top - insets.top).toFloat() 245 246 // Changing pivot to make computations easier 247 // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set, 248 // which would render the X and Y position set here incorrect 249 pivotX = 0f 250 pivotY = 0f 251 rotation = orientationHandler.degreesRotated 252 253 if (enableOverviewIconMenu()) { 254 elevation = resources.getDimension(R.dimen.task_thumbnail_icon_menu_elevation) 255 translationX = thumbnailAlignedX 256 translationY = thumbnailAlignedY 257 } else { 258 // Margin that insets the menuView inside the taskView 259 val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin) 260 translationX = 261 orientationHandler.getTaskMenuX( 262 thumbnailAlignedX, 263 this.taskContainer.snapshotView, 264 deviceProfile, 265 taskInsetMargin, 266 iconView, 267 ) 268 translationY = 269 orientationHandler.getTaskMenuY( 270 thumbnailAlignedY, 271 this.taskContainer.snapshotView, 272 this.taskContainer.stagePosition, 273 this, 274 taskInsetMargin, 275 iconView, 276 ) 277 } 278 } 279 280 private fun animateOpen() { 281 menuTranslationYBeforeOpen = translationY 282 menuTranslationXBeforeOpen = translationX 283 animateOpenOrClosed(false) 284 mIsOpen = true 285 } 286 287 private val iconView: View 288 get() = taskContainer.iconView.asView() 289 290 private fun animateOpenOrClosed(closing: Boolean, animated: Boolean = true) { 291 openCloseAnimator?.let { if (it.isRunning) it.cancel() } 292 openCloseAnimator = AnimatorSet() 293 // If we're opening, we just start from the beginning as a new `TaskMenuView` is created 294 // each time we do the open animation so there will never be a partial value here. 295 var revealAnimationStartProgress = 0f 296 if (closing && revealAnimator != null) { 297 revealAnimationStartProgress = 1f - revealAnimator!!.animatedFraction 298 } 299 revealAnimator = 300 createOpenCloseOutlineProvider() 301 .createRevealAnimator(this, closing, revealAnimationStartProgress) 302 revealAnimator!!.interpolator = 303 if (enableOverviewIconMenu()) Interpolators.EMPHASIZED else Interpolators.DECELERATE 304 val openCloseAnimatorBuilder = openCloseAnimator!!.play(revealAnimator) 305 if (enableOverviewIconMenu()) { 306 animateOpenOrCloseAppChip(closing, openCloseAnimatorBuilder) 307 } 308 openCloseAnimatorBuilder.with( 309 ObjectAnimator.ofFloat(this, ALPHA, (if (closing) 0 else 1).toFloat()) 310 ) 311 if (enableRefactorTaskThumbnail()) { 312 revealAnimator?.addUpdateListener { animation: ValueAnimator -> 313 val animatedFraction = animation.animatedFraction 314 val openProgress = if (closing) (1 - animatedFraction) else animatedFraction 315 taskContainer.updateMenuOpenProgress(openProgress) 316 } 317 } else { 318 openCloseAnimatorBuilder.with( 319 ObjectAnimator.ofFloat( 320 taskContainer.thumbnailViewDeprecated, 321 TaskThumbnailViewDeprecated.DIM_ALPHA, 322 if (closing) 0f else TaskView.MAX_PAGE_SCRIM_ALPHA, 323 ) 324 ) 325 } 326 openCloseAnimator!!.addListener( 327 object : AnimationSuccessListener() { 328 override fun onAnimationStart(animation: Animator) { 329 visibility = VISIBLE 330 if (closing) onClosingStartCallback?.run() 331 } 332 333 override fun onAnimationSuccess(animator: Animator) { 334 if (closing) closeComplete() 335 } 336 } 337 ) 338 val animationDuration = 339 when { 340 animated && closing -> REVEAL_CLOSE_DURATION 341 animated && !closing -> REVEAL_OPEN_DURATION 342 else -> 0L 343 } 344 openCloseAnimator!!.setDuration(animationDuration) 345 openCloseAnimator!!.start() 346 } 347 348 private fun TaskView.isOnGridBottomRow(): Boolean = 349 (recentsViewContainer.getOverviewPanel<View>() as RecentsView<*, *>).isOnGridBottomRow(this) 350 351 private fun closeComplete() { 352 mIsOpen = false 353 recentsViewContainer.dragLayer.removeView(this) 354 revealAnimator = null 355 } 356 357 private fun createOpenCloseOutlineProvider(): RoundedRectRevealOutlineProvider { 358 val radius = TaskCornerRadius.get(mContext) 359 val fromRect = 360 Rect( 361 if (enableOverviewIconMenu() && isLayoutRtl) width else 0, 362 0, 363 if (enableOverviewIconMenu() && !isLayoutRtl) 0 else width, 364 0, 365 ) 366 val toRect = Rect(0, 0, width, height) 367 return RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect) 368 } 369 370 /** 371 * Calculates max height based on how much space we have available. If not enough space then the 372 * view will scroll. The maximum menu size will sit inside the task with a margin on the top and 373 * bottom. 374 */ 375 private fun calculateMaxHeight(): Int = 376 taskView.pagedOrientationHandler.getTaskMenuHeight( 377 taskInsetMargin = resources.getDimension(R.dimen.task_card_margin), // taskInsetMargin 378 deviceProfile = recentsViewContainer.deviceProfile, 379 taskMenuX = translationX, 380 taskMenuY = 381 // Bottom menu can translate up to show more options. So we use the min 382 // translation allowed to calculate its max height. 383 if (enableOverviewIconMenu() && taskView.isOnGridBottomRow()) minMenuTop 384 else translationY, 385 ) 386 387 private fun setOnClosingStartCallback(onClosingStartCallback: Runnable?) { 388 this.onClosingStartCallback = onClosingStartCallback 389 } 390 391 private fun animateOpenOrCloseAppChip(closing: Boolean, animatorBuilder: AnimatorSet.Builder) { 392 val iconAppChip = taskContainer.iconView.asView() as IconAppChipView 393 394 // Animate menu up for enough room to display full menu when task on bottom row. 395 var additionalTranslationY = 0f 396 if (taskView.isOnGridBottomRow()) { 397 val currentMenuBottom: Float = menuTranslationYBeforeOpen + height 398 additionalTranslationY = 399 if (currentMenuBottom < maxMenuBottom) 0f 400 // Translate menu up for enough room to display full menu when task on bottom row. 401 else maxMenuBottom - currentMenuBottom 402 403 val currentMenuTop = menuTranslationYBeforeOpen + additionalTranslationY 404 // If it translate above the min accepted, it translates to the top of the screen 405 if (currentMenuTop < minMenuTop) { 406 // It subtracts the menuTranslation to make it 0 (top of the screen) + chip size. 407 additionalTranslationY = -menuTranslationYBeforeOpen + minMenuTop 408 } 409 } 410 411 val translationYAnim = 412 ObjectAnimator.ofFloat( 413 this, 414 TRANSLATION_Y, 415 if (closing) menuTranslationYBeforeOpen 416 else menuTranslationYBeforeOpen + additionalTranslationY, 417 ) 418 translationYAnim.interpolator = Interpolators.EMPHASIZED 419 animatorBuilder.with(translationYAnim) 420 421 val menuTranslationYAnim: ObjectAnimator = 422 ObjectAnimator.ofFloat( 423 iconAppChip.getMenuTranslationY(), 424 MultiPropertyFactory.MULTI_PROPERTY_VALUE, 425 if (closing) 0f else additionalTranslationY, 426 ) 427 menuTranslationYAnim.interpolator = Interpolators.EMPHASIZED 428 animatorBuilder.with(menuTranslationYAnim) 429 430 var additionalTranslationX = 0f 431 if ( 432 taskContainer.stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT 433 ) { 434 // Animate menu and icon when split task would display off the side of the screen. 435 additionalTranslationX = 436 max( 437 (translationX + width - 438 (recentsViewContainer.deviceProfile.widthPx - 439 resources.getDimensionPixelSize( 440 R.dimen.task_menu_edge_padding 441 ) * 2)) 442 .toDouble(), 443 0.0, 444 ) 445 .toFloat() 446 } 447 448 val translationXAnim = 449 ObjectAnimator.ofFloat( 450 this, 451 TRANSLATION_X, 452 if (closing) menuTranslationXBeforeOpen 453 else menuTranslationXBeforeOpen - additionalTranslationX, 454 ) 455 translationXAnim.interpolator = Interpolators.EMPHASIZED 456 animatorBuilder.with(translationXAnim) 457 458 val menuTranslationXAnim: ObjectAnimator = 459 ObjectAnimator.ofFloat( 460 iconAppChip.getMenuTranslationX(), 461 MultiPropertyFactory.MULTI_PROPERTY_VALUE, 462 if (closing) 0f else -additionalTranslationX, 463 ) 464 menuTranslationXAnim.interpolator = Interpolators.EMPHASIZED 465 animatorBuilder.with(menuTranslationXAnim) 466 } 467 468 override fun dispatchKeyEvent(event: KeyEvent): Boolean { 469 if (enableOverviewIconMenu()) { 470 if (event.action != KeyEvent.ACTION_DOWN) return super.dispatchKeyEvent(event) 471 472 val isFirstMenuOptionFocused = optionLayout.indexOfChild(optionLayout.focusedChild) == 0 473 val isLastMenuOptionFocused = 474 optionLayout.indexOfChild(optionLayout.focusedChild) == optionLayout.childCount - 1 475 if ( 476 (isLastMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_DOWN) || 477 (isFirstMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_UP) 478 ) { 479 iconView.requestFocus() 480 return true 481 } 482 } 483 return super.dispatchKeyEvent(event) 484 } 485 486 companion object { 487 private val REVEAL_OPEN_DURATION = if (enableOverviewIconMenu()) 417L else 150L 488 private val REVEAL_CLOSE_DURATION = if (enableOverviewIconMenu()) 333L else 100L 489 490 /** Show a task menu for the given taskContainer. */ 491 /** Show a task menu for the given taskContainer. */ 492 @JvmOverloads 493 fun showForTask( 494 taskContainer: TaskContainer, 495 onClosingStartCallback: Runnable? = null, 496 ): Boolean { 497 val container: RecentsViewContainer = 498 RecentsViewContainer.containerFromContext(taskContainer.taskView.context) 499 val taskMenuView = 500 container.layoutInflater.inflate(R.layout.task_menu, container.dragLayer, false) 501 as TaskMenuView 502 taskMenuView.setOnClosingStartCallback(onClosingStartCallback) 503 return taskMenuView.populateAndShowForTask(taskContainer) 504 } 505 } 506 } 507