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 android.window.OnBackInvokedDispatcher 31 import androidx.core.view.updateLayoutParams 32 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE 33 import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE 34 import com.android.app.animation.Interpolators.STANDARD 35 import com.android.launcher3.AbstractFloatingView 36 import com.android.launcher3.R 37 import com.android.launcher3.anim.AnimatorListeners 38 import com.android.launcher3.popup.RoundedArrowDrawable 39 import com.android.launcher3.util.Themes 40 import com.android.launcher3.views.ActivityContext 41 42 private const val ENTER_DURATION_MS = 300L 43 private const val EXIT_DURATION_MS = 150L 44 45 /** Floating tooltip for Taskbar education. */ 46 class TaskbarEduTooltip 47 @JvmOverloads 48 constructor( 49 context: Context, 50 attrs: AttributeSet? = null, 51 defStyleAttr: Int = 0, 52 ) : AbstractFloatingView(context, attrs, defStyleAttr) { 53 54 private val activityContext: ActivityContext = ActivityContext.lookupContext(context) 55 56 private val backgroundColor = 57 Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright) 58 59 private val tooltipCornerRadius = Themes.getDialogCornerRadius(context) 60 private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) 61 private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height) 62 private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius) 63 64 private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta) 65 private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta) 66 67 /** Container where the tooltip's body should be inflated. */ 68 lateinit var content: ViewGroup 69 private set 70 71 private lateinit var arrow: View 72 73 /** Callback invoked when the tooltip is being closed. */ <lambda>null74 var onCloseCallback: () -> Unit = {} 75 private var openCloseAnimator: AnimatorSet? = null 76 /** Used to set whether users can tap outside the current tooltip window to dismiss it */ 77 var allowTouchDismissal = true 78 79 /** Animates the tooltip into view. */ shownull80 fun show() { 81 if (isOpen) { 82 return 83 } 84 mIsOpen = true 85 activityContext.dragLayer.addView(this) 86 87 // Make sure we have enough height to display all of the content, which can be an issue on 88 // large text and display scaling configurations. If we run out of height, remove the width 89 // constraint to reduce the number of lines of text and hopefully free up some height. 90 activityContext.dragLayer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) 91 if ( 92 measuredHeight + activityContext.deviceProfile.taskbarHeight >= 93 activityContext.deviceProfile.availableHeightPx 94 ) { 95 updateLayoutParams { width = MATCH_PARENT } 96 } 97 98 openCloseAnimator = createOpenCloseAnimator(isOpening = true).apply { start() } 99 } 100 onFinishInflatenull101 override fun onFinishInflate() { 102 super.onFinishInflate() 103 104 content = requireViewById(R.id.content) 105 arrow = requireViewById(R.id.arrow) 106 arrow.background = 107 RoundedArrowDrawable( 108 arrowWidth, 109 arrowHeight, 110 arrowPointRadius, 111 tooltipCornerRadius, 112 measuredWidth.toFloat(), 113 measuredHeight.toFloat(), 114 (measuredWidth - arrowWidth) / 2, // arrowOffsetX 115 0f, // arrowOffsetY 116 false, // isPointingUp 117 true, // leftAligned 118 backgroundColor, 119 ) 120 } 121 handleClosenull122 override fun handleClose(animate: Boolean) { 123 if (!isOpen) { 124 return 125 } 126 127 onCloseCallback() 128 if (!animate) { 129 return closeComplete() 130 } 131 132 openCloseAnimator?.cancel() 133 openCloseAnimator = createOpenCloseAnimator(isOpening = false) 134 openCloseAnimator?.addListener(AnimatorListeners.forEndCallback(this::closeComplete)) 135 openCloseAnimator?.start() 136 } 137 isOfTypenull138 override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0 139 140 override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { 141 if ( 142 ev?.action == ACTION_DOWN && 143 !activityContext.dragLayer.isEventOverView(this, ev) && 144 allowTouchDismissal 145 ) { 146 close(true) 147 } 148 return false 149 } 150 onAttachedToWindownull151 override fun onAttachedToWindow() { 152 super.onAttachedToWindow() 153 findOnBackInvokedDispatcher() 154 ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this) 155 } 156 onDetachedFromWindownull157 override fun onDetachedFromWindow() { 158 super.onDetachedFromWindow() 159 findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this) 160 Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) 161 } 162 closeCompletenull163 private fun closeComplete() { 164 openCloseAnimator?.cancel() 165 openCloseAnimator = null 166 mIsOpen = false 167 activityContext.dragLayer.removeView(this) 168 } 169 createOpenCloseAnimatornull170 private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet { 171 val duration: Long 172 val alphaValues: FloatArray 173 val translateYValues: FloatArray 174 val fadeInterpolator: Interpolator 175 val translateYInterpolator: Interpolator 176 177 if (isOpening) { 178 duration = ENTER_DURATION_MS 179 alphaValues = floatArrayOf(0f, 1f) 180 translateYValues = floatArrayOf(enterYDelta, 0f) 181 fadeInterpolator = STANDARD 182 translateYInterpolator = EMPHASIZED_DECELERATE 183 } else { 184 duration = EXIT_DURATION_MS 185 alphaValues = floatArrayOf(1f, 0f) 186 translateYValues = floatArrayOf(0f, exitYDelta) 187 fadeInterpolator = EMPHASIZED_ACCELERATE 188 translateYInterpolator = EMPHASIZED_ACCELERATE 189 } 190 191 val fade = 192 ValueAnimator.ofFloat(*alphaValues).apply { 193 interpolator = fadeInterpolator 194 addUpdateListener { 195 val alpha = it.animatedValue as Float 196 content.alpha = alpha 197 arrow.alpha = alpha 198 } 199 } 200 201 val translateY = 202 ValueAnimator.ofFloat(*translateYValues).apply { 203 interpolator = translateYInterpolator 204 addUpdateListener { 205 val translationY = it.animatedValue as Float 206 content.translationY = translationY 207 arrow.translationY = translationY 208 } 209 } 210 211 return AnimatorSet().apply { 212 this.duration = duration 213 playTogether(fade, translateY) 214 } 215 } 216 } 217