1 /* <lambda>null2 * Copyright (C) 2021 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.quickstep.views 18 19 import android.animation.AnimatorSet 20 import android.animation.ObjectAnimator 21 import android.content.Context 22 import android.graphics.Rect 23 import android.graphics.drawable.ShapeDrawable 24 import android.graphics.drawable.shapes.RectShape 25 import android.util.AttributeSet 26 import android.view.Gravity 27 import android.view.MotionEvent 28 import android.view.View 29 import android.view.ViewGroup 30 import android.widget.FrameLayout 31 import android.widget.LinearLayout 32 import com.android.launcher3.DeviceProfile 33 import com.android.launcher3.InsettableFrameLayout 34 import com.android.launcher3.R 35 import com.android.launcher3.popup.ArrowPopup 36 import com.android.launcher3.popup.RoundedArrowDrawable 37 import com.android.launcher3.popup.SystemShortcut 38 import com.android.launcher3.util.Themes 39 import com.android.quickstep.TaskOverlayFactory 40 41 class TaskMenuViewWithArrow<T> : ArrowPopup<T> where T : RecentsViewContainer, T : Context { 42 companion object { 43 const val TAG = "TaskMenuViewWithArrow" 44 45 fun showForTask( 46 taskContainer: TaskContainer, 47 alignedOptionIndex: Int = 0, 48 onClosedCallback: Runnable? = null 49 ): Boolean { 50 val container: RecentsViewContainer = 51 RecentsViewContainer.containerFromContext(taskContainer.taskView.context) 52 val taskMenuViewWithArrow = 53 container.layoutInflater.inflate( 54 R.layout.task_menu_with_arrow, 55 container.dragLayer, 56 false 57 ) as TaskMenuViewWithArrow<*> 58 59 return taskMenuViewWithArrow.populateAndShowForTask( 60 taskContainer, 61 alignedOptionIndex, 62 onClosedCallback 63 ) 64 } 65 } 66 67 constructor(context: Context) : super(context) 68 69 constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 70 71 constructor( 72 context: Context, 73 attrs: AttributeSet, 74 defStyleAttr: Int 75 ) : super(context, attrs, defStyleAttr) 76 77 init { 78 clipToOutline = true 79 80 shouldScaleArrow = true 81 mIsArrowRotated = true 82 // This synchronizes the arrow and menu to open at the same time 83 mOpenChildFadeStartDelay = mOpenFadeStartDelay 84 mOpenChildFadeDuration = mOpenFadeDuration 85 mCloseFadeStartDelay = mCloseChildFadeStartDelay 86 mCloseFadeDuration = mCloseChildFadeDuration 87 } 88 89 private var alignedOptionIndex: Int = 0 90 private val extraSpaceForRowAlignment: Int 91 get() = optionMeasuredHeight * alignedOptionIndex 92 93 private val menuPaddingEnd = context.resources.getDimensionPixelSize(R.dimen.task_card_margin) 94 95 private lateinit var taskView: TaskView 96 private lateinit var optionLayout: LinearLayout 97 private lateinit var taskContainer: TaskContainer 98 99 private var optionMeasuredHeight = 0 100 private val arrowHorizontalPadding: Int 101 get() = 102 if (taskView.isLargeTile) 103 resources.getDimensionPixelSize(R.dimen.task_menu_horizontal_padding) 104 else 0 105 106 private var iconView: IconView? = null 107 private var scrim: View? = null 108 private val scrimAlpha = 0.8f 109 private var onClosedCallback: Runnable? = null 110 111 override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0 112 113 override fun getTargetObjectLocation(outPos: Rect?) { 114 popupContainer.getDescendantRectRelativeToSelf(taskContainer.iconView.asView(), outPos) 115 } 116 117 override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { 118 if (ev?.action == MotionEvent.ACTION_DOWN) { 119 if (!popupContainer.isEventOverView(this, ev)) { 120 close(true) 121 return true 122 } 123 } 124 return false 125 } 126 127 override fun onFinishInflate() { 128 super.onFinishInflate() 129 optionLayout = requireViewById(R.id.menu_option_layout) 130 } 131 132 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 133 val maxMenuHeight: Int = calculateMaxHeight() 134 val newHeightMeasureSpec = 135 if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) { 136 MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST) 137 } else heightMeasureSpec 138 super.onMeasure(widthMeasureSpec, newHeightMeasureSpec) 139 } 140 141 private fun calculateMaxHeight(): Int { 142 val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin) 143 return taskView.pagedOrientationHandler.getTaskMenuHeight( 144 taskInsetMargin, 145 mActivityContext.deviceProfile, 146 translationX, 147 translationY 148 ) 149 } 150 151 private fun populateAndShowForTask( 152 taskContainer: TaskContainer, 153 alignedOptionIndex: Int, 154 onClosedCallback: Runnable? 155 ): Boolean { 156 if (isAttachedToWindow) { 157 return false 158 } 159 160 taskView = taskContainer.taskView 161 this.taskContainer = taskContainer 162 this.alignedOptionIndex = alignedOptionIndex 163 this.onClosedCallback = onClosedCallback 164 if (!populateMenu()) return false 165 addScrim() 166 show() 167 return true 168 } 169 170 private fun addScrim() { 171 scrim = 172 View(context).apply { 173 layoutParams = 174 FrameLayout.LayoutParams( 175 FrameLayout.LayoutParams.MATCH_PARENT, 176 FrameLayout.LayoutParams.MATCH_PARENT 177 ) 178 setBackgroundColor(Themes.getAttrColor(context, R.attr.overviewScrimColor)) 179 alpha = 0f 180 } 181 popupContainer.addView(scrim) 182 } 183 184 /** @return true if successfully able to populate task view menu, false otherwise */ 185 private fun populateMenu(): Boolean { 186 // Icon may not be loaded 187 if (taskContainer.iconView.drawable == null) return false 188 189 addMenuOptions() 190 return optionLayout.childCount > 0 191 } 192 193 private fun addMenuOptions() { 194 // Add the options 195 TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer).forEach { 196 this.addMenuOption(it) 197 } 198 199 // Add the spaces between items 200 val divider = ShapeDrawable(RectShape()) 201 divider.paint.color = resources.getColor(android.R.color.transparent) 202 val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt() 203 optionLayout.showDividers = SHOW_DIVIDER_MIDDLE 204 205 // Set the orientation, which makes the menu show 206 val recentsView: RecentsView<*, *> = mActivityContext.getOverviewPanel() 207 val orientationHandler = recentsView.pagedOrientationHandler 208 val deviceProfile: DeviceProfile = mActivityContext.deviceProfile 209 orientationHandler.setTaskOptionsMenuLayoutOrientation( 210 deviceProfile, 211 optionLayout, 212 dividerSpacing, 213 divider 214 ) 215 } 216 217 private fun addMenuOption(menuOption: SystemShortcut<*>) { 218 val menuOptionView = 219 mActivityContext.layoutInflater.inflate(R.layout.task_view_menu_option, this, false) 220 as LinearLayout 221 menuOption.setIconAndLabelFor( 222 menuOptionView.requireViewById(R.id.icon), 223 menuOptionView.requireViewById(R.id.text) 224 ) 225 val lp = menuOptionView.layoutParams as LayoutParams 226 lp.width = LayoutParams.MATCH_PARENT 227 menuOptionView.setPaddingRelative( 228 menuOptionView.paddingStart, 229 menuOptionView.paddingTop, 230 menuPaddingEnd, 231 menuOptionView.paddingBottom 232 ) 233 menuOptionView.setOnClickListener { view: View? -> menuOption.onClick(view) } 234 optionLayout.addView(menuOptionView) 235 } 236 237 override fun assignMarginsAndBackgrounds(viewGroup: ViewGroup) { 238 assignMarginsAndBackgrounds( 239 this, 240 Themes.getAttrColor(context, com.android.internal.R.attr.colorSurface) 241 ) 242 } 243 244 override fun onCreateOpenAnimation(anim: AnimatorSet) { 245 scrim?.let { 246 anim.play( 247 ObjectAnimator.ofFloat(it, View.ALPHA, 0f, scrimAlpha) 248 .setDuration(mOpenDuration.toLong()) 249 ) 250 } 251 } 252 253 override fun onCreateCloseAnimation(anim: AnimatorSet) { 254 scrim?.let { 255 anim.play( 256 ObjectAnimator.ofFloat(it, View.ALPHA, scrimAlpha, 0f) 257 .setDuration(mCloseDuration.toLong()) 258 ) 259 } 260 } 261 262 override fun closeComplete() { 263 super.closeComplete() 264 popupContainer.removeView(scrim) 265 popupContainer.removeView(iconView) 266 onClosedCallback?.run() 267 } 268 269 /** 270 * Copy the iconView from taskView to dragLayer so it can stay on top of the scrim. It needs to 271 * be called after [getTargetObjectLocation] because [mTempRect] needs to be populated. 272 */ 273 private fun copyIconToDragLayer(insets: Rect) { 274 iconView = 275 IconView(context).apply { 276 layoutParams = 277 FrameLayout.LayoutParams( 278 taskContainer.iconView.width, 279 taskContainer.iconView.height 280 ) 281 x = mTempRect.left.toFloat() - insets.left 282 y = mTempRect.top.toFloat() - insets.top 283 drawable = taskContainer.iconView.drawable 284 setDrawableSize( 285 taskContainer.iconView.drawableWidth, 286 taskContainer.iconView.drawableHeight 287 ) 288 } 289 290 popupContainer.addView(iconView) 291 } 292 293 /** 294 * Orients this container to the left or right of the given icon, aligning with the desired row. 295 * 296 * These are the preferred orientations, in order (RTL prefers right-aligned over left): 297 * - Right and first option aligned 298 * - Right and second option aligned 299 * - Left and first option aligned 300 * - Left and second option aligned 301 * 302 * So we always align right if there is enough horizontal space 303 */ 304 override fun orientAboutObject() { 305 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) 306 // Needed for offsets later 307 optionMeasuredHeight = optionLayout.getChildAt(0).measuredHeight 308 val extraHorizontalSpace = (mArrowHeight + mArrowOffsetVertical + arrowHorizontalPadding) 309 310 val widthWithArrow = measuredWidth + paddingLeft + paddingRight + extraHorizontalSpace 311 getTargetObjectLocation(mTempRect) 312 val dragLayer: InsettableFrameLayout = popupContainer 313 val insets = dragLayer.insets 314 315 copyIconToDragLayer(insets) 316 317 // Put this menu to the right of the icon if there is space, 318 // which means the arrow is left aligned with the menu 319 val rightAlignedMenuStartX = mTempRect.left - widthWithArrow 320 val leftAlignedMenuStartX = mTempRect.right + extraHorizontalSpace 321 mIsLeftAligned = 322 if (mIsRtl) { 323 rightAlignedMenuStartX + insets.left < 0 324 } else { 325 leftAlignedMenuStartX + (widthWithArrow - extraHorizontalSpace) + insets.left < 326 dragLayer.width - insets.right 327 } 328 329 var menuStartX = if (mIsLeftAligned) leftAlignedMenuStartX else rightAlignedMenuStartX 330 331 // Offset y so that the arrow and row are center-aligned with the original icon. 332 val iconHeight = mTempRect.height() 333 val yOffset = (optionMeasuredHeight - iconHeight) / 2 334 var menuStartY = mTempRect.top - yOffset - extraSpaceForRowAlignment 335 336 // Insets are added later, so subtract them now. 337 menuStartX -= insets.left 338 menuStartY -= insets.top 339 340 x = menuStartX.toFloat() 341 y = menuStartY.toFloat() 342 343 val lp = layoutParams as FrameLayout.LayoutParams 344 val arrowLp = mArrow.layoutParams as FrameLayout.LayoutParams 345 lp.gravity = Gravity.TOP 346 arrowLp.gravity = lp.gravity 347 } 348 349 override fun addArrow() { 350 popupContainer.addView(mArrow) 351 mArrow.x = getArrowX() 352 mArrow.y = y + (optionMeasuredHeight / 2) - (mArrowHeight / 2) + extraSpaceForRowAlignment 353 354 updateArrowColor() 355 356 // This is inverted (x = height, y = width) because the arrow is rotated 357 mArrow.pivotX = if (mIsLeftAligned) 0f else mArrowHeight.toFloat() 358 mArrow.pivotY = 0f 359 } 360 361 private fun getArrowX(): Float { 362 return if (mIsLeftAligned) x - mArrowHeight else x + measuredWidth + mArrowOffsetVertical 363 } 364 365 override fun updateArrowColor() { 366 mArrow.background = 367 RoundedArrowDrawable.createHorizontalRoundedArrow( 368 mArrowWidth.toFloat(), 369 mArrowHeight.toFloat(), 370 mArrowPointRadius.toFloat(), 371 mIsLeftAligned, 372 mArrowColor 373 ) 374 elevation = mElevation 375 mArrow.elevation = mElevation 376 } 377 } 378