/* * 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, com.android.internal.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) } } }