• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 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.categorygrid
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
42 import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_GRID
43 import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_MEDIA_GRID
44 import com.android.photopicker.core.navigation.PhotopickerDestinations.PHOTO_GRID
45 import com.android.photopicker.core.navigation.Route
46 import com.android.photopicker.data.model.Group
47 import kotlinx.coroutines.Deferred
48 import kotlinx.coroutines.flow.StateFlow
49 
50 /**
51  * Feature class for the Photopicker's main category grid.
52  *
53  * This feature adds the [CategoryGrid] route and [MediaSet] route.
54  */
55 class CategoryGridFeature : PhotopickerUiFeature {
56     companion object Registration : FeatureRegistration {
57         override val TAG: String = "PhotoPickerCategoryGridFeature"
58 
isEnablednull59         override fun isEnabled(
60             config: PhotopickerConfiguration,
61             deferredPrefetchResultsMap: Map<PrefetchResultKey, Deferred<Any?>>,
62         ) = config.flags.PICKER_SEARCH_ENABLED
63 
64         override fun build(featureManager: FeatureManager) = CategoryGridFeature()
65 
66         const val GROUP_KEY = "selected_group"
67     }
68 
69     override val token = FeatureToken.CATEGORY_GRID.token
70 
71     /** Events consumed by the Category grid */
72     override val eventsConsumed = emptySet<RegisteredEventClass>()
73 
74     /** Events produced by the Category grid */
75     override val eventsProduced =
76         setOf(
77             Event.ShowSnackbarMessage::class.java,
78             Event.LogPhotopickerUIEvent::class.java,
79             Event.LogPhotopickerAlbumOpenedUIEvent::class.java,
80             Event.LogPhotopickerPageInfo::class.java,
81         )
82 
83     override fun registerLocations(): List<Pair<Location, Int>> {
84         return listOf(Pair(Location.NAVIGATION_BAR_NAV_BUTTON, Priority.HIGHEST.priority))
85     }
86 
registerNavigationRoutesnull87     override fun registerNavigationRoutes(): Set<Route> {
88         return setOf(
89             // The main grid of the user's category.
90             object : Route {
91                 override val route = ALBUM_GRID.route
92                 override val initialRoutePriority = Priority.HIGH.priority
93                 override val arguments = emptyList<NamedNavArgument>()
94                 override val deepLinks = emptyList<NavDeepLink>()
95                 override val isDialog = false
96                 override val dialogProperties = null
97 
98                 /*
99                 Animations for ALBUM_GRID for CategoryGridFeature
100                 - When navigating directly, content will slide IN from the left edge.
101                 - When navigating away, content will slide OUT towards the left edge.
102                 - When returning from the backstack, content will slide IN from the right edge.
103                 - When popping to another route on the backstack, content will slide OUT towards
104                   the left edge.
105                  */
106                 override val enterTransition:
107                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
108                     {
109                         if (initialState.destination.route == PHOTO_GRID.route) {
110                             // Positive value to slide left-to-right
111                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
112                         } else {
113                             // Negative value to slide right-to-left
114                             // if previous route was not from PHOTO_GRID.
115                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it }
116                         }
117                     }
118                 override val exitTransition:
119                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
120                     {
121                         if (targetState.destination.route == PHOTO_GRID.route) {
122                             // Positive value to slide left-to-right
123                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
124                         } else {
125                             // Negative value to slide right-to-left
126                             // if target route is not PHOTO_GRID
127                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it }
128                         }
129                     }
130                 override val popEnterTransition:
131                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
132                     {
133                         if (initialState.destination.route == PHOTO_GRID.route) {
134                             // Positive value to slide left-to-right
135                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
136                         } else {
137                             // Negative value to slide right-to-left
138                             // if previous route was not from PHOTO_GRID.
139                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it }
140                         }
141                     }
142                 override val popExitTransition:
143                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
144                     {
145                         if (targetState.destination.route == PHOTO_GRID.route) {
146                             // Positive value to slide left-to-right
147                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
148                         } else {
149                             // Negative value to slide right-to-left
150                             // if target route is not PHOTO_GRID
151                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it }
152                         }
153                     }
154 
155                 @Composable
156                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
157                     CategoryGrid()
158                 }
159             },
160             // Grid to show the media sets for the category selected by the user.
161             object : Route {
162                 override val route = PhotopickerDestinations.MEDIA_SET_GRID.route
163                 override val initialRoutePriority = Priority.MEDIUM.priority
164                 override val arguments = emptyList<NamedNavArgument>()
165                 override val deepLinks = emptyList<NavDeepLink>()
166                 override val isDialog = false
167                 override val dialogProperties = null
168 
169                 /**
170                  * Animations for MEDIA_SET_GRID are by default [EnterTransition.None] for entering
171                  * into view and [ExitTransition.None] while exiting.
172                  */
173                 override val enterTransition:
174                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
175                     {
176                         if (
177                             initialState.destination.route !=
178                                 PhotopickerDestinations.MEDIA_SET_CONTENT_GRID.route
179                         ) {
180                             // Positive value to slide left-to-right
181                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
182                         } else {
183                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it }
184                         }
185                     }
186                 override val exitTransition:
187                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
188                     {
189                         if (
190                             targetState.destination.route !=
191                                 PhotopickerDestinations.MEDIA_SET_CONTENT_GRID.route
192                         ) {
193                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
194                         } else {
195                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it }
196                         }
197                     }
198                 override val popEnterTransition:
199                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
200                     {
201                         if (
202                             initialState.destination.route !=
203                                 PhotopickerDestinations.MEDIA_SET_CONTENT_GRID.route
204                         ) {
205                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
206                         } else {
207                             slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it }
208                         }
209                     }
210                 override val popExitTransition:
211                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
212                     {
213                         if (
214                             targetState.destination.route !=
215                                 PhotopickerDestinations.MEDIA_SET_CONTENT_GRID.route
216                         ) {
217                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
218                         } else {
219                             slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it }
220                         }
221                     }
222 
223                 @Composable
224                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
225                     val flow: StateFlow<Group.Category?> =
226                         checkNotNull(
227                             navBackStackEntry
228                                 ?.savedStateHandle
229                                 ?.getStateFlow<Group.Category?>(GROUP_KEY, null)
230                         ) {
231                             "Unable to get a savedStateHandle for media set grid"
232                         }
233                     MediaSetGrid(flow)
234                 }
235             },
236             // Grid to show the album content for the album selected by the user.
237             object : Route {
238                 override val route = ALBUM_MEDIA_GRID.route
239                 override val initialRoutePriority = Priority.MEDIUM.priority
240                 override val arguments = emptyList<NamedNavArgument>()
241                 override val deepLinks = emptyList<NavDeepLink>()
242                 override val isDialog = false
243                 override val dialogProperties = null
244 
245                 /**
246                  * Animations for CATEGORY_CONTENT_GRID are by default [EnterTransition.None] for
247                  * entering into view and [ExitTransition.None] while exiting.
248                  */
249                 override val enterTransition:
250                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
251                     {
252                         // Positive value to slide left-to-right
253                         slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
254                     }
255                 override val exitTransition:
256                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
257                     {
258                         slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
259                     }
260                 override val popEnterTransition:
261                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
262                     {
263                         slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
264                     }
265                 override val popExitTransition:
266                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
267                     {
268                         slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
269                     }
270 
271                 @Composable
272                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
273                     val flow: StateFlow<Group.Album?> =
274                         checkNotNull(
275                             navBackStackEntry
276                                 ?.savedStateHandle
277                                 ?.getStateFlow<Group.Album?>(GROUP_KEY, null)
278                         ) {
279                             "Unable to get a savedStateHandle for album content grid"
280                         }
281                     AlbumMediaGrid(flow)
282                 }
283             },
284             // Grid to show the media set content for the media set selected by the user.
285             object : Route {
286                 override val route = PhotopickerDestinations.MEDIA_SET_CONTENT_GRID.route
287                 override val initialRoutePriority = Priority.MEDIUM.priority
288                 override val arguments = emptyList<NamedNavArgument>()
289                 override val deepLinks = emptyList<NavDeepLink>()
290                 override val isDialog = false
291                 override val dialogProperties = null
292 
293                 /**
294                  * Animations for CATEGORY_CONTENT_GRID are by default [EnterTransition.None] for
295                  * entering into view and [ExitTransition.None] while exiting.
296                  */
297                 override val enterTransition:
298                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
299                     {
300                         // Positive value to slide left-to-right
301                         slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
302                     }
303                 override val exitTransition:
304                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
305                     {
306                         slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
307                     }
308                 override val popEnterTransition:
309                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? =
310                     {
311                         slideInHorizontally(animationSpec = springDefaultEffectOffset) { it }
312                     }
313                 override val popExitTransition:
314                     (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? =
315                     {
316                         slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it }
317                     }
318 
319                 @Composable
320                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
321                     val flow: StateFlow<Group.MediaSet?> =
322                         checkNotNull(
323                             navBackStackEntry
324                                 ?.savedStateHandle
325                                 ?.getStateFlow<Group.MediaSet?>(GROUP_KEY, null)
326                         ) {
327                             "Unable to get a savedStateHandle for album content grid"
328                         }
329                     MediaSetContentGrid(flow)
330                 }
331             },
332         )
333     }
334 
335     @Composable
composenull336     override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
337         when (location) {
338             Location.NAVIGATION_BAR_NAV_BUTTON -> CategoryButton(modifier)
339             else -> {}
340         }
341     }
342 }
343