| /* |
| * 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.KtR |
| import com.android.quickstep.TaskOverlayFactory |
| import com.android.quickstep.views.TaskView.TaskIdAttributeContainer |
| |
| class TaskMenuViewWithArrow<T : BaseDraggingActivity> : ArrowPopup<T> { |
| companion object { |
| const val TAG = "TaskMenuViewWithArrow" |
| |
| fun showForTask( |
| taskContainer: TaskIdAttributeContainer, |
| alignSecondRow: Boolean = false |
| ): Boolean { |
| val activity = BaseDraggingActivity |
| .fromContext<BaseDraggingActivity>(taskContainer.taskView.context) |
| val taskMenuViewWithArrow = activity.layoutInflater |
| .inflate( |
| KtR.layout.task_menu_with_arrow, |
| activity.dragLayer, |
| false |
| ) as TaskMenuViewWithArrow<*> |
| |
| return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignSecondRow) |
| } |
| } |
| |
| 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 |
| // 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 alignSecondRow: Boolean = false |
| private val extraSpaceForSecondRowAlignment: Int |
| get() = if (alignSecondRow) optionMeasuredHeight else 0 |
| 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(KtR.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(KtR.id.menu_option_layout) |
| } |
| |
| private fun populateAndShowForTask( |
| taskContainer: TaskIdAttributeContainer, |
| alignSecondRow: Boolean |
| ): Boolean { |
| if (isAttachedToWindow) { |
| return false |
| } |
| |
| taskView = taskContainer.taskView |
| this.taskContainer = taskContainer |
| this.alignSecondRow = alignSecondRow |
| 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, mActivityContext.deviceProfile, 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(KtR.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( |
| KtR.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 first option |
| * or second. |
| * |
| * 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 - extraSpaceForSecondRowAlignment |
| |
| // 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) + |
| extraSpaceForSecondRowAlignment |
| |
| 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 |
| } |
| |
| } |