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