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     private final int mNumOfCols;
31     private final int mNumOfRows;
32     @PageOrientation
33     private final int mPageOrientation;
34     private final boolean mUseDefinedDimensions;
35     private final int mDefinedWidth;
36     private final int mDefinedHeight;
37     private final int mDefinedMarginHorizontal;
38     private final int mDefinedMarginVertical;
39     private final int mDefinedPageIndicatorSize;
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         mNumOfCols = windowBackground.getResources().getInteger(
48                 R.integer.car_app_selector_column_number);
49         mNumOfRows = windowBackground.getResources().getInteger(
50                 R.integer.car_app_selector_row_number);
51         mPageOrientation = windowBackground.getResources().getBoolean(R.bool.use_vertical_app_grid)
52                 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL;
53         mUseDefinedDimensions = windowBackground.getResources().getBoolean(
54                 R.bool.use_defined_app_grid_dimensions);
55         mDefinedWidth = windowBackground.getResources().getDimensionPixelSize(
56                 R.dimen.app_grid_width);
57         mDefinedHeight = windowBackground.getResources().getDimensionPixelSize(
58                 R.dimen.app_grid_height);
59         mDefinedMarginHorizontal = windowBackground.getResources().getDimensionPixelSize(
60                 R.dimen.app_grid_margin_horizontal);
61         mDefinedMarginVertical = windowBackground.getResources().getDimensionPixelSize(
62                 R.dimen.app_grid_margin_vertical);
63         mDefinedPageIndicatorSize = windowBackground.getResources().getDimensionPixelSize(
64                 R.dimen.page_indicator_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             gridWidth = roundDownToModuloMultiple(gridWidth, mNumOfCols);
113             gridHeight = roundDownToModuloMultiple(gridHeight, mNumOfRows);
114             int cellWidth = gridWidth / mNumOfCols;
115             int cellHeight = gridHeight / mNumOfRows;
116             mGridDimensions = new GridDimensions(gridWidth, gridHeight, cellWidth, cellHeight);
117 
118             // Step 3: Since the grid dimens are rounded, we need to recalculate the margins.
119             int marginHorizontal = (windowWidth - gridWidth) / 2;
120             int marginVertical = (windowHeight - gridHeight) / 2;
121 
122             // Step 4: Calculate RecyclerView and PageIndicator dimens for layout params.
123             int recyclerViewWidth, recyclerViewHeight;
124             int pageIndicatorWidth, pageIndicatorHeight;
125             if (isHorizontal()) {
126                 // horizontal app grid should have HORIZONTAL page indicator bar and the
127                 // recyclerview width should span the entire window to not clip off the page margin
128                 recyclerViewWidth = windowWidth;
129                 recyclerViewHeight = gridHeight;
130                 pageIndicatorWidth = gridWidth;
131                 pageIndicatorHeight = mDefinedPageIndicatorSize;
132             } else {
133                 // vertical app grid should have VERTICAL page indicator bar and the
134                 // recyclerview height should span the entire window to not clip off the page margin
135                 recyclerViewWidth = gridWidth;
136                 recyclerViewHeight = windowHeight;
137                 pageIndicatorWidth = mDefinedPageIndicatorSize;
138                 pageIndicatorHeight = gridHeight;
139             }
140             mPageDimensions = new PageDimensions(recyclerViewWidth, recyclerViewHeight,
141                     marginHorizontal, marginVertical, pageIndicatorWidth, pageIndicatorHeight,
142                     windowWidth, windowHeight);
143         }
144         return consumed;
145     }
146 
isHorizontal()147     private boolean isHorizontal() {
148         return AppGridConstants.isHorizontal(mPageOrientation);
149     }
150 
151     /**
152      * Rounds down to the nearest modulo multiple. For example, when {@code input} is 1024 and
153      * {@code modulo} is 5, we want to round down to 1020, since 1020 is the largest number
154      * such that {1020 % 5 = 0}.
155      */
roundDownToModuloMultiple(int input, int modulo)156     private int roundDownToModuloMultiple(int input, int modulo) {
157         return input / modulo * modulo;
158     }
159 
160     /**
161      * Data structure representing dimensions of the app grid.
162      *
163      * {@link GridDimensions#cellWidthPx} and {@link GridDimensions#cellHeightPx}:
164      * The width and height of each app item cell (view holder layout).
165      *
166      * {@link GridDimensions#gridWidthPx} and {@link GridDimensions#gridWidthPx}:
167      * The width and height of the app grid. These values should be equal to or less than the
168      * RecyclerView dimensions, with equal case being page margin size being 0 px.
169      */
170     public static class GridDimensions {
171         public int gridWidthPx;
172         public int gridHeightPx;
173         public int cellWidthPx;
174         public int cellHeightPx;
175 
GridDimensions(int gridWidth, int gridHeight, int cellWidth, int cellHeight)176         public GridDimensions(int gridWidth, int gridHeight, int cellWidth, int cellHeight) {
177             gridWidthPx = gridWidth;
178             gridHeightPx = gridHeight;
179             cellWidthPx = cellWidth;
180             cellHeightPx = cellHeight;
181         }
182 
183         @Override
toString()184         public String toString() {
185             return "%s {".formatted(super.toString())
186                     + " gridWidthPx: %d".formatted(gridWidthPx)
187                     + " gridHeightPx: %d".formatted(gridHeightPx)
188                     + " cellWidthPx: %d".formatted(cellWidthPx)
189                     + " cellHeightPx: %d".formatted(cellHeightPx)
190                     + "}";
191         }
192     }
193 
194     /**
195      * Data structure representing dimensions of the app grid.
196      *
197      * {@link PageDimensions#recyclerViewWidthPx} and {@link PageDimensions#recyclerViewHeightPx}
198      * The width and height of recycler view layout params.
199      *
200      * {@link PageDimensions#marginHorizontalPx} and {@link PageDimensions#marginVerticalPx}
201      * The margins on the left/right and top/bottom of the recycler view, respectively.
202      *
203      * {@link PageDimensions#pageIndicatorWidthPx} and {@link PageDimensions#pageIndicatorHeightPx}
204      * The width and height of the page indicator prior to resizing and adjusting offsets.
205      */
206     public static class PageDimensions {
207         public int recyclerViewWidthPx;
208         public int recyclerViewHeightPx;
209         public int marginHorizontalPx;
210         public int marginVerticalPx;
211         public int pageIndicatorWidthPx;
212         public int pageIndicatorHeightPx;
213         public int windowWidthPx;
214         public int windowHeightPx;
215 
PageDimensions(int recyclerViewWidth, int recyclerViewHeight, int marginHorizontal, int marginVertical, int pageIndicatorWidth, int pageIndicatorHeight, int windowWidth, int windowHeight)216         public PageDimensions(int recyclerViewWidth, int recyclerViewHeight, int marginHorizontal,
217                 int marginVertical, int pageIndicatorWidth, int pageIndicatorHeight,
218                 int windowWidth, int windowHeight) {
219             recyclerViewWidthPx = recyclerViewWidth;
220             recyclerViewHeightPx = recyclerViewHeight;
221             marginHorizontalPx = marginHorizontal;
222             marginVerticalPx = marginVertical;
223             pageIndicatorWidthPx = pageIndicatorWidth;
224             pageIndicatorHeightPx = pageIndicatorHeight;
225             windowWidthPx = windowWidth;
226             windowHeightPx = windowHeight;
227         }
228 
229         @Override
toString()230         public String toString() {
231             return "%s {".formatted(super.toString())
232                     + " recyclerViewWidthPx: %d".formatted(recyclerViewWidthPx)
233                     + " recyclerViewHeightPx: %d".formatted(recyclerViewHeightPx)
234                     + " marginHorizontalPx: %d".formatted(marginHorizontalPx)
235                     + " marginVerticalPx: %d".formatted(marginVerticalPx)
236                     + " pageIndicatorWidthPx: %d".formatted(pageIndicatorWidthPx)
237                     + " pageIndicatorHeightPx: %d".formatted(pageIndicatorHeightPx)
238                     + " windowWidthPx: %d".formatted(windowWidthPx)
239                     + " windowHeightPx: %d".formatted(windowHeightPx)
240                     + "}";
241         }
242     }
243 }
244