blob: 6e17bc94b16bf8112a58404fcabc0721e8d96f0f [file] [log] [blame]
/*
* Copyright (C) 2020 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.systemui.controls
import android.annotation.StringRes
import android.content.Context
import android.graphics.CornerPathEffect
import android.graphics.drawable.ShapeDrawable
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.TextView
import com.android.systemui.Prefs
import com.android.systemui.R
import com.android.systemui.recents.TriangleShape
/**
* Manager for showing an onboarding tooltip on screen.
*
* The tooltip can be made to appear below or above a point. The number of times it will appear
* is determined by an shared preference (defined in [Prefs]).
*
* @property context A context to use to inflate the views and retrieve shared preferences from
* @property preferenceName name of the preference to use to track the number of times the tooltip
* has been shown.
* @property maxTimesShown the maximum number of times to show the tooltip
* @property below whether the tooltip should appear below (with up pointing arrow) or above (down
* pointing arrow) the specified point.
* @see [TooltipManager.show]
*/
class TooltipManager(
context: Context,
private val preferenceName: String,
private val maxTimesShown: Int = 2,
private val below: Boolean = true
) {
companion object {
private const val SHOW_DELAY_MS: Long = 500
private const val SHOW_DURATION_MS: Long = 300
private const val HIDE_DURATION_MS: Long = 100
}
private var shown = Prefs.getInt(context, preferenceName, 0)
val layout: ViewGroup =
LayoutInflater.from(context).inflate(R.layout.controls_onboarding, null) as ViewGroup
val preferenceStorer = { num: Int ->
Prefs.putInt(context, preferenceName, num)
}
init {
layout.alpha = 0f
}
private val textView = layout.requireViewById<TextView>(R.id.onboarding_text)
private val dismissView = layout.requireViewById<View>(R.id.dismiss).apply {
setOnClickListener {
hide(true)
}
}
private val arrowView = layout.requireViewById<View>(R.id.arrow).apply {
val typedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true)
val toastColor = context.resources.getColor(typedValue.resourceId, context.theme)
val arrowRadius = context.resources.getDimensionPixelSize(
R.dimen.recents_onboarding_toast_arrow_corner_radius)
val arrowLp = layoutParams
val arrowDrawable = ShapeDrawable(TriangleShape.create(
arrowLp.width.toFloat(), arrowLp.height.toFloat(), below))
val arrowPaint = arrowDrawable.paint
arrowPaint.color = toastColor
// The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
arrowPaint.pathEffect = CornerPathEffect(arrowRadius.toFloat())
setBackground(arrowDrawable)
}
init {
if (!below) {
layout.removeView(arrowView)
layout.addView(arrowView)
(arrowView.layoutParams as ViewGroup.MarginLayoutParams).apply {
bottomMargin = topMargin
topMargin = 0
}
}
}
/**
* Show the tooltip
*
* @param stringRes the id of the string to show in the tooltip
* @param x horizontal position (w.r.t. screen) for the arrow point
* @param y vertical position (w.r.t. screen) for the arrow point
*/
fun show(@StringRes stringRes: Int, x: Int, y: Int) {
if (!shouldShow()) return
textView.setText(stringRes)
shown++
preferenceStorer(shown)
layout.post {
val p = IntArray(2)
layout.getLocationOnScreen(p)
layout.translationX = (x - p[0] - layout.width / 2).toFloat()
layout.translationY = (y - p[1]).toFloat() - if (!below) layout.height else 0
if (layout.alpha == 0f) {
layout.animate()
.alpha(1f)
.withLayer()
.setStartDelay(SHOW_DELAY_MS)
.setDuration(SHOW_DURATION_MS)
.setInterpolator(DecelerateInterpolator())
.start()
}
}
}
/**
* Hide the tooltip
*
* @param animate whether to animate the fade out
*/
fun hide(animate: Boolean = false) {
if (layout.alpha == 0f) return
layout.post {
if (animate) {
layout.animate()
.alpha(0f)
.withLayer()
.setStartDelay(0)
.setDuration(HIDE_DURATION_MS)
.setInterpolator(AccelerateInterpolator())
.start()
} else {
layout.animate().cancel()
layout.alpha = 0f
}
}
}
private fun shouldShow() = shown < maxTimesShown
}