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