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.R
35 import com.android.launcher3.popup.ArrowPopup
36 import com.android.launcher3.popup.RoundedArrowDrawable
37 import com.android.launcher3.util.DisplayController
38 import com.android.launcher3.util.Themes
39 import com.android.launcher3.views.ActivityContext
40 
41 /** Popup view with arrow for taskbar pinning */
42 class TaskbarDividerPopupView<T : TaskbarActivityContext>
43 @JvmOverloads
44 constructor(
45     context: Context,
46     attrs: AttributeSet? = null,
47     defStyleAttr: Int = 0,
48 ) : ArrowPopup<T>(context, attrs, defStyleAttr) {
49     companion object {
50         private const val TAG = "TaskbarDividerPopupView"
51         private const val DIVIDER_POPUP_CLOSING_DELAY = 333L
52         private const val DIVIDER_POPUP_CLOSING_ANIMATION_DURATION = 83L
53 
54         @JvmStatic
55         fun createAndPopulate(
56             view: View,
57             taskbarActivityContext: TaskbarActivityContext,
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)
67         }
68     }
69 
70     private lateinit var dividerView: View
71 
72     private val popupCornerRadius = Themes.getDialogCornerRadius(context)
73     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
74     private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height)
75     private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius)
76 
77     private var alwaysShowTaskbarOn = !DisplayController.isTransientTaskbar(context)
78     private var didPreferenceChange = false
79     private var verticalOffsetForPopupView =
80         resources.getDimensionPixelSize(R.dimen.taskbar_pinning_popup_menu_vertical_margin)
81 
82     /** Callback invoked when the pinning popup view is closing. */
83     var onCloseCallback: (preferenceChanged: Boolean) -> Unit = {}
84 
85     init {
86         // This synchronizes the arrow and menu to open at the same time
87         mOpenChildFadeStartDelay = mOpenFadeStartDelay
88         mOpenChildFadeDuration = mOpenFadeDuration
89         mCloseFadeStartDelay = mCloseChildFadeStartDelay
90         mCloseFadeDuration = mCloseChildFadeDuration
91     }
92 
93     override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_PINNING_POPUP != 0
94 
95     override fun getTargetObjectLocation(outPos: Rect) {
96         popupContainer.getDescendantRectRelativeToSelf(dividerView, outPos)
97     }
98 
99     @SuppressLint("UseSwitchCompatOrMaterialCode", "ClickableViewAccessibility")
100     override fun onFinishInflate() {
101         super.onFinishInflate()
102         val taskbarSwitchOption = requireViewById<LinearLayout>(R.id.taskbar_switch_option)
103         val alwaysShowTaskbarSwitch = requireViewById<Switch>(R.id.taskbar_pinning_switch)
104         val taskbarVisibilityIcon = requireViewById<View>(R.id.taskbar_pinning_visibility_icon)
105 
106         alwaysShowTaskbarSwitch.isChecked = alwaysShowTaskbarOn
107         alwaysShowTaskbarSwitch.setOnTouchListener { view, event ->
108             (view.parent as View).onTouchEvent(event)
109         }
110         alwaysShowTaskbarSwitch.setOnClickListener { view -> (view.parent as View).performClick() }
111 
112         if (ActivityContext.lookupContext<TaskbarActivityContext>(context).isGestureNav) {
113             taskbarSwitchOption.setOnClickListener {
114                 alwaysShowTaskbarSwitch.isChecked = !alwaysShowTaskbarOn
115                 onClickAlwaysShowTaskbarSwitchOption()
116             }
117         } else {
118             alwaysShowTaskbarSwitch.isEnabled = false
119         }
120 
121         if (!alwaysShowTaskbarSwitch.isEnabled) {
122             taskbarVisibilityIcon.background.setTint(
123                 resources.getColor(android.R.color.system_neutral2_500, context.theme)
124             )
125         }
126     }
127 
128     /** Orient object as usual and then center object horizontally. */
129     override fun orientAboutObject() {
130         super.orientAboutObject()
131         x = mTempRect.centerX() - measuredWidth / 2f
132     }
133 
134     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
135         if (ev?.action == MotionEvent.ACTION_DOWN) {
136             if (!popupContainer.isEventOverView(this, ev)) {
137                 close(true)
138             }
139         } else if (popupContainer.isEventOverView(dividerView, ev)) {
140             return true
141         }
142         return false
143     }
144 
145     private fun populateForView(view: View): TaskbarDividerPopupView<*> {
146         dividerView = view
147         tryUpdateBackground()
148         return this
149     }
150 
151     /** Updates the text background to match the shape of this background (when applicable). */
152     private fun tryUpdateBackground() {
153         if (background !is GradientDrawable) {
154             return
155         }
156         val background = background as GradientDrawable
157         val color = context.getColor(R.color.popup_shade_first)
158         val backgroundMask = GradientDrawable()
159         backgroundMask.setColor(color)
160         backgroundMask.shape = GradientDrawable.RECTANGLE
161         if (background.cornerRadii != null) {
162             backgroundMask.cornerRadii = background.cornerRadii
163         } else {
164             backgroundMask.cornerRadius = background.cornerRadius
165         }
166 
167         setBackground(backgroundMask)
168     }
169 
170     override fun addArrow() {
171         super.addArrow()
172         // Change arrow location to the middle of popup.
173         mArrow.x = (dividerView.x + dividerView.width / 2) - (mArrowWidth / 2)
174     }
175 
176     override fun updateArrowColor() {
177         if (!Gravity.isVertical(mGravity)) {
178             mArrow.background =
179                 RoundedArrowDrawable(
180                     arrowWidth,
181                     arrowHeight,
182                     arrowPointRadius,
183                     popupCornerRadius,
184                     measuredWidth.toFloat(),
185                     measuredHeight.toFloat(),
186                     (measuredWidth - arrowWidth) / 2, // arrowOffsetX
187                     -mArrowOffsetVertical.toFloat(), // arrowOffsetY
188                     false, // isPointingUp
189                     true, // leftAligned
190                     context.getColor(R.color.popup_shade_first),
191                 )
192             elevation = mElevation
193             mArrow.elevation = mElevation
194         }
195     }
196 
197     override fun getExtraVerticalOffset(): Int {
198         return (mActivityContext.deviceProfile.taskbarHeight -
199             mActivityContext.deviceProfile.taskbarIconSize) / 2 + verticalOffsetForPopupView
200     }
201 
202     override fun onCreateCloseAnimation(anim: AnimatorSet?) {
203         // If taskbar pinning preference changed insert custom close animation for popup menu.
204         if (didPreferenceChange) {
205             mOpenCloseAnimator = getCloseAnimator()
206         }
207         onCloseCallback(didPreferenceChange)
208         onCloseCallback = {}
209     }
210 
211     /** Aligning the view pivot to center for animation. */
212     override fun setPivotForOpenCloseAnimation() {
213         pivotX = measuredWidth / 2f
214         pivotY = measuredHeight.toFloat()
215     }
216 
217     private fun getCloseAnimator(): AnimatorSet {
218         val alphaValues = floatArrayOf(1f, 0f)
219         val translateYValue =
220             if (!alwaysShowTaskbarOn) verticalOffsetForPopupView else -verticalOffsetForPopupView
221         val alpha = getAnimatorOfFloat(this, ALPHA, *alphaValues)
222         val arrowAlpha = getAnimatorOfFloat(mArrow, ALPHA, *alphaValues)
223         val translateY =
224             ObjectAnimator.ofFloat(
225                 this,
226                 TRANSLATION_Y,
227                 *floatArrayOf(this.translationY, this.translationY + translateYValue)
228             )
229         val arrowTranslateY =
230             ObjectAnimator.ofFloat(
231                 mArrow,
232                 TRANSLATION_Y,
233                 *floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue)
234             )
235         val animatorSet = AnimatorSet()
236         animatorSet.playTogether(alpha, arrowAlpha, translateY, arrowTranslateY)
237         return animatorSet
238     }
239 
240     private fun getAnimatorOfFloat(
241         view: View,
242         property: Property<View, Float>,
243         vararg values: Float
244     ): Animator {
245         val animator: Animator = ObjectAnimator.ofFloat(view, property, *values)
246         animator.setDuration(DIVIDER_POPUP_CLOSING_ANIMATION_DURATION)
247         animator.interpolator = EMPHASIZED_ACCELERATE
248         return animator
249     }
250 
251     private fun onClickAlwaysShowTaskbarSwitchOption() {
252         didPreferenceChange = true
253         // Allow switch animation to finish and then close the popup.
254         postDelayed(DIVIDER_POPUP_CLOSING_DELAY) {
255             if (isOpen) {
256                 close(true)
257             }
258         }
259     }
260 }
261