• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.core.view.updateLayoutParams
31 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
32 import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
33 import com.android.app.animation.Interpolators.STANDARD
34 import com.android.launcher3.AbstractFloatingView
35 import com.android.launcher3.R
36 import com.android.launcher3.anim.AnimatorListeners
37 import com.android.launcher3.popup.RoundedArrowDrawable
38 import com.android.launcher3.util.Themes
39 import com.android.launcher3.views.ActivityContext
40 
41 private const val ENTER_DURATION_MS = 300L
42 private const val EXIT_DURATION_MS = 150L
43 
44 /** Floating tooltip for Taskbar education. */
45 class TaskbarEduTooltip
46 @JvmOverloads
47 constructor(
48     context: Context,
49     attrs: AttributeSet? = null,
50     defStyleAttr: Int = 0,
51 ) : AbstractFloatingView(context, attrs, defStyleAttr) {
52 
53     private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
54 
55     private val backgroundColor =
56         Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright)
57 
58     private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
59     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
60     private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height)
61     private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius)
62 
63     private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta)
64     private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta)
65 
66     /** Container where the tooltip's body should be inflated. */
67     lateinit var content: ViewGroup
68         private set
69     private lateinit var arrow: View
70 
71     /** Callback invoked when the tooltip is being closed. */
<lambda>null72     var onCloseCallback: () -> Unit = {}
73     private var openCloseAnimator: AnimatorSet? = null
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 = findViewById(R.id.content)
101         arrow = findViewById(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 (ev?.action == ACTION_DOWN && !activityContext.dragLayer.isEventOverView(this, ev)) {
138             close(true)
139         }
140         return false
141     }
142 
onDetachedFromWindownull143     override fun onDetachedFromWindow() {
144         super.onDetachedFromWindow()
145         Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0)
146     }
147 
closeCompletenull148     private fun closeComplete() {
149         openCloseAnimator?.cancel()
150         openCloseAnimator = null
151         mIsOpen = false
152         activityContext.dragLayer.removeView(this)
153     }
154 
createOpenCloseAnimatornull155     private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet {
156         val duration: Long
157         val alphaValues: FloatArray
158         val translateYValues: FloatArray
159         val fadeInterpolator: Interpolator
160         val translateYInterpolator: Interpolator
161 
162         if (isOpening) {
163             duration = ENTER_DURATION_MS
164             alphaValues = floatArrayOf(0f, 1f)
165             translateYValues = floatArrayOf(enterYDelta, 0f)
166             fadeInterpolator = STANDARD
167             translateYInterpolator = EMPHASIZED_DECELERATE
168         } else {
169             duration = EXIT_DURATION_MS
170             alphaValues = floatArrayOf(1f, 0f)
171             translateYValues = floatArrayOf(0f, exitYDelta)
172             fadeInterpolator = EMPHASIZED_ACCELERATE
173             translateYInterpolator = EMPHASIZED_ACCELERATE
174         }
175 
176         val fade =
177             ValueAnimator.ofFloat(*alphaValues).apply {
178                 interpolator = fadeInterpolator
179                 addUpdateListener {
180                     val alpha = it.animatedValue as Float
181                     content.alpha = alpha
182                     arrow.alpha = alpha
183                 }
184             }
185 
186         val translateY =
187             ValueAnimator.ofFloat(*translateYValues).apply {
188                 interpolator = translateYInterpolator
189                 addUpdateListener {
190                     val translationY = it.animatedValue as Float
191                     content.translationY = translationY
192                     arrow.translationY = translationY
193                 }
194             }
195 
196         return AnimatorSet().apply {
197             this.duration = duration
198             playTogether(fade, translateY)
199         }
200     }
201 }
202