Adds a tooltip for multiple structures

Adds a tooltip (manager) to display tooltips in Controls surfaces. The
manager supports the following:

* The tooltip will not be shown after a certain number of times. Tracked
by a Shared Pref that is passed to the TooltipManager
* The tooltip will be shown pointing to a given position on screen
* The tooltip can be parametrized to show the arrow pointing up
(default) or down.

Fixes: 150707923
Test: manual

Change-Id: I70e7c38343a16ae6cd887a0fdcfa5b0f896e413e
This commit is contained in:
Fabian Kozynski
2020-03-06 09:52:46 -05:00
parent 61278391c5
commit 75ad41effa
8 changed files with 315 additions and 15 deletions

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:padding="4dp"
android:orientation="vertical">
<View
android:id="@+id/arrow"
android:elevation="2dp"
android:layout_width="10dp"
android:layout_height="8dp"
android:layout_marginBottom="-2dp"
android:layout_gravity="center_horizontal"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="4dp"
android:background="@drawable/recents_onboarding_toast_rounded_background"
android:layout_gravity="center_horizontal"
android:elevation="2dp"
android:orientation="horizontal">
<TextView
android:id="@+id/onboarding_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:textColor="?attr/wallpaperTextColor"
android:textSize="16sp"/>
<ImageView
android:id="@+id/dismiss"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:padding="10dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:alpha="0.7"
android:src="@drawable/ic_close_white"
android:tint="?attr/wallpaperTextColor"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/accessibility_desc_close"/>
</LinearLayout>
</LinearLayout>

View File

@@ -15,17 +15,10 @@
~ limitations under the License.
-->
<androidx.core.widget.NestedScrollView
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/listAll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginTop="@dimen/controls_management_list_margin">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listAll"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.core.widget.NestedScrollView>
android:layout_marginTop="@dimen/controls_management_list_margin"/>

View File

@@ -2653,4 +2653,7 @@
<string name="controls_pin_verify">Verify device PIN</string>
<!-- Controls PIN entry dialog, text hint [CHAR LIMIT=30] -->
<string name="controls_pin_instructions">Enter PIN</string>
<!-- Tooltip to show in management screen when there are multiple structures [CHAR_LIMIT=50] -->
<string name="controls_structure_tooltip">Swipe to see other structures</string>
</resources>

View File

@@ -650,6 +650,7 @@
<!-- Controls styles -->
<style name="Theme.ControlsManagement" parent="@android:style/Theme.DeviceDefault.NoActionBar">
<item name="android:windowIsTranslucent">false</item>
<item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item>
</style>
<style name="TextAppearance.Control">

View File

@@ -59,7 +59,8 @@ public final class Prefs {
Key.TOUCHED_RINGER_TOGGLE,
Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP,
Key.HAS_SEEN_BUBBLES_EDUCATION,
Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION
Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION,
Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT
})
public @interface Key {
@Deprecated
@@ -107,6 +108,7 @@ public final class Prefs {
String HAS_SEEN_ODI_CAPTIONS_TOOLTIP = "HasSeenODICaptionsTooltip";
String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding";
String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding";
String CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT = "ControlsStructureSwipeTooltipCount";
}
public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {

View File

@@ -0,0 +1,160 @@
/*
* 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
}

View File

@@ -19,22 +19,27 @@ package com.android.systemui.controls.management
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.TextUtils
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.viewpager2.widget.ViewPager2
import com.android.systemui.Prefs
import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.TooltipManager
import com.android.systemui.controls.controller.ControlsControllerImpl
import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.PageIndicator
import com.android.systemui.settings.CurrentUserTracker
import java.text.Collator
import java.util.concurrent.Executor
@@ -51,6 +56,8 @@ class ControlsFavoritingActivity @Inject constructor(
companion object {
private const val TAG = "ControlsFavoritingActivity"
const val EXTRA_APP = "extra_app_label"
private const val TOOLTIP_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT
private const val TOOLTIP_MAX_SHOWN = 2
}
private var component: ComponentName? = null
@@ -61,7 +68,8 @@ class ControlsFavoritingActivity @Inject constructor(
private lateinit var titleView: TextView
private lateinit var iconView: ImageView
private lateinit var iconFrame: View
private lateinit var pageIndicator: PageIndicator
private lateinit var pageIndicator: ManagementPageIndicator
private var mTooltipManager: TooltipManager? = null
private var listOfStructures = emptyList<StructureContainer>()
private lateinit var comparator: Comparator<StructureContainer>
@@ -172,9 +180,48 @@ class ControlsFavoritingActivity @Inject constructor(
layoutResource = R.layout.controls_management_favorites
inflate()
}
statusText = requireViewById(R.id.status_message)
pageIndicator = requireViewById(R.id.structure_page_indicator)
if (shouldShowTooltip()) {
mTooltipManager = TooltipManager(statusText.context,
TOOLTIP_PREFS_KEY, TOOLTIP_MAX_SHOWN)
addContentView(
mTooltipManager?.layout,
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
Gravity.TOP or Gravity.LEFT
)
)
}
pageIndicator = requireViewById<ManagementPageIndicator>(
R.id.structure_page_indicator).apply {
addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
if (v.visibility == View.VISIBLE && mTooltipManager != null) {
val p = IntArray(2)
v.getLocationOnScreen(p)
val x = p[0] + (right - left) / 2
val y = p[1] + bottom - top
mTooltipManager?.show(R.string.controls_structure_tooltip, x, y)
}
}
})
visibilityListener = {
if (it != View.VISIBLE) {
mTooltipManager?.hide(true)
}
}
}
titleView = requireViewById<TextView>(R.id.title).apply {
text = appName ?: resources.getText(R.string.controls_favorite_default_title)
@@ -184,6 +231,12 @@ class ControlsFavoritingActivity @Inject constructor(
iconView = requireViewById(com.android.internal.R.id.icon)
iconFrame = requireViewById(R.id.icon_frame)
structurePager = requireViewById<ViewPager2>(R.id.structure_pager)
structurePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
mTooltipManager?.hide(true)
}
})
bindButtons()
}
@@ -207,11 +260,25 @@ class ControlsFavoritingActivity @Inject constructor(
}
}
override fun onPause() {
super.onPause()
mTooltipManager?.hide(false)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
mTooltipManager?.hide(false)
}
override fun onDestroy() {
currentUserTracker.stopTracking()
listingController.removeCallback(listingCallback)
super.onDestroy()
}
private fun shouldShowTooltip(): Boolean {
return Prefs.getInt(applicationContext, TOOLTIP_PREFS_KEY, 0) < TOOLTIP_MAX_SHOWN
}
}
data class StructureContainer(val structureName: CharSequence, val model: ControlsModel)

View File

@@ -40,4 +40,13 @@ class ManagementPageIndicator(
super.setLocation(location)
}
}
var visibilityListener: (Int) -> Unit = {}
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (changedView == this) {
visibilityListener(visibility)
}
}
}