/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.quickstep.views import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.content.Context import android.graphics.Rect import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RectShape import android.util.AttributeSet import android.view.Gravity import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import com.android.launcher3.BaseDraggingActivity import com.android.launcher3.DeviceProfile import com.android.launcher3.InsettableFrameLayout import com.android.launcher3.R import com.android.launcher3.popup.ArrowPopup import com.android.launcher3.popup.RoundedArrowDrawable import com.android.launcher3.popup.SystemShortcut import com.android.launcher3.util.Themes import com.android.quickstep.TaskOverlayFactory import com.android.quickstep.views.TaskView.TaskIdAttributeContainer class TaskMenuViewWithArrow : ArrowPopup { companion object { const val TAG = "TaskMenuViewWithArrow" fun showForTask( taskContainer: TaskIdAttributeContainer, alignedOptionIndex: Int = 0 ): Boolean { val activity = BaseDraggingActivity.fromContext( taskContainer.taskView.context ) val taskMenuViewWithArrow = activity.layoutInflater.inflate( R.layout.task_menu_with_arrow, activity.dragLayer, false ) as TaskMenuViewWithArrow<*> return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex) } } constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor( context: Context, attrs: AttributeSet, defStyleAttr: Int ) : super(context, attrs, defStyleAttr) init { clipToOutline = true shouldScaleArrow = true mIsArrowRotated = true // This synchronizes the arrow and menu to open at the same time OPEN_CHILD_FADE_START_DELAY = OPEN_FADE_START_DELAY OPEN_CHILD_FADE_DURATION = OPEN_FADE_DURATION CLOSE_FADE_START_DELAY = CLOSE_CHILD_FADE_START_DELAY CLOSE_FADE_DURATION = CLOSE_CHILD_FADE_DURATION } private var alignedOptionIndex: Int = 0 private val extraSpaceForRowAlignment: Int get() = optionMeasuredHeight * alignedOptionIndex private val menuWidth = context.resources.getDimensionPixelSize(R.dimen.task_menu_width_grid) private lateinit var taskView: TaskView private lateinit var optionLayout: LinearLayout private lateinit var taskContainer: TaskIdAttributeContainer private var optionMeasuredHeight = 0 private val arrowHorizontalPadding: Int get() = if (taskView.isFocusedTask) resources.getDimensionPixelSize(R.dimen.task_menu_horizontal_padding) else 0 private var iconView: IconView? = null private var scrim: View? = null private val scrimAlpha = 0.8f override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0 override fun getTargetObjectLocation(outPos: Rect?) { popupContainer.getDescendantRectRelativeToSelf(taskContainer.iconView, outPos) } override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { if (ev?.action == MotionEvent.ACTION_DOWN) { if (!popupContainer.isEventOverView(this, ev)) { close(true) return true } } return false } override fun onFinishInflate() { super.onFinishInflate() optionLayout = findViewById(R.id.menu_option_layout) } private fun populateAndShowForTask( taskContainer: TaskIdAttributeContainer, alignedOptionIndex: Int ): Boolean { if (isAttachedToWindow) { return false } taskView = taskContainer.taskView this.taskContainer = taskContainer this.alignedOptionIndex = alignedOptionIndex if (!populateMenu()) return false addScrim() show() return true } private fun addScrim() { scrim = View(context).apply { layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) setBackgroundColor(Themes.getAttrColor(context, R.attr.overviewScrimColor)) alpha = 0f } popupContainer.addView(scrim) } /** @return true if successfully able to populate task view menu, false otherwise */ private fun populateMenu(): Boolean { // Icon may not be loaded if (taskContainer.task.icon == null) return false addMenuOptions() return true } private fun addMenuOptions() { // Add the options TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer).forEach { this.addMenuOption(it) } // Add the spaces between items val divider = ShapeDrawable(RectShape()) divider.paint.color = resources.getColor(android.R.color.transparent) val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt() optionLayout.showDividers = SHOW_DIVIDER_MIDDLE // Set the orientation, which makes the menu show val recentsView: RecentsView<*, *> = mActivityContext.getOverviewPanel() val orientationHandler = recentsView.pagedOrientationHandler val deviceProfile: DeviceProfile = mActivityContext.deviceProfile orientationHandler.setTaskOptionsMenuLayoutOrientation( deviceProfile, optionLayout, dividerSpacing, divider ) } private fun addMenuOption(menuOption: SystemShortcut<*>) { val menuOptionView = mActivityContext.layoutInflater.inflate(R.layout.task_view_menu_option, this, false) as LinearLayout menuOption.setIconAndLabelFor( menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text) ) val lp = menuOptionView.layoutParams as LayoutParams lp.width = menuWidth menuOptionView.setOnClickListener { view: View? -> menuOption.onClick(view) } optionLayout.addView(menuOptionView) } override fun assignMarginsAndBackgrounds(viewGroup: ViewGroup) { assignMarginsAndBackgrounds( this, Themes.getAttrColor(context, com.android.internal.R.attr.colorSurface) ) } override fun onCreateOpenAnimation(anim: AnimatorSet) { scrim?.let { anim.play( ObjectAnimator.ofFloat(it, View.ALPHA, 0f, scrimAlpha) .setDuration(OPEN_DURATION.toLong()) ) } } override fun onCreateCloseAnimation(anim: AnimatorSet) { scrim?.let { anim.play( ObjectAnimator.ofFloat(it, View.ALPHA, scrimAlpha, 0f) .setDuration(CLOSE_DURATION.toLong()) ) } } override fun closeComplete() { super.closeComplete() popupContainer.removeView(scrim) popupContainer.removeView(iconView) } /** * Copy the iconView from taskView to dragLayer so it can stay on top of the scrim. It needs to * be called after [getTargetObjectLocation] because [mTempRect] needs to be populated. */ private fun copyIconToDragLayer(insets: Rect) { iconView = IconView(context).apply { layoutParams = FrameLayout.LayoutParams( taskContainer.iconView.width, taskContainer.iconView.height ) x = mTempRect.left.toFloat() - insets.left y = mTempRect.top.toFloat() - insets.top drawable = taskContainer.iconView.drawable setDrawableSize( taskContainer.iconView.drawableWidth, taskContainer.iconView.drawableHeight ) } popupContainer.addView(iconView) } /** * Orients this container to the left or right of the given icon, aligning with the desired row. * * These are the preferred orientations, in order (RTL prefers right-aligned over left): * - Right and first option aligned * - Right and second option aligned * - Left and first option aligned * - Left and second option aligned * * So we always align right if there is enough horizontal space */ override fun orientAboutObject() { measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) // Needed for offsets later optionMeasuredHeight = optionLayout.getChildAt(0).measuredHeight val extraHorizontalSpace = (mArrowHeight + mArrowOffsetVertical + arrowHorizontalPadding) val widthWithArrow = measuredWidth + paddingLeft + paddingRight + extraHorizontalSpace getTargetObjectLocation(mTempRect) val dragLayer: InsettableFrameLayout = popupContainer val insets = dragLayer.insets copyIconToDragLayer(insets) // Put this menu to the right of the icon if there is space, // which means the arrow is left aligned with the menu val rightAlignedMenuStartX = mTempRect.left - widthWithArrow val leftAlignedMenuStartX = mTempRect.right + extraHorizontalSpace mIsLeftAligned = if (mIsRtl) { rightAlignedMenuStartX + insets.left < 0 } else { leftAlignedMenuStartX + (widthWithArrow - extraHorizontalSpace) + insets.left < dragLayer.width - insets.right } var menuStartX = if (mIsLeftAligned) leftAlignedMenuStartX else rightAlignedMenuStartX // Offset y so that the arrow and row are center-aligned with the original icon. val iconHeight = mTempRect.height() val yOffset = (optionMeasuredHeight - iconHeight) / 2 var menuStartY = mTempRect.top - yOffset - extraSpaceForRowAlignment // Insets are added later, so subtract them now. menuStartX -= insets.left menuStartY -= insets.top x = menuStartX.toFloat() y = menuStartY.toFloat() val lp = layoutParams as FrameLayout.LayoutParams val arrowLp = mArrow.layoutParams as FrameLayout.LayoutParams lp.gravity = Gravity.TOP arrowLp.gravity = lp.gravity } override fun addArrow() { popupContainer.addView(mArrow) mArrow.x = getArrowX() mArrow.y = y + (optionMeasuredHeight / 2) - (mArrowHeight / 2) + extraSpaceForRowAlignment updateArrowColor() // This is inverted (x = height, y = width) because the arrow is rotated mArrow.pivotX = if (mIsLeftAligned) 0f else mArrowHeight.toFloat() mArrow.pivotY = 0f } private fun getArrowX(): Float { return if (mIsLeftAligned) x - mArrowHeight else x + measuredWidth + mArrowOffsetVertical } override fun updateArrowColor() { mArrow.background = RoundedArrowDrawable( mArrowWidth.toFloat(), mArrowHeight.toFloat(), mArrowPointRadius.toFloat(), mIsLeftAligned, mArrowColor ) elevation = mElevation mArrow.elevation = mElevation } }