1 /*
2  * Copyright (C) 2023 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.car.carlauncher.pagination;
18 
19 import android.view.View;
20 
21 import com.android.car.carlauncher.AppGridConstants;
22 import com.android.car.carlauncher.AppGridConstants.PageOrientation;
23 import com.android.car.carlauncher.R;
24 import com.android.car.carlauncher.recyclerview.PageMarginDecoration;
25 
26 /**
27  * Helper class for PaginationController that computes the measurements of app grid and app items.
28  */
29 public class PageMeasurementHelper {
30     @PageOrientation
31     private final int mPageOrientation;
32     private final boolean mUseDefinedDimensions;
33     private final int mDefinedWidth;
34     private final int mDefinedHeight;
35     private final int mDefinedMarginHorizontal;
36     private final int mDefinedMarginVertical;
37     private final int mDefinedPageIndicatorSize;
38     private final int mMinItemWidth;
39     private final int mMinItemHeight;
40 
41     private int mWindowWidth;
42     private int mWindowHeight;
43     private GridDimensions mGridDimensions;
44     private PageDimensions mPageDimensions;
45 
PageMeasurementHelper(View windowBackground)46     public PageMeasurementHelper(View windowBackground) {
47         mPageOrientation = windowBackground.getResources().getBoolean(R.bool.use_vertical_app_grid)
48                 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL;
49         mUseDefinedDimensions = windowBackground.getResources().getBoolean(
50                 R.bool.use_defined_app_grid_dimensions);
51         mDefinedWidth = windowBackground.getResources().getDimensionPixelSize(
52                 R.dimen.app_grid_width);
53         mDefinedHeight = windowBackground.getResources().getDimensionPixelSize(
54                 R.dimen.app_grid_height);
55         mDefinedMarginHorizontal = windowBackground.getResources().getDimensionPixelSize(
56                 R.dimen.app_grid_margin_horizontal);
57         mDefinedMarginVertical = windowBackground.getResources().getDimensionPixelSize(
58                 R.dimen.app_grid_margin_vertical);
59         mDefinedPageIndicatorSize = windowBackground.getResources().getDimensionPixelSize(
60                 R.dimen.page_indicator_height);
61         mMinItemWidth = windowBackground.getResources().getDimensionPixelSize(
62                 R.dimen.car_app_selector_column_min_width);
63         mMinItemHeight = windowBackground.getResources().getDimensionPixelSize(
64                 R.dimen.car_app_selector_column_min_height);
65     }
66 
67     /**
68      * @return the most recently updated app grid dimension, or {@code null} if
69      * {@link PageMeasurementHelper#handleWindowSizeChange} was never called.
70      */
getGridDimensions()71     public GridDimensions getGridDimensions() {
72         return mGridDimensions;
73     }
74 
75     /**
76      * @return the most recently updated page margin decoration or {@code null} if
77      * {@link PageMeasurementHelper#handleWindowSizeChange} was never called.
78      */
getPageDimensions()79     public PageDimensions getPageDimensions() {
80         return mPageDimensions;
81     }
82 
83     /**
84      * Handles window dimension change by calculating spaces available for the app grid. Returns
85      * {@code true} if the new measurements is different, and {@code false} otherwise.
86      *
87      * If dimensions has changed, it is the caller's responsibility to retrieve the page and grid
88      * dimensions and update their respective layout params. {@link PageMarginDecoration} should
89      * also be recreated and reattached to redraw the page margins.
90      *
91      * @param windowWidth width available for app grid in px.
92      * @param windowHeight height available for app grid to fill in px.
93      * @return true if the
94      */
handleWindowSizeChange(int windowWidth, int windowHeight)95     public boolean handleWindowSizeChange(int windowWidth, int windowHeight) {
96         if (mUseDefinedDimensions) {
97             windowWidth = mDefinedWidth;
98             windowHeight = mDefinedHeight;
99         }
100         boolean consumed = windowWidth != mWindowWidth || windowHeight != mWindowHeight;
101         if (consumed) {
102             mWindowWidth = windowWidth;
103             mWindowHeight = windowHeight;
104             // Step 1: calculate the width and height available to for the grid layout by accounting
105             // for spaces required to place page indicator and page margins.
106             int gridWidth = windowWidth - mDefinedMarginHorizontal * 2
107                     - (isHorizontal() ? 0 : mDefinedPageIndicatorSize);
108             int gridHeight = windowHeight - mDefinedMarginVertical * 2
109                     - (isHorizontal() ? mDefinedPageIndicatorSize : 0);
110 
111             // Step 2: Round the measurements to ensure child view holder cells have an exact fit.
112 
113             // Calculate the maximum number of columns that can fit in the grid,
114             // ensuring each column has at least the minimum item width.
115             int numOfCols = gridWidth / mMinItemWidth;
116             gridWidth = roundDownToModuloMultiple(gridWidth, numOfCols);
117 
118             // Calculate the maximum number of columns that can fit in the grid,
119             // ensuring each column has at least the minimum item width.
120             int numOfRows = gridHeight / mMinItemHeight;
121             gridHeight = roundDownToModuloMultiple(gridHeight, numOfRows);
122 
123             int cellWidth = gridWidth / numOfCols;
124             int cellHeight = gridHeight / numOfRows;
125             mGridDimensions = new GridDimensions(gridWidth, gridHeight, cellWidth, cellHeight,
126                     numOfRows, numOfCols);
127 
128             // Step 3: Since the grid dimens are rounded, we need to recalculate the margins.
129             int marginHorizontal = (windowWidth - gridWidth) / 2;
130             int marginVertical = (windowHeight - gridHeight) / 2;
131 
132             // Step 4: Calculate RecyclerView and PageIndicator dimens for layout params.
133             int recyclerViewWidth, recyclerViewHeight;
134             int pageIndicatorWidth, pageIndicatorHeight;
135             if (isHorizontal()) {
136                 // horizontal app grid should have HORIZONTAL page indicator bar and the
137                 // recyclerview width should span the entire window to not clip off the page margin
138                 recyclerViewWidth = windowWidth;
139                 recyclerViewHeight = gridHeight;
140                 pageIndicatorWidth = gridWidth;
141                 pageIndicatorHeight = mDefinedPageIndicatorSize;
142             } else {
143                 // vertical app grid should have VERTICAL page indicator bar and the
144                 // recyclerview height should span the entire window to not clip off the page margin
145                 recyclerViewWidth = gridWidth;
146                 recyclerViewHeight = windowHeight;
147                 pageIndicatorWidth = mDefinedPageIndicatorSize;
148                 pageIndicatorHeight = gridHeight;
149             }
150             mPageDimensions = new PageDimensions(recyclerViewWidth, recyclerViewHeight,
151                     marginHorizontal, marginVertical, pageIndicatorWidth, pageIndicatorHeight,
152                     windowWidth, windowHeight);
153         }
154         return consumed;
155     }
156 
isHorizontal()157     private boolean isHorizontal() {
158         return AppGridConstants.isHorizontal(mPageOrientation);
159     }
160 
161     /**
162      * Rounds down to the nearest modulo multiple. For example, when {@code input} is 1024 and
163      * {@code modulo} is 5, we want to round down to 1020, since 1020 is the largest number
164      * such that {1020 % 5 = 0}.
165      */
roundDownToModuloMultiple(int input, int modulo)166     private int roundDownToModuloMultiple(int input, int modulo) {
167         return input / modulo * modulo;
168     }
169 
170     /**
171      * Data structure representing dimensions of the app grid.
172      *
173      * {@link GridDimensions#cellWidthPx} and {@link GridDimensions#cellHeightPx}:
174      * The width and height of each app item cell (view holder layout).
175      *
176      * {@link GridDimensions#gridWidthPx} and {@link GridDimensions#gridWidthPx}:
177      * The width and height of the app grid. These values should be equal to or less than the
178      * RecyclerView dimensions, with equal case being page margin size being 0 px.
179      */
180     public static class GridDimensions {
181         public int gridWidthPx;
182         public int gridHeightPx;
183         public int cellWidthPx;
184         public int cellHeightPx;
185         public int mNumOfRows;
186         public int mNumOfCols;
187 
GridDimensions(int gridWidth, int gridHeight, int cellWidth, int cellHeight, int numOfRows, int numOfCols)188         public GridDimensions(int gridWidth, int gridHeight, int cellWidth, int cellHeight,
189                 int numOfRows, int numOfCols) {
190             gridWidthPx = gridWidth;
191             gridHeightPx = gridHeight;
192             cellWidthPx = cellWidth;
193             cellHeightPx = cellHeight;
194             mNumOfRows = numOfRows;
195             mNumOfCols = numOfCols;
196         }
197 
198         @Override
toString()199         public String toString() {
200             return "%s {".formatted(super.toString())
201                     + " gridWidthPx: %d".formatted(gridWidthPx)
202                     + " gridHeightPx: %d".formatted(gridHeightPx)
203                     + " cellWidthPx: %d".formatted(cellWidthPx)
204                     + " cellHeightPx: %d".formatted(cellHeightPx)
205                     + " numOfRows: %d".formatted(mNumOfRows)
206                     + " numOfCols: %d".formatted(mNumOfCols)
207                     + "}";
208         }
209     }
210 
211     /**
212      * Data structure representing dimensions of the app grid.
213      *
214      * {@link PageDimensions#recyclerViewWidthPx} and {@link PageDimensions#recyclerViewHeightPx}
215      * The width and height of recycler view layout params.
216      *
217      * {@link PageDimensions#marginHorizontalPx} and {@link PageDimensions#marginVerticalPx}
218      * The margins on the left/right and top/bottom of the recycler view, respectively.
219      *
220      * {@link PageDimensions#pageIndicatorWidthPx} and {@link PageDimensions#pageIndicatorHeightPx}
221      * The width and height of the page indicator prior to resizing and adjusting offsets.
222      */
223     public static class PageDimensions {
224         public int recyclerViewWidthPx;
225         public int recyclerViewHeightPx;
226         public int marginHorizontalPx;
227         public int marginVerticalPx;
228         public int pageIndicatorWidthPx;
229         public int pageIndicatorHeightPx;
230         public int windowWidthPx;
231         public int windowHeightPx;
232 
PageDimensions(int recyclerViewWidth, int recyclerViewHeight, int marginHorizontal, int marginVertical, int pageIndicatorWidth, int pageIndicatorHeight, int windowWidth, int windowHeight)233         public PageDimensions(int recyclerViewWidth, int recyclerViewHeight, int marginHorizontal,
234                 int marginVertical, int pageIndicatorWidth, int pageIndicatorHeight,
235                 int windowWidth, int windowHeight) {
236             recyclerViewWidthPx = recyclerViewWidth;
237             recyclerViewHeightPx = recyclerViewHeight;
238             marginHorizontalPx = marginHorizontal;
239             marginVerticalPx = marginVertical;
240             pageIndicatorWidthPx = pageIndicatorWidth;
241             pageIndicatorHeightPx = pageIndicatorHeight;
242             windowWidthPx = windowWidth;
243             windowHeightPx = windowHeight;
244         }
245 
246         @Override
toString()247         public String toString() {
248             return "%s {".formatted(super.toString())
249                     + " recyclerViewWidthPx: %d".formatted(recyclerViewWidthPx)
250                     + " recyclerViewHeightPx: %d".formatted(recyclerViewHeightPx)
251                     + " marginHorizontalPx: %d".formatted(marginHorizontalPx)
252                     + " marginVerticalPx: %d".formatted(marginVerticalPx)
253                     + " pageIndicatorWidthPx: %d".formatted(pageIndicatorWidthPx)
254                     + " pageIndicatorHeightPx: %d".formatted(pageIndicatorHeightPx)
255                     + " windowWidthPx: %d".formatted(windowWidthPx)
256                     + " windowHeightPx: %d".formatted(windowHeightPx)
257                     + "}";
258         }
259     }
260 }
261