1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.taskbar
17 
18 import android.animation.AnimatorSet
19 import android.animation.ValueAnimator
20 import android.content.Context
21 import android.provider.Settings
22 import android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING
23 import android.util.AttributeSet
24 import android.view.MotionEvent
25 import android.view.MotionEvent.ACTION_DOWN
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
29 import android.view.animation.Interpolator
30 import android.window.OnBackInvokedDispatcher
31 import androidx.core.view.updateLayoutParams
32 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
33 import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
34 import com.android.app.animation.Interpolators.STANDARD
35 import com.android.launcher3.AbstractFloatingView
36 import com.android.launcher3.R
37 import com.android.launcher3.anim.AnimatorListeners
38 import com.android.launcher3.popup.RoundedArrowDrawable
39 import com.android.launcher3.util.Themes
40 import com.android.launcher3.views.ActivityContext
41 
42 private const val ENTER_DURATION_MS = 300L
43 private const val EXIT_DURATION_MS = 150L
44 
45 /** Floating tooltip for Taskbar education. */
46 class TaskbarEduTooltip
47 @JvmOverloads
48 constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
49     AbstractFloatingView(context, attrs, defStyleAttr) {
50 
51     private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
52 
53     private val backgroundColor = context.getColor(R.color.materialColorSurfaceBright)
54 
55     private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
56     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
57     private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height)
58     private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius)
59 
60     private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta)
61     private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta)
62 
63     /** Container where the tooltip's body should be inflated. */
64     lateinit var content: ViewGroup
65         private set
66 
67     private lateinit var arrow: View
68 
69     /** Callback invoked when the tooltip is being closed. */
<lambda>null70     var onCloseCallback: () -> Unit = {}
71     private var openCloseAnimator: AnimatorSet? = null
72     /** Used to set whether users can tap outside the current tooltip window to dismiss it */
73     var allowTouchDismissal = true
74 
75     /** Animates the tooltip into view. */
shownull76     fun show() {
77         if (isOpen) {
78             return
79         }
80         mIsOpen = true
81         activityContext.dragLayer.addView(this)
82 
83         // Make sure we have enough height to display all of the content, which can be an issue on
84         // large text and display scaling configurations. If we run out of height, remove the width
85         // constraint to reduce the number of lines of text and hopefully free up some height.
86         activityContext.dragLayer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
87         if (
88             measuredHeight + activityContext.deviceProfile.taskbarHeight >=
89                 activityContext.deviceProfile.availableHeightPx
90         ) {
91             updateLayoutParams { width = MATCH_PARENT }
92         }
93 
94         openCloseAnimator = createOpenCloseAnimator(isOpening = true).apply { start() }
95     }
96 
onFinishInflatenull97     override fun onFinishInflate() {
98         super.onFinishInflate()
99 
100         content = requireViewById(R.id.content)
101         arrow = requireViewById(R.id.arrow)
102         arrow.background =
103             RoundedArrowDrawable(
104                 arrowWidth,
105                 arrowHeight,
106                 arrowPointRadius,
107                 tooltipCornerRadius,
108                 measuredWidth.toFloat(),
109                 measuredHeight.toFloat(),
110                 (measuredWidth - arrowWidth) / 2, // arrowOffsetX
111                 0f, // arrowOffsetY
112                 false, // isPointingUp
113                 true, // leftAligned
114                 backgroundColor,
115             )
116     }
117 
handleClosenull118     override fun handleClose(animate: Boolean) {
119         if (!isOpen) {
120             return
121         }
122 
123         onCloseCallback()
124         if (!animate) {
125             return closeComplete()
126         }
127 
128         openCloseAnimator?.cancel()
129         openCloseAnimator = createOpenCloseAnimator(isOpening = false)
130         openCloseAnimator?.addListener(AnimatorListeners.forEndCallback(this::closeComplete))
131         openCloseAnimator?.start()
132     }
133 
isOfTypenull134     override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0
135 
136     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
137         if (
138             ev?.action == ACTION_DOWN &&
139                 !activityContext.dragLayer.isEventOverView(this, ev) &&
140                 allowTouchDismissal
141         ) {
142             close(true)
143         }
144         return false
145     }
146 
onAttachedToWindownull147     override fun onAttachedToWindow() {
148         super.onAttachedToWindow()
149         findOnBackInvokedDispatcher()
150             ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this)
151     }
152 
onDetachedFromWindownull153     override fun onDetachedFromWindow() {
154         super.onDetachedFromWindow()
155         findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this)
156         Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0)
157     }
158 
closeCompletenull159     private fun closeComplete() {
160         openCloseAnimator?.cancel()
161         openCloseAnimator = null
162         mIsOpen = false
163         activityContext.dragLayer.removeView(this)
164     }
165 
createOpenCloseAnimatornull166     private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet {
167         val duration: Long
168         val alphaValues: FloatArray
169         val translateYValues: FloatArray
170         val fadeInterpolator: Interpolator
171         val translateYInterpolator: Interpolator
172 
173         if (isOpening) {
174             duration = ENTER_DURATION_MS
175             alphaValues = floatArrayOf(0f, 1f)
176             translateYValues = floatArrayOf(enterYDelta, 0f)
177             fadeInterpolator = STANDARD
178             translateYInterpolator = EMPHASIZED_DECELERATE
179         } else {
180             duration = EXIT_DURATION_MS
181             alphaValues = floatArrayOf(1f, 0f)
182             translateYValues = floatArrayOf(0f, exitYDelta)
183             fadeInterpolator = EMPHASIZED_ACCELERATE
184             translateYInterpolator = EMPHASIZED_ACCELERATE
185         }
186 
187         val fade =
188             ValueAnimator.ofFloat(*alphaValues).apply {
189                 interpolator = fadeInterpolator
190                 addUpdateListener {
191                     val alpha = it.animatedValue as Float
192                     content.alpha = alpha
193                     arrow.alpha = alpha
194                 }
195             }
196 
197         val translateY =
198             ValueAnimator.ofFloat(*translateYValues).apply {
199                 interpolator = translateYInterpolator
200                 addUpdateListener {
201                     val translationY = it.animatedValue as Float
202                     content.translationY = translationY
203                     arrow.translationY = translationY
204                 }
205             }
206 
207         return AnimatorSet().apply {
208             this.duration = duration
209             playTogether(fade, translateY)
210         }
211     }
212 }
213