• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.systemui.qs.panels.ui.compose
18 
19 import android.view.MotionEvent
20 import androidx.compose.foundation.layout.Arrangement.spacedBy
21 import androidx.compose.foundation.layout.Column
22 import androidx.compose.foundation.layout.PaddingValues
23 import androidx.compose.foundation.layout.Row
24 import androidx.compose.foundation.layout.Spacer
25 import androidx.compose.foundation.layout.fillMaxWidth
26 import androidx.compose.foundation.layout.requiredHeight
27 import androidx.compose.foundation.layout.wrapContentSize
28 import androidx.compose.foundation.layout.wrapContentWidth
29 import androidx.compose.foundation.pager.HorizontalPager
30 import androidx.compose.foundation.pager.PagerState
31 import androidx.compose.foundation.pager.rememberPagerState
32 import androidx.compose.foundation.shape.CornerSize
33 import androidx.compose.material3.MaterialTheme
34 import androidx.compose.runtime.Composable
35 import androidx.compose.runtime.LaunchedEffect
36 import androidx.compose.runtime.remember
37 import androidx.compose.runtime.snapshotFlow
38 import androidx.compose.ui.Alignment
39 import androidx.compose.ui.Modifier
40 import androidx.compose.ui.input.pointer.pointerInteropFilter
41 import androidx.compose.ui.res.integerResource
42 import androidx.compose.ui.unit.dp
43 import com.android.compose.animation.scene.ContentScope
44 import com.android.compose.modifiers.padding
45 import com.android.systemui.common.ui.compose.PagerDots
46 import com.android.systemui.compose.modifiers.sysuiResTag
47 import com.android.systemui.development.ui.compose.BuildNumber
48 import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
49 import com.android.systemui.lifecycle.rememberViewModel
50 import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
51 import com.android.systemui.qs.panels.ui.compose.Dimensions.FooterHeight
52 import com.android.systemui.qs.panels.ui.compose.Dimensions.InterPageSpacing
53 import com.android.systemui.qs.panels.ui.compose.toolbar.EditModeButton
54 import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
55 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
56 import com.android.systemui.qs.panels.ui.viewmodel.toolbar.EditModeButtonViewModel
57 import com.android.systemui.qs.ui.compose.borderOnFocus
58 import com.android.systemui.res.R
59 import javax.inject.Inject
60 
61 class PaginatedGridLayout
62 @Inject
63 constructor(
64     private val viewModelFactory: PaginatedGridViewModel.Factory,
65     @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout,
66 ) : GridLayout by delegateGridLayout {
67     @Composable
68     override fun ContentScope.TileGrid(
69         tiles: List<TileViewModel>,
70         modifier: Modifier,
71         listening: () -> Boolean,
72     ) {
73         val viewModel =
74             rememberViewModel(traceName = "PaginatedGridLayout-TileGrid") {
75                 viewModelFactory.create()
76             }
77 
78         val columns = viewModel.columns
79         val rows = integerResource(R.integer.quick_settings_paginated_grid_num_rows)
80 
81         val pages =
82             remember(tiles, columns, rows) {
83                 delegateGridLayout.splitIntoPages(tiles, rows = rows, columns = columns)
84             }
85 
86         val pagerState = rememberPagerState(0) { pages.size }
87 
88         LaunchedEffect(listening, pagerState) {
89             snapshotFlow { listening() }
90                 .collect {
91                     // Whenever we go from not listening to listening, we should be in the first
92                     // page. If we did this when going from listening to not listening, opening
93                     // edit mode in second page will cause it to go to first page during the
94                     // transition.
95                     if (listening()) {
96                         pagerState.scrollToPage(0)
97                     }
98                 }
99         }
100 
101         // Used to track if this is currently in the first page or not, for animations
102         LaunchedEffect(key1 = pagerState) {
103             snapshotFlow { pagerState.currentPage == 0 }.collect { viewModel.inFirstPage = it }
104         }
105 
106         Column {
107             val contentPaddingValue =
108                 if (pages.size > 1) {
109                     InterPageSpacing
110                 } else {
111                     0.dp
112                 }
113             val contentPadding = PaddingValues(horizontal = contentPaddingValue)
114 
115             /* Use negative padding equal with value equal to content padding. That way, each page
116              * layout extends to the sides, but the content is as if there was no padding. That
117              * way, the clipping bounds of the HorizontalPager extend beyond the tiles in each page.
118              */
119             HorizontalPager(
120                 state = pagerState,
121                 modifier =
122                     Modifier.sysuiResTag("qs_pager")
123                         .padding(horizontal = { -contentPaddingValue.roundToPx() })
124                         .pointerInteropFilter { event ->
125                             if (event.actionMasked == MotionEvent.ACTION_UP) {
126                                 viewModel.registerSideSwipeGesture()
127                             }
128                             false
129                         },
130                 contentPadding = contentPadding,
131                 pageSpacing = if (pages.size > 1) InterPageSpacing else 0.dp,
132                 beyondViewportPageCount = 1,
133                 verticalAlignment = Alignment.Top,
134             ) {
135                 val page = pages[it]
136 
137                 with(delegateGridLayout) { TileGrid(tiles = page, modifier = Modifier, listening) }
138             }
139             FooterBar(
140                 buildNumberViewModelFactory = viewModel.buildNumberViewModelFactory,
141                 pagerState = pagerState,
142                 editButtonViewModelFactory = viewModel.editModeButtonViewModelFactory,
143             )
144         }
145     }
146 }
147 
148 private object Dimensions {
149     val FooterHeight = 48.dp
150     val InterPageSpacing = 16.dp
151 }
152 
153 @Composable
FooterBarnull154 private fun FooterBar(
155     buildNumberViewModelFactory: BuildNumberViewModel.Factory,
156     pagerState: PagerState,
157     editButtonViewModelFactory: EditModeButtonViewModel.Factory,
158 ) {
159     val editButtonViewModel =
160         rememberViewModel(traceName = "PaginatedGridLayout-editButtonViewModel") {
161             editButtonViewModelFactory.create()
162         }
163 
164     // Use requiredHeight so it won't be squished if the view doesn't quite fit. As this is
165     // expected to be inside a scrollable container, this should not be an issue.
166     // Also, we construct the layout this way to do the following:
167     // * PagerDots is centered in the row, taking as much space as it needs.
168     // * On the start side, we place the BuildNumber, taking as much space as it needs, but
169     //   constrained by the available space left over after PagerDots.
170     // * On the end side, we place the edit mode button, with the same constraints as for
171     //   BuildNumber (but it will usually fit, as it's just a square button).
172     Row(
173         modifier = Modifier.requiredHeight(FooterHeight).fillMaxWidth(),
174         verticalAlignment = Alignment.CenterVertically,
175         horizontalArrangement = spacedBy(8.dp),
176     ) {
177         Row(Modifier.weight(1f)) {
178             BuildNumber(
179                 viewModelFactory = buildNumberViewModelFactory,
180                 textColor = MaterialTheme.colorScheme.onSurface,
181                 modifier =
182                     Modifier.borderOnFocus(
183                             color = MaterialTheme.colorScheme.secondary,
184                             cornerSize = CornerSize(1.dp),
185                         )
186                         .wrapContentSize(),
187             )
188             Spacer(modifier = Modifier.weight(1f))
189         }
190         PagerDots(
191             pagerState = pagerState,
192             activeColor = MaterialTheme.colorScheme.primary,
193             nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
194             modifier = Modifier.wrapContentWidth(),
195         )
196         Row(Modifier.weight(1f)) {
197             Spacer(modifier = Modifier.weight(1f))
198             EditModeButton(viewModel = editButtonViewModel)
199         }
200     }
201 }
202