1 /*
2  * Copyright (C) 2022 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.intentresolver
17 
18 import android.app.SharedElementCallback
19 import android.view.View
20 import androidx.activity.ComponentActivity
21 import androidx.lifecycle.lifecycleScope
22 import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
23 import com.android.internal.annotations.VisibleForTesting
24 import java.util.function.Supplier
25 import kotlinx.coroutines.Job
26 import kotlinx.coroutines.delay
27 import kotlinx.coroutines.launch
28 
29 /**
30  * A helper class to track app's readiness for the scene transition animation. The app is ready when
31  * both the image is laid out and the drawer offset is calculated.
32  */
33 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
34 class EnterTransitionAnimationDelegate(
35     private val activity: ComponentActivity,
36     private val transitionTargetSupplier: Supplier<View?>,
37 ) : View.OnLayoutChangeListener, TransitionElementStatusCallback {
38 
39     private val transitionElements = HashSet<String>()
40     private var previewReady = false
41     private var offsetCalculated = false
42     private var timeoutJob: Job? = null
43 
44     init {
45         activity.setEnterSharedElementCallback(
46             object : SharedElementCallback() {
onMapSharedElementsnull47                 override fun onMapSharedElements(
48                     names: MutableList<String>,
49                     sharedElements: MutableMap<String, View>
50                 ) {
51                     this@EnterTransitionAnimationDelegate.onMapSharedElements(names, sharedElements)
52                 }
53             }
54         )
55     }
56 
postponeTransitionnull57     fun postponeTransition() {
58         activity.postponeEnterTransition()
59         timeoutJob =
60             activity.lifecycleScope.launch {
61                 delay(activity.resources.getInteger(R.integer.config_shortAnimTime).toLong())
62                 onTimeout()
63             }
64     }
65 
onTimeoutnull66     private fun onTimeout() {
67         // We only mark the preview readiness and not the offset readiness
68         // (see [#markOffsetCalculated()]) as this is what legacy logic, effectively, did. We might
69         // want to review that aspect separately.
70         onAllTransitionElementsReady()
71     }
72 
onTransitionElementReadynull73     override fun onTransitionElementReady(name: String) {
74         transitionElements.add(name)
75     }
76 
onAllTransitionElementsReadynull77     override fun onAllTransitionElementsReady() {
78         timeoutJob?.cancel()
79         if (!previewReady) {
80             previewReady = true
81             maybeStartListenForLayout()
82         }
83     }
84 
markOffsetCalculatednull85     fun markOffsetCalculated() {
86         if (!offsetCalculated) {
87             offsetCalculated = true
88             maybeStartListenForLayout()
89         }
90     }
91 
onMapSharedElementsnull92     private fun onMapSharedElements(
93         names: MutableList<String>,
94         sharedElements: MutableMap<String, View>
95     ) {
96         names.removeAll { !transitionElements.contains(it) }
97         sharedElements.entries.removeAll { !transitionElements.contains(it.key) }
98     }
99 
maybeStartListenForLayoutnull100     private fun maybeStartListenForLayout() {
101         val drawer = transitionTargetSupplier.get()
102         if (previewReady && offsetCalculated && drawer != null) {
103             if (drawer.isInLayout) {
104                 startPostponedEnterTransition()
105             } else {
106                 drawer.addOnLayoutChangeListener(this)
107                 drawer.requestLayout()
108             }
109         }
110     }
111 
onLayoutChangenull112     override fun onLayoutChange(
113         v: View,
114         left: Int,
115         top: Int,
116         right: Int,
117         bottom: Int,
118         oldLeft: Int,
119         oldTop: Int,
120         oldRight: Int,
121         oldBottom: Int
122     ) {
123         v.removeOnLayoutChangeListener(this)
124         startPostponedEnterTransition()
125     }
126 
startPostponedEnterTransitionnull127     private fun startPostponedEnterTransition() {
128         if (transitionElements.isNotEmpty() && activity.isActivityTransitionRunning) {
129             // Disable the window animations as it interferes with the transition animation.
130             activity.window.setWindowAnimations(0)
131         }
132         activity.startPostponedEnterTransition()
133     }
134 }
135