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 package com.android.systemui.shared.animation
17 
18 import android.graphics.Point
19 import android.view.Surface
20 import android.view.View
21 import android.view.WindowManager
22 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
23 import java.lang.ref.WeakReference
24 
25 /**
26  * Creates an animation where all registered views are moved into their final location
27  * by moving from the center of the screen to the sides
28  */
29 class UnfoldMoveFromCenterAnimator @JvmOverloads constructor(
30     private val windowManager: WindowManager,
31     /**
32      * Allows to set custom translation applier
33      * Could be useful when a view could be translated from
34      * several sources and we want to set the translation
35      * using custom methods instead of [View.setTranslationX] or
36      * [View.setTranslationY]
37      */
38     private val translationApplier: TranslationApplier = object : TranslationApplier {},
39     /**
40      * Allows to set custom implementation for getting
41      * view location. Could be useful if logical view bounds
42      * are different than actual bounds (e.g. view container may
43      * have larger width than width of the items in the container)
44      */
45     private val viewCenterProvider: ViewCenterProvider = object : ViewCenterProvider {}
46 ) : UnfoldTransitionProgressProvider.TransitionProgressListener {
47 
48     private val screenSize = Point()
49     private var isVerticalFold = false
50 
51     private val animatedViews: MutableList<AnimatedView> = arrayListOf()
52 
53     private var lastAnimationProgress: Float = 0f
54 
55     /**
56      * Updates display properties in order to calculate the initial position for the views
57      * Must be called before [registerViewForAnimation]
58      */
59     fun updateDisplayProperties() {
60         windowManager.defaultDisplay.getSize(screenSize)
61 
62         // Simple implementation to get current fold orientation,
63         // this might not be correct on all devices
64         // TODO: use JetPack WindowManager library to get the fold orientation
65         isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 ||
66             windowManager.defaultDisplay.rotation == Surface.ROTATION_180
67     }
68 
69     /**
70      * If target view positions have changed (e.g. because of layout changes) call this method
71      * to re-query view positions and update the translations
72      */
73     fun updateViewPositions() {
74         animatedViews.forEach { animatedView ->
75             animatedView.view.get()?.let {
76                 animatedView.updateAnimatedView(it)
77             }
78         }
79         onTransitionProgress(lastAnimationProgress)
80     }
81 
82     /**
83      * Registers a view to be animated, the view should be measured and layouted
84      * After finishing the animation it is necessary to clear
85      * the views using [clearRegisteredViews]
86      */
87     fun registerViewForAnimation(view: View) {
88         val animatedView = createAnimatedView(view)
89         animatedViews.add(animatedView)
90     }
91 
92     /**
93      * Unregisters all registered views and resets their translation
94      */
95     fun clearRegisteredViews() {
96         onTransitionProgress(1f)
97         animatedViews.clear()
98     }
99 
100     override fun onTransitionProgress(progress: Float) {
101         animatedViews.forEach {
102             it.view.get()?.let { view ->
103                 translationApplier.apply(
104                     view = view,
105                     x = it.startTranslationX * (1 - progress),
106                     y = it.startTranslationY * (1 - progress)
107                 )
108             }
109         }
110         lastAnimationProgress = progress
111     }
112 
113     private fun createAnimatedView(view: View): AnimatedView =
114         AnimatedView(view = WeakReference(view)).updateAnimatedView(view)
115 
116     private fun AnimatedView.updateAnimatedView(view: View): AnimatedView {
117         val viewCenter = Point()
118         viewCenterProvider.getViewCenter(view, viewCenter)
119 
120         val viewCenterX = viewCenter.x
121         val viewCenterY = viewCenter.y
122 
123         if (isVerticalFold) {
124             val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX
125             startTranslationX = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
126             startTranslationY = 0f
127         } else {
128             val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY
129             startTranslationX = 0f
130             startTranslationY = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
131         }
132 
133         return this
134     }
135 
136     /**
137      * Interface that allows to use custom logic to apply translation to view
138      */
139     interface TranslationApplier {
140         /**
141          * Called when we need to apply [x] and [y] translation to [view]
142          */
143         fun apply(view: View, x: Float, y: Float) {
144             view.translationX = x
145             view.translationY = y
146         }
147     }
148 
149     /**
150      * Interface that allows to use custom logic to get the center of the view
151      */
152     interface ViewCenterProvider {
153         /**
154          * Called when we need to get the center of the view
155          */
156         fun getViewCenter(view: View, outPoint: Point) {
157             val viewLocation = IntArray(2)
158             view.getLocationOnScreen(viewLocation)
159 
160             val viewX = viewLocation[0]
161             val viewY = viewLocation[1]
162 
163             outPoint.x = viewX + view.width / 2
164             outPoint.y = viewY + view.height / 2
165         }
166     }
167 
168     private class AnimatedView(
169         val view: WeakReference<View>,
170         var startTranslationX: Float = 0f,
171         var startTranslationY: Float = 0f
172     )
173 }
174 
175 private const val TRANSLATION_PERCENTAGE = 0.3f
176