1 /* <lambda>null2 * 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.quickstep.views 17 18 import android.animation.AnimatorSet 19 import android.animation.ObjectAnimator 20 import android.animation.RectEvaluator 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.Drawable 26 import android.util.AttributeSet 27 import android.view.View 28 import android.view.ViewAnimationUtils 29 import android.view.ViewOutlineProvider 30 import android.widget.FrameLayout 31 import android.widget.ImageView 32 import android.widget.TextView 33 import com.android.app.animation.Interpolators 34 import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY 35 import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X 36 import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y 37 import com.android.launcher3.R 38 import com.android.launcher3.Utilities 39 import com.android.launcher3.util.MultiPropertyFactory 40 import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction 41 import com.android.launcher3.util.MultiValueAlpha 42 import com.android.quickstep.util.RecentsOrientedState 43 import kotlin.math.max 44 import kotlin.math.min 45 46 /** An icon app menu view which can be used in place of an IconView in overview TaskViews. */ 47 class IconAppChipView 48 @JvmOverloads 49 constructor( 50 context: Context, 51 attrs: AttributeSet? = null, 52 defStyleAttr: Int = 0, 53 defStyleRes: Int = 0, 54 ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon { 55 56 private var iconView: IconView? = null 57 private var iconArrowView: ImageView? = null 58 private var menuAnchorView: View? = null 59 // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name. 60 private var iconTextCollapsedView: TextView? = null 61 private var iconTextExpandedView: TextView? = null 62 63 private val backgroundRelativeLtrLocation = Rect() 64 private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation) 65 66 // Menu dimensions 67 private val collapsedMenuDefaultWidth: Int = 68 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width) 69 private val expandedMenuDefaultWidth: Int = 70 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width) 71 private val collapsedMenuDefaultHeight = 72 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height) 73 private val expandedMenuDefaultHeight = 74 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height) 75 private val iconMenuMarginTopStart = 76 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin) 77 private val menuToChipGap: Int = 78 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap) 79 80 // Background dimensions 81 private val backgroundMarginTopStart: Int = 82 resources.getDimensionPixelSize( 83 R.dimen.task_thumbnail_icon_menu_background_margin_top_start 84 ) 85 86 // Contents dimensions 87 private val appNameHorizontalMargin = 88 resources.getDimensionPixelSize( 89 R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed 90 ) 91 private val arrowMarginEnd = 92 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin) 93 private val iconViewMarginStart = 94 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin) 95 private val appIconSize = 96 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size) 97 private val arrowSize = 98 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size) 99 private val iconViewDrawableExpandedSize = 100 resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size) 101 102 private var animator: AnimatorSet? = null 103 104 private val multiValueAlpha: MultiValueAlpha = 105 MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) } 106 107 private val viewTranslationX: MultiPropertyFactory<View> = 108 MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR) 109 110 private val viewTranslationY: MultiPropertyFactory<View> = 111 MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR) 112 113 var maxWidth = Int.MAX_VALUE 114 /** 115 * Sets the maximum width of this Icon Menu. This is usually used when space is limited for 116 * split screen. 117 */ 118 set(value) { 119 // Width showing only the app icon and arrow. Max width should not be set to less than 120 // this. 121 val minMaxWidth = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd 122 field = max(value, minMaxWidth) 123 } 124 125 var status: AppChipStatus = AppChipStatus.Collapsed 126 private set 127 128 override fun onFinishInflate() { 129 super.onFinishInflate() 130 iconView = findViewById(R.id.icon_view) 131 iconTextCollapsedView = findViewById(R.id.icon_text_collapsed) 132 iconTextExpandedView = findViewById(R.id.icon_text_expanded) 133 iconArrowView = findViewById(R.id.icon_arrow) 134 menuAnchorView = findViewById(R.id.icon_view_menu_anchor) 135 } 136 137 override fun setText(text: CharSequence?) { 138 iconTextCollapsedView?.text = text 139 iconTextExpandedView?.text = text 140 } 141 142 override fun getDrawable(): Drawable? = iconView?.drawable 143 144 override fun setDrawable(icon: Drawable?) { 145 iconView?.drawable = icon 146 } 147 148 override fun setDrawableSize(iconWidth: Int, iconHeight: Int) { 149 iconView?.setDrawableSize(iconWidth, iconHeight) 150 } 151 152 override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) { 153 val orientationHandler = orientationState.orientationHandler 154 // Layout params for anchor view 155 val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams 156 anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap 157 menuAnchorView!!.layoutParams = anchorLayoutParams 158 159 // Layout Params for the Menu View (this) 160 val iconMenuParams = layoutParams as LayoutParams 161 iconMenuParams.width = expandedMenuDefaultWidth 162 iconMenuParams.height = expandedMenuDefaultHeight 163 orientationHandler.setIconAppChipMenuParams( 164 this, 165 iconMenuParams, 166 iconMenuMarginTopStart, 167 iconMenuMarginTopStart, 168 ) 169 layoutParams = iconMenuParams 170 171 // Layout params for the background 172 val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds() 173 backgroundRelativeLtrLocation.set(collapsedBackgroundBounds) 174 outlineProvider = 175 object : ViewOutlineProvider() { 176 val mRtlAppliedOutlineBounds: Rect = Rect() 177 178 override fun getOutline(view: View, outline: Outline) { 179 mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation) 180 if (isLayoutRtl) { 181 val width = width 182 mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right 183 mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left 184 } 185 outline.setRoundRect( 186 mRtlAppliedOutlineBounds, 187 mRtlAppliedOutlineBounds.height() / 2f, 188 ) 189 } 190 } 191 192 // Layout Params for the Icon View 193 val iconParams = iconView!!.layoutParams as LayoutParams 194 val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart 195 orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent) 196 197 iconView!!.layoutParams = iconParams 198 iconView!!.setDrawableSize(appIconSize, appIconSize) 199 200 // Layout Params for the collapsed Icon Text View 201 val textMarginStart = 202 iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMargin 203 val iconTextCollapsedParams = iconTextCollapsedView!!.layoutParams as LayoutParams 204 orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart) 205 val collapsedTextWidth = 206 (collapsedBackgroundBounds.width() - 207 iconViewMarginStart - 208 appIconSize - 209 arrowSize - 210 appNameHorizontalMargin - 211 arrowMarginEnd) 212 iconTextCollapsedParams.width = collapsedTextWidth 213 iconTextCollapsedView!!.layoutParams = iconTextCollapsedParams 214 iconTextCollapsedView!!.alpha = 1f 215 216 // Layout Params for the expanded Icon Text View 217 val iconTextExpandedParams = iconTextExpandedView!!.layoutParams as LayoutParams 218 orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart) 219 iconTextExpandedView!!.layoutParams = iconTextExpandedParams 220 iconTextExpandedView!!.alpha = 0f 221 iconTextExpandedView!!.setRevealClip( 222 true, 223 0f, 224 appIconSize / 2f, 225 collapsedTextWidth.toFloat(), 226 ) 227 228 // Layout Params for the Icon Arrow View 229 val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams 230 val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize 231 orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart) 232 iconArrowView!!.pivotY = iconArrowParams.height / 2f 233 iconArrowView!!.layoutParams = iconArrowParams 234 235 // This method is called twice sometimes (like when rotating split tasks). It is called 236 // once before onMeasure and onLayout, and again after onMeasure but before onLayout with 237 // a new width. This happens because we update widths on rotation and on measure of 238 // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if 239 // it has just measured, so we explicitly call it here. 240 measure( 241 MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY), 242 MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY), 243 ) 244 } 245 246 override fun setIconColorTint(color: Int, amount: Float) { 247 // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu. 248 val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR) 249 multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha 250 } 251 252 override fun setContentAlpha(alpha: Float) { 253 multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha 254 } 255 256 override fun setModalAlpha(alpha: Float) { 257 multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha 258 } 259 260 override fun setFlexSplitAlpha(alpha: Float) { 261 multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha 262 } 263 264 override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0 265 266 override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0 267 268 /** Gets the view split x-axis translation */ 269 fun getSplitTranslationX(): MultiPropertyFactory<View>.MultiProperty = 270 viewTranslationX.get(INDEX_SPLIT_TRANSLATION) 271 272 /** 273 * Sets the view split x-axis translation 274 * 275 * @param value x-axis translation 276 */ 277 fun setSplitTranslationX(value: Float) { 278 getSplitTranslationX().value = value 279 } 280 281 /** Gets the view split y-axis translation */ 282 fun getSplitTranslationY(): MultiPropertyFactory<View>.MultiProperty = 283 viewTranslationY[INDEX_SPLIT_TRANSLATION] 284 285 /** 286 * Sets the view split y-axis translation 287 * 288 * @param value y-axis translation 289 */ 290 fun setSplitTranslationY(value: Float) { 291 getSplitTranslationY().value = value 292 } 293 294 /** Gets the menu x-axis translation for split task */ 295 fun getMenuTranslationX(): MultiPropertyFactory<View>.MultiProperty = 296 viewTranslationX[INDEX_MENU_TRANSLATION] 297 298 /** Gets the menu y-axis translation for split task */ 299 fun getMenuTranslationY(): MultiPropertyFactory<View>.MultiProperty = 300 viewTranslationY[INDEX_MENU_TRANSLATION] 301 302 internal fun revealAnim(isRevealing: Boolean, animated: Boolean = true) { 303 cancelInProgressAnimations() 304 val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds() 305 val expandedBackgroundBounds = getExpandedBackgroundLtrBounds() 306 val initialBackground = Rect(backgroundRelativeLtrLocation) 307 animator = AnimatorSet() 308 309 if (isRevealing) { 310 val isRtl = isLayoutRtl 311 bringToFront() 312 // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu 313 val expandedTextRevealAnim = 314 ViewAnimationUtils.createCircularReveal( 315 iconTextExpandedView, 316 0, 317 iconTextExpandedView!!.height / 2, 318 iconTextCollapsedView!!.width.toFloat(), 319 iconTextExpandedView!!.width.toFloat(), 320 ) 321 // Animate background clipping 322 val backgroundAnimator = 323 ValueAnimator.ofObject( 324 backgroundAnimationRectEvaluator, 325 initialBackground, 326 expandedBackgroundBounds, 327 ) 328 backgroundAnimator.addUpdateListener { invalidateOutline() } 329 330 val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat() 331 val arrowTranslationX = 332 (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat() 333 val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMargin 334 val iconCenterToTextExpanded = 335 iconViewDrawableExpandedSize / 2f + appNameHorizontalMargin 336 val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed 337 338 val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX 339 val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX 340 341 animator!!.playTogether( 342 expandedTextRevealAnim, 343 backgroundAnimator, 344 ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling), 345 ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling), 346 ObjectAnimator.ofFloat( 347 iconTextCollapsedView, 348 TRANSLATION_X, 349 textTranslationXWithRtl, 350 ), 351 ObjectAnimator.ofFloat( 352 iconTextExpandedView, 353 TRANSLATION_X, 354 textTranslationXWithRtl, 355 ), 356 ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 0f), 357 ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 1f), 358 ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl), 359 ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f), 360 ) 361 animator!!.duration = MENU_BACKGROUND_REVEAL_DURATION.toLong() 362 status = AppChipStatus.Expanded 363 } else { 364 // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu 365 val expandedTextClipAnim = 366 ViewAnimationUtils.createCircularReveal( 367 iconTextExpandedView, 368 0, 369 iconTextExpandedView!!.height / 2, 370 iconTextExpandedView!!.width.toFloat(), 371 iconTextCollapsedView!!.width.toFloat(), 372 ) 373 374 // Animate background clipping 375 val backgroundAnimator = 376 ValueAnimator.ofObject( 377 backgroundAnimationRectEvaluator, 378 initialBackground, 379 collapsedBackgroundBounds, 380 ) 381 backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? -> 382 invalidateOutline() 383 } 384 385 animator!!.playTogether( 386 expandedTextClipAnim, 387 backgroundAnimator, 388 ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f), 389 ObjectAnimator.ofFloat(iconTextCollapsedView, TRANSLATION_X, 0f), 390 ObjectAnimator.ofFloat(iconTextExpandedView, TRANSLATION_X, 0f), 391 ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 1f), 392 ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 0f), 393 ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f), 394 ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f), 395 ) 396 animator!!.duration = MENU_BACKGROUND_HIDE_DURATION.toLong() 397 status = AppChipStatus.Collapsed 398 } 399 400 if (!animated) animator!!.duration = 0 401 animator!!.interpolator = Interpolators.EMPHASIZED 402 animator!!.start() 403 } 404 405 private fun getCollapsedBackgroundLtrBounds(): Rect { 406 val bounds = 407 Rect(0, 0, min(maxWidth, collapsedMenuDefaultWidth), collapsedMenuDefaultHeight) 408 bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart) 409 return bounds 410 } 411 412 private fun getExpandedBackgroundLtrBounds() = 413 Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight) 414 415 private fun cancelInProgressAnimations() { 416 // We null the `AnimatorSet` because it holds references to the `Animators` which aren't 417 // expecting to be mutable and will cause a crash if they are re-used. 418 if (animator != null && animator!!.isStarted) { 419 animator!!.cancel() 420 animator = null 421 } 422 } 423 424 override fun focusSearch(direction: Int): View? { 425 if (mParent == null) return null 426 return when (direction) { 427 FOCUS_RIGHT, 428 FOCUS_DOWN -> mParent.focusSearch(this, View.FOCUS_FORWARD) 429 FOCUS_UP, 430 FOCUS_LEFT -> mParent.focusSearch(this, View.FOCUS_BACKWARD) 431 else -> super.focusSearch(direction) 432 } 433 } 434 435 fun reset() { 436 setText(null) 437 setDrawable(null) 438 } 439 440 override fun asView(): View = this 441 442 enum class AppChipStatus { 443 Expanded, 444 Collapsed, 445 } 446 447 private companion object { 448 private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b } 449 450 private const val MENU_BACKGROUND_REVEAL_DURATION = 417 451 private const val MENU_BACKGROUND_HIDE_DURATION = 333 452 453 private const val NUM_ALPHA_CHANNELS = 4 454 private const val INDEX_CONTENT_ALPHA = 0 455 private const val INDEX_COLOR_FILTER_ALPHA = 1 456 private const val INDEX_MODAL_ALPHA = 2 457 /** Used to hide the app chip for 90:10 flex split. */ 458 private const val INDEX_MINIMUM_RATIO_ALPHA = 3 459 460 private const val INDEX_SPLIT_TRANSLATION = 0 461 private const val INDEX_MENU_TRANSLATION = 1 462 private const val INDEX_COUNT_TRANSLATION = 2 463 } 464 } 465