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(
49     context: Context,
50     attrs: AttributeSet? = null,
51     defStyleAttr: Int = 0,
52 ) : AbstractFloatingView(context, attrs, defStyleAttr) {
53 
54     private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
55 
56     private val backgroundColor =
57         Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright)
58 
59     private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
60     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
61     private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height)
62     private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius)
63 
64     private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta)
65     private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta)
66 
67     /** Container where the tooltip's body should be inflated. */
68     lateinit var content: ViewGroup
69         private set
70 
71     private lateinit var arrow: View
72 
73     /** Callback invoked when the tooltip is being closed. */
<lambda>null74     var onCloseCallback: () -> Unit = {}
75     private var openCloseAnimator: AnimatorSet? = null
76     /** Used to set whether users can tap outside the current tooltip window to dismiss it */
77     var allowTouchDismissal = true
78 
79     /** Animates the tooltip into view. */
shownull80     fun show() {
81         if (isOpen) {
82             return
83         }
84         mIsOpen = true
85         activityContext.dragLayer.addView(this)
86 
87         // Make sure we have enough height to display all of the content, which can be an issue on
88         // large text and display scaling configurations. If we run out of height, remove the width
89         // constraint to reduce the number of lines of text and hopefully free up some height.
90         activityContext.dragLayer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
91         if (
92             measuredHeight + activityContext.deviceProfile.taskbarHeight >=
93                 activityContext.deviceProfile.availableHeightPx
94         ) {
95             updateLayoutParams { width = MATCH_PARENT }
96         }
97 
98         openCloseAnimator = createOpenCloseAnimator(isOpening = true).apply { start() }
99     }
100 
onFinishInflatenull101     override fun onFinishInflate() {
102         super.onFinishInflate()
103 
104         content = requireViewById(R.id.content)
105         arrow = requireViewById(R.id.arrow)
106         arrow.background =
107             RoundedArrowDrawable(
108                 arrowWidth,
109                 arrowHeight,
110                 arrowPointRadius,
111                 tooltipCornerRadius,
112                 measuredWidth.toFloat(),
113                 measuredHeight.toFloat(),
114                 (measuredWidth - arrowWidth) / 2, // arrowOffsetX
115                 0f, // arrowOffsetY
116                 false, // isPointingUp
117                 true, // leftAligned
118                 backgroundColor,
119             )
120     }
121 
handleClosenull122     override fun handleClose(animate: Boolean) {
123         if (!isOpen) {
124             return
125         }
126 
127         onCloseCallback()
128         if (!animate) {
129             return closeComplete()
130         }
131 
132         openCloseAnimator?.cancel()
133         openCloseAnimator = createOpenCloseAnimator(isOpening = false)
134         openCloseAnimator?.addListener(AnimatorListeners.forEndCallback(this::closeComplete))
135         openCloseAnimator?.start()
136     }
137 
isOfTypenull138     override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0
139 
140     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
141         if (
142             ev?.action == ACTION_DOWN &&
143                 !activityContext.dragLayer.isEventOverView(this, ev) &&
144                 allowTouchDismissal
145         ) {
146             close(true)
147         }
148         return false
149     }
150 
onAttachedToWindownull151     override fun onAttachedToWindow() {
152         super.onAttachedToWindow()
153         findOnBackInvokedDispatcher()
154             ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this)
155     }
156 
onDetachedFromWindownull157     override fun onDetachedFromWindow() {
158         super.onDetachedFromWindow()
159         findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this)
160         Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0)
161     }
162 
closeCompletenull163     private fun closeComplete() {
164         openCloseAnimator?.cancel()
165         openCloseAnimator = null
166         mIsOpen = false
167         activityContext.dragLayer.removeView(this)
168     }
169 
createOpenCloseAnimatornull170     private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet {
171         val duration: Long
172         val alphaValues: FloatArray
173         val translateYValues: FloatArray
174         val fadeInterpolator: Interpolator
175         val translateYInterpolator: Interpolator
176 
177         if (isOpening) {
178             duration = ENTER_DURATION_MS
179             alphaValues = floatArrayOf(0f, 1f)
180             translateYValues = floatArrayOf(enterYDelta, 0f)
181             fadeInterpolator = STANDARD
182             translateYInterpolator = EMPHASIZED_DECELERATE
183         } else {
184             duration = EXIT_DURATION_MS
185             alphaValues = floatArrayOf(1f, 0f)
186             translateYValues = floatArrayOf(0f, exitYDelta)
187             fadeInterpolator = EMPHASIZED_ACCELERATE
188             translateYInterpolator = EMPHASIZED_ACCELERATE
189         }
190 
191         val fade =
192             ValueAnimator.ofFloat(*alphaValues).apply {
193                 interpolator = fadeInterpolator
194                 addUpdateListener {
195                     val alpha = it.animatedValue as Float
196                     content.alpha = alpha
197                     arrow.alpha = alpha
198                 }
199             }
200 
201         val translateY =
202             ValueAnimator.ofFloat(*translateYValues).apply {
203                 interpolator = translateYInterpolator
204                 addUpdateListener {
205                     val translationY = it.animatedValue as Float
206                     content.translationY = translationY
207                     arrow.translationY = translationY
208                 }
209             }
210 
211         return AnimatorSet().apply {
212             this.duration = duration
213             playTogether(fade, translateY)
214         }
215     }
216 }
217