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 androidx.core.view.updateLayoutParams 31 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE 32 import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE 33 import com.android.app.animation.Interpolators.STANDARD 34 import com.android.launcher3.AbstractFloatingView 35 import com.android.launcher3.R 36 import com.android.launcher3.anim.AnimatorListeners 37 import com.android.launcher3.popup.RoundedArrowDrawable 38 import com.android.launcher3.util.Themes 39 import com.android.launcher3.views.ActivityContext 40 41 private const val ENTER_DURATION_MS = 300L 42 private const val EXIT_DURATION_MS = 150L 43 44 /** Floating tooltip for Taskbar education. */ 45 class TaskbarEduTooltip 46 @JvmOverloads 47 constructor( 48 context: Context, 49 attrs: AttributeSet? = null, 50 defStyleAttr: Int = 0, 51 ) : AbstractFloatingView(context, attrs, defStyleAttr) { 52 53 private val activityContext: ActivityContext = ActivityContext.lookupContext(context) 54 55 private val backgroundColor = 56 Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright) 57 58 private val tooltipCornerRadius = Themes.getDialogCornerRadius(context) 59 private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) 60 private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height) 61 private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius) 62 63 private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta) 64 private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta) 65 66 /** Container where the tooltip's body should be inflated. */ 67 lateinit var content: ViewGroup 68 private set 69 private lateinit var arrow: View 70 71 /** Callback invoked when the tooltip is being closed. */ <lambda>null72 var onCloseCallback: () -> Unit = {} 73 private var openCloseAnimator: AnimatorSet? = null 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 = findViewById(R.id.content) 101 arrow = findViewById(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 (ev?.action == ACTION_DOWN && !activityContext.dragLayer.isEventOverView(this, ev)) { 138 close(true) 139 } 140 return false 141 } 142 onDetachedFromWindownull143 override fun onDetachedFromWindow() { 144 super.onDetachedFromWindow() 145 Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) 146 } 147 closeCompletenull148 private fun closeComplete() { 149 openCloseAnimator?.cancel() 150 openCloseAnimator = null 151 mIsOpen = false 152 activityContext.dragLayer.removeView(this) 153 } 154 createOpenCloseAnimatornull155 private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet { 156 val duration: Long 157 val alphaValues: FloatArray 158 val translateYValues: FloatArray 159 val fadeInterpolator: Interpolator 160 val translateYInterpolator: Interpolator 161 162 if (isOpening) { 163 duration = ENTER_DURATION_MS 164 alphaValues = floatArrayOf(0f, 1f) 165 translateYValues = floatArrayOf(enterYDelta, 0f) 166 fadeInterpolator = STANDARD 167 translateYInterpolator = EMPHASIZED_DECELERATE 168 } else { 169 duration = EXIT_DURATION_MS 170 alphaValues = floatArrayOf(1f, 0f) 171 translateYValues = floatArrayOf(0f, exitYDelta) 172 fadeInterpolator = EMPHASIZED_ACCELERATE 173 translateYInterpolator = EMPHASIZED_ACCELERATE 174 } 175 176 val fade = 177 ValueAnimator.ofFloat(*alphaValues).apply { 178 interpolator = fadeInterpolator 179 addUpdateListener { 180 val alpha = it.animatedValue as Float 181 content.alpha = alpha 182 arrow.alpha = alpha 183 } 184 } 185 186 val translateY = 187 ValueAnimator.ofFloat(*translateYValues).apply { 188 interpolator = translateYInterpolator 189 addUpdateListener { 190 val translationY = it.animatedValue as Float 191 content.translationY = translationY 192 arrow.translationY = translationY 193 } 194 } 195 196 return AnimatorSet().apply { 197 this.duration = duration 198 playTogether(fade, translateY) 199 } 200 } 201 } 202