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.animation.Interpolator 29 import com.android.launcher3.AbstractFloatingView 30 import com.android.launcher3.R 31 import com.android.launcher3.anim.AnimatorListeners 32 import com.android.launcher3.popup.RoundedArrowDrawable 33 import com.android.launcher3.util.Themes 34 import com.android.launcher3.views.ActivityContext 35 import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE 36 import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE 37 import com.android.systemui.animation.Interpolators.STANDARD 38 39 private const val ENTER_DURATION_MS = 300L 40 private const val EXIT_DURATION_MS = 150L 41 42 /** Floating tooltip for Taskbar education. */ 43 class TaskbarEduTooltip 44 @JvmOverloads 45 constructor( 46 context: Context, 47 attrs: AttributeSet? = null, 48 defStyleAttr: Int = 0, 49 ) : AbstractFloatingView(context, attrs, defStyleAttr) { 50 51 private val activityContext: ActivityContext = ActivityContext.lookupContext(context) 52 53 private val backgroundColor = 54 Themes.getAttrColor(context, com.android.internal.R.attr.colorSurface) 55 56 private val tooltipCornerRadius = Themes.getDialogCornerRadius(context) 57 private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) 58 private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height) 59 private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius) 60 61 private val enterYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_enter_y_delta) 62 private val exitYDelta = resources.getDimension(R.dimen.taskbar_edu_tooltip_exit_y_delta) 63 64 /** Container where the tooltip's body should be inflated. */ 65 lateinit var content: ViewGroup 66 private set 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 73 /** Animates the tooltip into view. */ shownull74 fun show() { 75 if (isOpen) { 76 return 77 } 78 mIsOpen = true 79 activityContext.dragLayer.addView(this) 80 openCloseAnimator = createOpenCloseAnimator(isOpening = true).apply { start() } 81 } 82 onFinishInflatenull83 override fun onFinishInflate() { 84 super.onFinishInflate() 85 86 content = findViewById(R.id.content) 87 arrow = findViewById(R.id.arrow) 88 arrow.background = 89 RoundedArrowDrawable( 90 arrowWidth, 91 arrowHeight, 92 arrowPointRadius, 93 tooltipCornerRadius, 94 measuredWidth.toFloat(), 95 measuredHeight.toFloat(), 96 (measuredWidth - arrowWidth) / 2, // arrowOffsetX 97 0f, // arrowOffsetY 98 false, // isPointingUp 99 true, // leftAligned 100 backgroundColor, 101 ) 102 } 103 handleClosenull104 override fun handleClose(animate: Boolean) { 105 if (!isOpen) { 106 return 107 } 108 109 onCloseCallback() 110 if (!animate) { 111 return closeComplete() 112 } 113 114 openCloseAnimator?.cancel() 115 openCloseAnimator = createOpenCloseAnimator(isOpening = false) 116 openCloseAnimator?.addListener(AnimatorListeners.forEndCallback(this::closeComplete)) 117 openCloseAnimator?.start() 118 } 119 isOfTypenull120 override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0 121 122 override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { 123 if (ev?.action == ACTION_DOWN && !activityContext.dragLayer.isEventOverView(this, ev)) { 124 close(true) 125 } 126 return false 127 } 128 onDetachedFromWindownull129 override fun onDetachedFromWindow() { 130 super.onDetachedFromWindow() 131 Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) 132 } 133 closeCompletenull134 private fun closeComplete() { 135 openCloseAnimator?.cancel() 136 openCloseAnimator = null 137 mIsOpen = false 138 activityContext.dragLayer.removeView(this) 139 } 140 createOpenCloseAnimatornull141 private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet { 142 val duration: Long 143 val alphaValues: FloatArray 144 val translateYValues: FloatArray 145 val fadeInterpolator: Interpolator 146 val translateYInterpolator: Interpolator 147 148 if (isOpening) { 149 duration = ENTER_DURATION_MS 150 alphaValues = floatArrayOf(0f, 1f) 151 translateYValues = floatArrayOf(enterYDelta, 0f) 152 fadeInterpolator = STANDARD 153 translateYInterpolator = EMPHASIZED_DECELERATE 154 } else { 155 duration = EXIT_DURATION_MS 156 alphaValues = floatArrayOf(1f, 0f) 157 translateYValues = floatArrayOf(0f, exitYDelta) 158 fadeInterpolator = EMPHASIZED_ACCELERATE 159 translateYInterpolator = EMPHASIZED_ACCELERATE 160 } 161 162 val fade = 163 ValueAnimator.ofFloat(*alphaValues).apply { 164 interpolator = fadeInterpolator 165 addUpdateListener { 166 val alpha = it.animatedValue as Float 167 content.alpha = alpha 168 arrow.alpha = alpha 169 } 170 } 171 172 val translateY = 173 ValueAnimator.ofFloat(*translateYValues).apply { 174 interpolator = translateYInterpolator 175 addUpdateListener { 176 val translationY = it.animatedValue as Float 177 content.translationY = translationY 178 arrow.translationY = translationY 179 } 180 } 181 182 return AnimatorSet().apply { 183 this.duration = duration 184 playTogether(fade, translateY) 185 } 186 } 187 } 188