• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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