1 /*
2  * Copyright (C) 2021 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 
17 package com.android.systemui.statusbar.events
18 
19 import android.animation.ValueAnimator
20 import android.content.Context
21 import android.view.Gravity
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
25 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
26 import android.widget.FrameLayout
27 import com.android.systemui.R
28 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher
29 import com.android.systemui.statusbar.window.StatusBarWindowController
30 import javax.inject.Inject
31 
32 /**
33  * Controls the view for system event animations.
34  */
35 class SystemEventChipAnimationController @Inject constructor(
36     private val context: Context,
37     private val statusBarWindowController: StatusBarWindowController,
38     private val locationPublisher: StatusBarLocationPublisher
39 ) : SystemStatusChipAnimationCallback {
40     var showPersistentDot = false
41         set(value) {
42             field = value
43             statusBarWindowController.setForceStatusBarVisible(value)
44             maybeUpdateShowDot()
45         }
46 
47     private lateinit var animationWindowView: FrameLayout
48     private lateinit var animationDotView: View
49     private var currentAnimatedView: View? = null
50 
51     // TODO: move to dagger
52     private var initialized = false
53 
54     override fun onChipAnimationStart(
55         viewCreator: (context: Context) -> View,
56         @SystemAnimationState state: Int
57     ) {
58         if (!initialized) init()
59 
60         if (state == ANIMATING_IN) {
61             currentAnimatedView = viewCreator(context)
62             animationWindowView.addView(currentAnimatedView, layoutParamsDefault())
63 
64             // We are animating IN; chip comes in from View.END
65             currentAnimatedView?.apply {
66                 val translation = width.toFloat()
67                 translationX = if (isLayoutRtl) -translation else translation
68                 alpha = 0f
69                 visibility = View.VISIBLE
70                 setPadding(locationPublisher.marginLeft, 0, locationPublisher.marginRight, 0)
71             }
72         } else {
73             // We are animating away
74             currentAnimatedView?.apply {
75                 translationX = 0f
76                 alpha = 1f
77             }
78         }
79     }
80 
81     override fun onChipAnimationEnd(@SystemAnimationState state: Int) {
82         if (state == ANIMATING_IN) {
83             // Finished animating in
84             currentAnimatedView?.apply {
85                 translationX = 0f
86                 alpha = 1f
87             }
88         } else {
89             // Finished animating away
90             currentAnimatedView?.apply {
91                 visibility = View.INVISIBLE
92             }
93             animationWindowView.removeView(currentAnimatedView)
94         }
95     }
96 
97     override fun onChipAnimationUpdate(
98         animator: ValueAnimator,
99         @SystemAnimationState state: Int
100     ) {
101         // Alpha is parameterized 0,1, and translation from (width, 0)
102         currentAnimatedView?.apply {
103             val amt = animator.animatedValue as Float
104 
105             alpha = amt
106 
107             val w = width
108             val translation = (1 - amt) * w
109             translationX = if (isLayoutRtl) -translation else translation
110         }
111     }
112 
113     private fun maybeUpdateShowDot() {
114         if (!initialized) return
115         if (!showPersistentDot && currentAnimatedView == null) {
116             animationDotView.visibility = View.INVISIBLE
117         }
118     }
119 
120     private fun init() {
121         initialized = true
122         animationWindowView = LayoutInflater.from(context)
123                 .inflate(R.layout.system_event_animation_window, null) as FrameLayout
124         animationDotView = animationWindowView.findViewById(R.id.dot_view)
125         val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
126         lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL
127         statusBarWindowController.addViewToWindow(animationWindowView, lp)
128     }
129 
130     private fun start() = if (animationWindowView.isLayoutRtl) right() else left()
131     private fun right() = locationPublisher.marginRight
132     private fun left() = locationPublisher.marginLeft
133 
134     private fun layoutParamsDefault(): FrameLayout.LayoutParams =
135         FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also {
136             it.gravity = Gravity.END or Gravity.CENTER_VERTICAL
137             it.marginStart = start()
138     }
139 }
140