1 /* <lambda>null2 * 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.Animator 19 import android.animation.AnimatorSet 20 import android.animation.ObjectAnimator 21 import android.annotation.SuppressLint 22 import android.content.Context 23 import android.graphics.Rect 24 import android.graphics.drawable.GradientDrawable 25 import android.util.AttributeSet 26 import android.util.Property 27 import android.view.Gravity 28 import android.view.MotionEvent 29 import android.view.View 30 import android.widget.LinearLayout 31 import android.widget.Switch 32 import androidx.core.view.postDelayed 33 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE 34 import com.android.launcher3.Flags 35 import com.android.launcher3.R 36 import com.android.launcher3.popup.ArrowPopup 37 import com.android.launcher3.popup.RoundedArrowDrawable 38 import com.android.launcher3.util.Themes 39 import com.android.launcher3.views.ActivityContext 40 import kotlin.math.max 41 import kotlin.math.min 42 43 /** Popup view with arrow for taskbar pinning */ 44 class TaskbarDividerPopupView<T : TaskbarActivityContext> 45 @JvmOverloads 46 constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 47 ArrowPopup<T>(context, attrs, defStyleAttr) { 48 companion object { 49 private const val TAG = "TaskbarDividerPopupView" 50 private const val DIVIDER_POPUP_CLOSING_DELAY = 333L 51 private const val DIVIDER_POPUP_CLOSING_ANIMATION_DURATION = 83L 52 53 @JvmStatic 54 fun createAndPopulate( 55 view: View, 56 taskbarActivityContext: TaskbarActivityContext, 57 horizontalPosition: Float, 58 ): TaskbarDividerPopupView<*> { 59 val taskMenuViewWithArrow = 60 taskbarActivityContext.layoutInflater.inflate( 61 R.layout.taskbar_divider_popup_menu, 62 taskbarActivityContext.dragLayer, 63 false, 64 ) as TaskbarDividerPopupView<*> 65 66 return taskMenuViewWithArrow.populateForView(view, horizontalPosition) 67 } 68 } 69 70 private lateinit var dividerView: View 71 private var horizontalPosition = 0.0f 72 private val taskbarActivityContext: TaskbarActivityContext = 73 ActivityContext.lookupContext(context) 74 75 private val popupCornerRadius = Themes.getDialogCornerRadius(context) 76 private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) 77 private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height) 78 private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius) 79 private val minPaddingFromScreenEdge = 80 resources.getDimension(R.dimen.taskbar_pinning_popup_menu_min_padding_from_screen_edge) 81 82 // TODO: add test for isTransientTaskbar & long presses divider and ensures the popup shows up. 83 private var alwaysShowTaskbarOn = !taskbarActivityContext.isTransientTaskbar 84 private var didPreferenceChange = false 85 private var verticalOffsetForPopupView = 86 resources.getDimensionPixelSize(R.dimen.taskbar_pinning_popup_menu_vertical_margin) 87 88 /** Callback invoked when the pinning popup view is closing. */ 89 var onCloseCallback: (preferenceChanged: Boolean) -> Unit = {} 90 91 init { 92 // This synchronizes the arrow and menu to open at the same time 93 mOpenChildFadeStartDelay = mOpenFadeStartDelay 94 mOpenChildFadeDuration = mOpenFadeDuration 95 mCloseFadeStartDelay = mCloseChildFadeStartDelay 96 mCloseFadeDuration = mCloseChildFadeDuration 97 } 98 99 override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_PINNING_POPUP != 0 100 101 override fun getTargetObjectLocation(outPos: Rect) { 102 popupContainer.getDescendantRectRelativeToSelf(dividerView, outPos) 103 } 104 105 @SuppressLint("UseSwitchCompatOrMaterialCode", "ClickableViewAccessibility") 106 override fun onFinishInflate() { 107 super.onFinishInflate() 108 val taskbarSwitchOption = requireViewById<LinearLayout>(R.id.taskbar_switch_option) 109 val alwaysShowTaskbarSwitch = requireViewById<Switch>(R.id.taskbar_pinning_switch) 110 val taskbarVisibilityIcon = requireViewById<View>(R.id.taskbar_pinning_visibility_icon) 111 112 alwaysShowTaskbarSwitch.isChecked = alwaysShowTaskbarOn 113 alwaysShowTaskbarSwitch.setOnTouchListener { view, event -> 114 (view.parent as View).onTouchEvent(event) 115 } 116 alwaysShowTaskbarSwitch.setOnClickListener { view -> (view.parent as View).performClick() } 117 118 if (taskbarActivityContext.isGestureNav) { 119 taskbarSwitchOption.setOnClickListener { 120 alwaysShowTaskbarSwitch.isChecked = !alwaysShowTaskbarOn 121 onClickAlwaysShowTaskbarSwitchOption() 122 } 123 } else { 124 alwaysShowTaskbarSwitch.isEnabled = false 125 } 126 127 if (!alwaysShowTaskbarSwitch.isEnabled) { 128 taskbarVisibilityIcon.background.setTint( 129 resources.getColor(android.R.color.system_neutral2_500, context.theme) 130 ) 131 } 132 } 133 134 /** Orient object as usual and then center object horizontally. */ 135 override fun orientAboutObject() { 136 super.orientAboutObject() 137 x = 138 if (Flags.showTaskbarPinningPopupFromAnywhere()) { 139 val xForCenterAlignment = horizontalPosition - measuredWidth / 2f 140 val maxX = popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge 141 when { 142 // Left-aligned popup and its arrow pointing to the event position if there is 143 // not enough space to center it. 144 xForCenterAlignment < minPaddingFromScreenEdge -> 145 max( 146 minPaddingFromScreenEdge, 147 horizontalPosition - mArrowOffsetHorizontal - mArrowWidth / 2, 148 ) 149 150 // Right-aligned popup and its arrow pointing to the event position if there 151 // is not enough space to center it. 152 xForCenterAlignment > maxX -> 153 min( 154 horizontalPosition - measuredWidth + 155 mArrowOffsetHorizontal + 156 mArrowWidth / 2, 157 popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge, 158 ) 159 160 // Default alignment where the popup and its arrow are centered relative to the 161 // event position. 162 else -> xForCenterAlignment 163 } 164 } else { 165 mTempRect.centerX() - measuredWidth / 2f 166 } 167 } 168 169 override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { 170 if (ev?.action == MotionEvent.ACTION_DOWN) { 171 if (!popupContainer.isEventOverView(this, ev)) { 172 close(true) 173 } 174 } else if (popupContainer.isEventOverView(dividerView, ev)) { 175 return true 176 } 177 return false 178 } 179 180 private fun populateForView(view: View, horizontalPosition: Float): TaskbarDividerPopupView<*> { 181 dividerView = view 182 this@TaskbarDividerPopupView.horizontalPosition = horizontalPosition 183 tryUpdateBackground() 184 return this 185 } 186 187 /** Updates the text background to match the shape of this background (when applicable). */ 188 private fun tryUpdateBackground() { 189 if (background !is GradientDrawable) { 190 return 191 } 192 val background = background as GradientDrawable 193 val color = context.getColor(R.color.popup_shade_first) 194 val backgroundMask = GradientDrawable() 195 backgroundMask.setColor(color) 196 backgroundMask.shape = GradientDrawable.RECTANGLE 197 if (background.cornerRadii != null) { 198 backgroundMask.cornerRadii = background.cornerRadii 199 } else { 200 backgroundMask.cornerRadius = background.cornerRadius 201 } 202 203 setBackground(backgroundMask) 204 } 205 206 override fun addArrow() { 207 super.addArrow() 208 if (Flags.showTaskbarPinningPopupFromAnywhere()) { 209 mArrow.x = 210 min( 211 max( 212 minPaddingFromScreenEdge + mArrowOffsetHorizontal, 213 horizontalPosition - mArrowWidth / 2, 214 ), 215 popupContainer.getWidth() - 216 minPaddingFromScreenEdge - 217 mArrowOffsetHorizontal - 218 mArrowWidth, 219 ) 220 } else { 221 val location = IntArray(2) 222 popupContainer.getLocationInDragLayer(dividerView, location) 223 val dividerViewX = location[0].toFloat() 224 // Change arrow location to the middle of popup. 225 mArrow.x = (dividerViewX + dividerView.width / 2) - (mArrowWidth / 2) 226 } 227 } 228 229 override fun updateArrowColor() { 230 if (Flags.showTaskbarPinningPopupFromAnywhere()) { 231 super.updateArrowColor() 232 } else if (!Gravity.isVertical(mGravity)) { 233 mArrow.background = 234 RoundedArrowDrawable( 235 arrowWidth, 236 arrowHeight, 237 arrowPointRadius, 238 popupCornerRadius, 239 measuredWidth.toFloat(), 240 measuredHeight.toFloat(), 241 (measuredWidth - arrowWidth) / 2, // arrowOffsetX 242 -mArrowOffsetVertical.toFloat(), // arrowOffsetY 243 false, // isPointingUp 244 true, // leftAligned 245 context.getColor(R.color.popup_shade_first), 246 ) 247 elevation = mElevation 248 mArrow.elevation = mElevation 249 } 250 } 251 252 override fun getExtraVerticalOffset(): Int { 253 return (mActivityContext.deviceProfile.taskbarHeight - 254 mActivityContext.deviceProfile.taskbarIconSize) / 2 + verticalOffsetForPopupView 255 } 256 257 override fun onCreateCloseAnimation(anim: AnimatorSet?) { 258 // If taskbar pinning preference changed insert custom close animation for popup menu. 259 if (didPreferenceChange) { 260 mOpenCloseAnimator = getCloseAnimator() 261 } 262 onCloseCallback(didPreferenceChange) 263 onCloseCallback = {} 264 } 265 266 /** Aligning the view pivot to center for animation. */ 267 override fun setPivotForOpenCloseAnimation() { 268 pivotX = mArrow.x + mArrowWidth / 2 - x 269 pivotY = measuredHeight.toFloat() 270 } 271 272 private fun getCloseAnimator(): AnimatorSet { 273 val alphaValues = floatArrayOf(1f, 0f) 274 val translateYValue = 275 if (!alwaysShowTaskbarOn) verticalOffsetForPopupView else -verticalOffsetForPopupView 276 val alpha = getAnimatorOfFloat(this, ALPHA, *alphaValues) 277 val arrowAlpha = getAnimatorOfFloat(mArrow, ALPHA, *alphaValues) 278 val translateY = 279 ObjectAnimator.ofFloat( 280 this, 281 TRANSLATION_Y, 282 *floatArrayOf(this.translationY, this.translationY + translateYValue), 283 ) 284 val arrowTranslateY = 285 ObjectAnimator.ofFloat( 286 mArrow, 287 TRANSLATION_Y, 288 *floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue), 289 ) 290 val animatorSet = AnimatorSet() 291 animatorSet.playTogether(alpha, arrowAlpha, translateY, arrowTranslateY) 292 return animatorSet 293 } 294 295 private fun getAnimatorOfFloat( 296 view: View, 297 property: Property<View, Float>, 298 vararg values: Float, 299 ): Animator { 300 val animator: Animator = ObjectAnimator.ofFloat(view, property, *values) 301 animator.setDuration(DIVIDER_POPUP_CLOSING_ANIMATION_DURATION) 302 animator.interpolator = EMPHASIZED_ACCELERATE 303 return animator 304 } 305 306 private fun onClickAlwaysShowTaskbarSwitchOption() { 307 didPreferenceChange = true 308 // Allow switch animation to finish and then close the popup. 309 postDelayed(DIVIDER_POPUP_CLOSING_DELAY) { 310 if (isOpen) { 311 close(true) 312 } 313 } 314 } 315 } 316