blob: 06a579300ab9f69bf77ba5e4169f5ff566295c60 [file] [log] [blame]
/*
* 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
}
}