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