f38d98d1a6
This is an effort to align/unify color usage across all launcher projects and the system itself. This fix recreates all dynamic color attributes present in the OS since many launcher areas cannot access private attributes. It is recomemnded to use these new local attributes in all launcher areas and avoid creating new color resources when these can be used directly. Bug: 352690172 Test: Presubmits Flag: EXEMPT bugfix Change-Id: Ic36b8bf70ee37f2fdd32e459e66f0e2d001d6ba2
216 lines
7.8 KiB
Kotlin
216 lines
7.8 KiB
Kotlin
/*
|
|
* Copyright (C) 2023 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.launcher3.taskbar
|
|
|
|
import android.animation.AnimatorSet
|
|
import android.animation.ValueAnimator
|
|
import android.content.Context
|
|
import android.provider.Settings
|
|
import android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING
|
|
import android.util.AttributeSet
|
|
import android.view.MotionEvent
|
|
import android.view.MotionEvent.ACTION_DOWN
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
import android.view.animation.Interpolator
|
|
import android.window.OnBackInvokedDispatcher
|
|
import androidx.core.view.updateLayoutParams
|
|
import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
|
|
import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
|
|
import com.android.app.animation.Interpolators.STANDARD
|
|
import com.android.launcher3.AbstractFloatingView
|
|
import com.android.launcher3.R
|
|
import com.android.launcher3.anim.AnimatorListeners
|
|
import com.android.launcher3.popup.RoundedArrowDrawable
|
|
import com.android.launcher3.util.Themes
|
|
import com.android.launcher3.views.ActivityContext
|
|
|
|
private const val ENTER_DURATION_MS = 300L
|
|
private const val EXIT_DURATION_MS = 150L
|
|
|
|
/** Floating tooltip for Taskbar education. */
|
|
class TaskbarEduTooltip
|
|
@JvmOverloads
|
|
constructor(
|
|
context: Context,
|
|
attrs: AttributeSet? = null,
|
|
defStyleAttr: Int = 0,
|
|
) : AbstractFloatingView(context, attrs, defStyleAttr) {
|
|
|
|
private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
|
|
|
|
private val backgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright)
|
|
|
|
private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
|
|
private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
|
|
private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height)
|
|
private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius)
|
|
|
|
private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta)
|
|
private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta)
|
|
|
|
/** Container where the tooltip's body should be inflated. */
|
|
lateinit var content: ViewGroup
|
|
private set
|
|
|
|
private lateinit var arrow: View
|
|
|
|
/** Callback invoked when the tooltip is being closed. */
|
|
var onCloseCallback: () -> Unit = {}
|
|
private var openCloseAnimator: AnimatorSet? = null
|
|
/** Used to set whether users can tap outside the current tooltip window to dismiss it */
|
|
var allowTouchDismissal = true
|
|
|
|
/** Animates the tooltip into view. */
|
|
fun show() {
|
|
if (isOpen) {
|
|
return
|
|
}
|
|
mIsOpen = true
|
|
activityContext.dragLayer.addView(this)
|
|
|
|
// Make sure we have enough height to display all of the content, which can be an issue on
|
|
// large text and display scaling configurations. If we run out of height, remove the width
|
|
// constraint to reduce the number of lines of text and hopefully free up some height.
|
|
activityContext.dragLayer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
|
|
if (
|
|
measuredHeight + activityContext.deviceProfile.taskbarHeight >=
|
|
activityContext.deviceProfile.availableHeightPx
|
|
) {
|
|
updateLayoutParams { width = MATCH_PARENT }
|
|
}
|
|
|
|
openCloseAnimator = createOpenCloseAnimator(isOpening = true).apply { start() }
|
|
}
|
|
|
|
override fun onFinishInflate() {
|
|
super.onFinishInflate()
|
|
|
|
content = requireViewById(R.id.content)
|
|
arrow = requireViewById(R.id.arrow)
|
|
arrow.background =
|
|
RoundedArrowDrawable(
|
|
arrowWidth,
|
|
arrowHeight,
|
|
arrowPointRadius,
|
|
tooltipCornerRadius,
|
|
measuredWidth.toFloat(),
|
|
measuredHeight.toFloat(),
|
|
(measuredWidth - arrowWidth) / 2, // arrowOffsetX
|
|
0f, // arrowOffsetY
|
|
false, // isPointingUp
|
|
true, // leftAligned
|
|
backgroundColor,
|
|
)
|
|
}
|
|
|
|
override fun handleClose(animate: Boolean) {
|
|
if (!isOpen) {
|
|
return
|
|
}
|
|
|
|
onCloseCallback()
|
|
if (!animate) {
|
|
return closeComplete()
|
|
}
|
|
|
|
openCloseAnimator?.cancel()
|
|
openCloseAnimator = createOpenCloseAnimator(isOpening = false)
|
|
openCloseAnimator?.addListener(AnimatorListeners.forEndCallback(this::closeComplete))
|
|
openCloseAnimator?.start()
|
|
}
|
|
|
|
override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0
|
|
|
|
override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
|
|
if (
|
|
ev?.action == ACTION_DOWN &&
|
|
!activityContext.dragLayer.isEventOverView(this, ev) &&
|
|
allowTouchDismissal
|
|
) {
|
|
close(true)
|
|
}
|
|
return false
|
|
}
|
|
|
|
override fun onAttachedToWindow() {
|
|
super.onAttachedToWindow()
|
|
findOnBackInvokedDispatcher()
|
|
?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this)
|
|
}
|
|
|
|
override fun onDetachedFromWindow() {
|
|
super.onDetachedFromWindow()
|
|
findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this)
|
|
Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0)
|
|
}
|
|
|
|
private fun closeComplete() {
|
|
openCloseAnimator?.cancel()
|
|
openCloseAnimator = null
|
|
mIsOpen = false
|
|
activityContext.dragLayer.removeView(this)
|
|
}
|
|
|
|
private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet {
|
|
val duration: Long
|
|
val alphaValues: FloatArray
|
|
val translateYValues: FloatArray
|
|
val fadeInterpolator: Interpolator
|
|
val translateYInterpolator: Interpolator
|
|
|
|
if (isOpening) {
|
|
duration = ENTER_DURATION_MS
|
|
alphaValues = floatArrayOf(0f, 1f)
|
|
translateYValues = floatArrayOf(enterYDelta, 0f)
|
|
fadeInterpolator = STANDARD
|
|
translateYInterpolator = EMPHASIZED_DECELERATE
|
|
} else {
|
|
duration = EXIT_DURATION_MS
|
|
alphaValues = floatArrayOf(1f, 0f)
|
|
translateYValues = floatArrayOf(0f, exitYDelta)
|
|
fadeInterpolator = EMPHASIZED_ACCELERATE
|
|
translateYInterpolator = EMPHASIZED_ACCELERATE
|
|
}
|
|
|
|
val fade =
|
|
ValueAnimator.ofFloat(*alphaValues).apply {
|
|
interpolator = fadeInterpolator
|
|
addUpdateListener {
|
|
val alpha = it.animatedValue as Float
|
|
content.alpha = alpha
|
|
arrow.alpha = alpha
|
|
}
|
|
}
|
|
|
|
val translateY =
|
|
ValueAnimator.ofFloat(*translateYValues).apply {
|
|
interpolator = translateYInterpolator
|
|
addUpdateListener {
|
|
val translationY = it.animatedValue as Float
|
|
content.translationY = translationY
|
|
arrow.translationY = translationY
|
|
}
|
|
}
|
|
|
|
return AnimatorSet().apply {
|
|
this.duration = duration
|
|
playTogether(fade, translateY)
|
|
}
|
|
}
|
|
}
|