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