1 /*
<lambda>null2  * Copyright (C) 2020 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 
17 package com.android.systemui.controls
18 
19 import android.annotation.StringRes
20 import android.content.Context
21 import android.graphics.CornerPathEffect
22 import android.graphics.drawable.ShapeDrawable
23 import android.util.TypedValue
24 import android.view.LayoutInflater
25 import android.view.View
26 import android.view.ViewGroup
27 import android.view.animation.AccelerateInterpolator
28 import android.view.animation.DecelerateInterpolator
29 import android.widget.TextView
30 import com.android.systemui.Prefs
31 import com.android.systemui.R
32 import com.android.systemui.recents.TriangleShape
33 
34 /**
35  * Manager for showing an onboarding tooltip on screen.
36  *
37  * The tooltip can be made to appear below or above a point. The number of times it will appear
38  * is determined by an shared preference (defined in [Prefs]).
39  *
40  * @property context A context to use to inflate the views and retrieve shared preferences from
41  * @property preferenceName name of the preference to use to track the number of times the tooltip
42  *                          has been shown.
43  * @property maxTimesShown the maximum number of times to show the tooltip
44  * @property below whether the tooltip should appear below (with up pointing arrow) or above (down
45  *                 pointing arrow) the specified point.
46  * @see [TooltipManager.show]
47  */
48 class TooltipManager(
49     context: Context,
50     private val preferenceName: String,
51     private val maxTimesShown: Int = 2,
52     private val below: Boolean = true
53 ) {
54 
55     companion object {
56         private const val SHOW_DELAY_MS: Long = 500
57         private const val SHOW_DURATION_MS: Long = 300
58         private const val HIDE_DURATION_MS: Long = 100
59     }
60 
61     private var shown = Prefs.getInt(context, preferenceName, 0)
62 
63     val layout: ViewGroup =
64         LayoutInflater.from(context).inflate(R.layout.controls_onboarding, null) as ViewGroup
65     val preferenceStorer = { num: Int ->
66         Prefs.putInt(context, preferenceName, num)
67     }
68 
69     init {
70         layout.alpha = 0f
71     }
72 
73     private val textView = layout.requireViewById<TextView>(R.id.onboarding_text)
74     private val dismissView = layout.requireViewById<View>(R.id.dismiss).apply {
75         setOnClickListener {
76             hide(true)
77         }
78     }
79 
80     private val arrowView = layout.requireViewById<View>(R.id.arrow).apply {
81         val typedValue = TypedValue()
82         context.theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true)
83         val toastColor = context.resources.getColor(typedValue.resourceId, context.theme)
84         val arrowRadius = context.resources.getDimensionPixelSize(
85             R.dimen.recents_onboarding_toast_arrow_corner_radius)
86         val arrowLp = layoutParams
87         val arrowDrawable = ShapeDrawable(TriangleShape.create(
88             arrowLp.width.toFloat(), arrowLp.height.toFloat(), below))
89         val arrowPaint = arrowDrawable.paint
90         arrowPaint.color = toastColor
91         // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
92         arrowPaint.pathEffect = CornerPathEffect(arrowRadius.toFloat())
93         setBackground(arrowDrawable)
94     }
95 
96     init {
97         if (!below) {
98             layout.removeView(arrowView)
99             layout.addView(arrowView)
100             (arrowView.layoutParams as ViewGroup.MarginLayoutParams).apply {
101                 bottomMargin = topMargin
102                 topMargin = 0
103             }
104         }
105     }
106 
107     /**
108      * Show the tooltip
109      *
110      * @param stringRes the id of the string to show in the tooltip
111      * @param x horizontal position (w.r.t. screen) for the arrow point
112      * @param y vertical position (w.r.t. screen) for the arrow point
113      */
114     fun show(@StringRes stringRes: Int, x: Int, y: Int) {
115         if (!shouldShow()) return
116         textView.setText(stringRes)
117         shown++
118         preferenceStorer(shown)
119         layout.post {
120             val p = IntArray(2)
121             layout.getLocationOnScreen(p)
122             layout.translationX = (x - p[0] - layout.width / 2).toFloat()
123             layout.translationY = (y - p[1]).toFloat() - if (!below) layout.height else 0
124             if (layout.alpha == 0f) {
125                 layout.animate()
126                     .alpha(1f)
127                     .withLayer()
128                     .setStartDelay(SHOW_DELAY_MS)
129                     .setDuration(SHOW_DURATION_MS)
130                     .setInterpolator(DecelerateInterpolator())
131                     .start()
132             }
133         }
134     }
135 
136     /**
137      * Hide the tooltip
138      *
139      * @param animate whether to animate the fade out
140      */
141     fun hide(animate: Boolean = false) {
142         if (layout.alpha == 0f) return
143         layout.post {
144             if (animate) {
145                 layout.animate()
146                     .alpha(0f)
147                     .withLayer()
148                     .setStartDelay(0)
149                     .setDuration(HIDE_DURATION_MS)
150                     .setInterpolator(AccelerateInterpolator())
151                     .start()
152             } else {
153                 layout.animate().cancel()
154                 layout.alpha = 0f
155             }
156         }
157     }
158 
159     private fun shouldShow() = shown < maxTimesShown
160 }