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