• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 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.photopicker.features.albumgrid
18 
19 import androidx.compose.animation.AnimatedContentTransitionScope
20 import androidx.compose.animation.EnterTransition
21 import androidx.compose.animation.ExitTransition
22 import androidx.compose.animation.slideInHorizontally
23 import androidx.compose.animation.slideOutHorizontally
24 import androidx.compose.runtime.Composable
25 import androidx.compose.ui.Modifier
26 import androidx.navigation.NamedNavArgument
27 import androidx.navigation.NavBackStackEntry
28 import androidx.navigation.NavDeepLink
29 import com.android.photopicker.core.animations.springDefaultEffectOffset
30 import com.android.photopicker.core.configuration.PhotopickerConfiguration
31 import com.android.photopicker.core.events.Event
32 import com.android.photopicker.core.events.RegisteredEventClass
33 import com.android.photopicker.core.features.FeatureManager
34 import com.android.photopicker.core.features.FeatureRegistration
35 import com.android.photopicker.core.features.FeatureToken
36 import com.android.photopicker.core.features.Location
37 import com.android.photopicker.core.features.LocationParams
38 import com.android.photopicker.core.features.PhotopickerUiFeature
39 import com.android.photopicker.core.features.PrefetchResultKey
40 import com.android.photopicker.core.features.Priority
41 import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_GRID
42 import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_MEDIA_GRID
43 import com.android.photopicker.core.navigation.Route
44 import com.android.photopicker.data.model.Group
45 import kotlinx.coroutines.Deferred
46 import kotlinx.coroutines.flow.StateFlow
47 
48 /**
49  * Feature class for the Photopicker's primary album grid.
50  *
51  * This feature adds the [ALBUM_GRID] route and [ALBUM_MEDIA_GRID] route.
52  */
53 class AlbumGridFeature : PhotopickerUiFeature {
54     companion object Registration : FeatureRegistration {
55         override val TAG: String = "PhotopickerAlbumGridFeature"
56 
isEnablednull57         override fun isEnabled(
58             config: PhotopickerConfiguration,
59             deferredPrefetchResultsMap: Map<PrefetchResultKey, Deferred<Any?>>,
60         ) = !config.flags.PICKER_SEARCH_ENABLED
61 
62         override fun build(featureManager: FeatureManager) = AlbumGridFeature()
63 
64         const val ALBUM_KEY = "selected_album"
65     }
66 
67     override val token = FeatureToken.ALBUM_GRID.token
68 
69     /** Events consumed by the Album grid */
70     override val eventsConsumed = emptySet<RegisteredEventClass>()
71 
72     /** Events produced by the Album grid */
73     override val eventsProduced =
74         setOf(
75             Event.ShowSnackbarMessage::class.java,
76             Event.LogPhotopickerUIEvent::class.java,
77             Event.LogPhotopickerAlbumOpenedUIEvent::class.java,
78         )
79 
80     override fun registerLocations(): List<Pair<Location, Int>> {
81         return listOf(Pair(Location.NAVIGATION_BAR_NAV_BUTTON, Priority.HIGH.priority))
82     }
83 
registerNavigationRoutesnull84     override fun registerNavigationRoutes(): Set<Route> {
85         return setOf(
86             // The main grid of the user's albums.
87             object : Route {
88                 override val route = ALBUM_GRID.route
89                 override val initialRoutePriority = Priority.MEDIUM.priority
90                 override val arguments = emptyList<NamedNavArgument>()
91                 override val deepLinks = emptyList<NavDeepLink>()
92                 override val isDialog = false
93                 override val dialogProperties = null
94 
95                 /*
96                 Animations for ALBUM_GRID
97                 - When navigating directly, content will slide IN from the left edge.
98                 - When navigating away, content will slide OUT towards the left edge.
99                 - When returning from the backstack, content will slide IN from the right edge.
100                 - When popping to another route on the backstack, content will slide OUT towards
101                   the left edge.
102                  */
103                 override val enterTransition:
104                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
105                     {
106                         if (initialState.destination.route == ALBUM_MEDIA_GRID.route) {
107                             // Negative value to slide right-to-left
108                             // if previous route was from ALBUM_MEDIA_GRID.
109                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it }
110                         } else {
111                             // Positive value to slide left-to-right
112                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
113                         }
114                     }
115                 override val exitTransition:
116                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
117                     {
118                         if (targetState.destination.route == ALBUM_MEDIA_GRID.route) {
119                             // Negative value to slide right-to-left
120                             // if target route is ALBUM_MEDIA_GRID
121                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it }
122                         } else {
123                             // Positive value to slide left-to-right
124                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
125                         }
126                     }
127                 override val popEnterTransition:
128                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
129                     {
130                         if (initialState.destination.route == ALBUM_MEDIA_GRID.route) {
131                             // Negative value to slide right-to-left
132                             // if previous route was from ALBUM_MEDIA_GRID.
133                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it }
134                         } else {
135                             // Positive value to slide left-to-right
136                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
137                         }
138                     }
139                 override val popExitTransition:
140                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
141                     {
142                         if (targetState.destination.route == ALBUM_MEDIA_GRID.route) {
143                             // Negative value to slide right-to-left
144                             // if target route is ALBUM_MEDIA_GRID
145                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it }
146                         } else {
147                             // Positive value to slide left-to-right
148                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
149                         }
150                     }
151 
152                 @Composable
153                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
154                     AlbumGrid()
155                 }
156             },
157             // Grid to show the album content for the album selected by the user.
158             object : Route {
159                 override val route = ALBUM_MEDIA_GRID.route
160                 override val initialRoutePriority = Priority.MEDIUM.priority
161                 override val arguments = emptyList<NamedNavArgument>()
162                 override val deepLinks = emptyList<NavDeepLink>()
163                 override val isDialog = false
164                 override val dialogProperties = null
165 
166                 /**
167                  * Animations for ALBUM_CONTENT_GRID are by default [EnterTransition.None] for
168                  * entering into view and [ExitTransition.None] while exiting.
169                  */
170                 override val enterTransition:
171                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
172                     {
173                         // Positive value to slide left-to-right
174                         slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
175                     }
176                 override val exitTransition:
177                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
178                     {
179                         slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
180                     }
181                 override val popEnterTransition:
182                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
183                     {
184                         slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
185                     }
186                 override val popExitTransition:
187                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
188                     {
189                         slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
190                     }
191 
192                 @Composable
193                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
194                     val flow: StateFlow<Group.Album?> =
195                         checkNotNull(
196                             navBackStackEntry
197                                 ?.savedStateHandle
198                                 ?.getStateFlow<Group.Album?>(ALBUM_KEY, null)
199                         ) {
200                             "Unable to get a savedStateHandle for album content grid"
201                         }
202                     AlbumMediaGrid(flow)
203                 }
204             },
205         )
206     }
207 
208     @Composable
composenull209     override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
210         when (location) {
211             Location.NAVIGATION_BAR_NAV_BUTTON -> AlbumGridNavButton(modifier)
212             else -> {}
213         }
214     }
215 }
216