/* * 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.res.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(R.id.onboarding_text) private val dismissView = layout.requireViewById(R.id.dismiss).apply { setOnClickListener { hide(true) } } private val arrowView = layout.requireViewById(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 }