DynamicGrid.java revision 2651d134224d7166a9b55a9ffe8cecf04b96e072
1/* 2 * Copyright (C) 2008 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 17package com.android.launcher3; 18 19import android.content.res.Configuration; 20import android.content.res.Resources; 21import android.graphics.Paint; 22import android.graphics.PointF; 23import android.graphics.Paint.FontMetrics; 24import android.graphics.Rect; 25import android.util.DisplayMetrics; 26import android.util.TypedValue; 27import android.view.Gravity; 28import android.view.View; 29import android.view.ViewGroup.LayoutParams; 30import android.widget.FrameLayout; 31 32import java.util.ArrayList; 33import java.util.Collections; 34import java.util.Comparator; 35 36 37class DeviceProfileQuery { 38 float widthDps; 39 float heightDps; 40 float value; 41 PointF dimens; 42 43 DeviceProfileQuery(float w, float h, float v) { 44 widthDps = w; 45 heightDps = h; 46 value = v; 47 dimens = new PointF(w, h); 48 } 49} 50 51class DeviceProfile { 52 String name; 53 float minWidthDps; 54 float minHeightDps; 55 float numRows; 56 float numColumns; 57 float iconSize; 58 float iconTextSize; 59 float numHotseatIcons; 60 float hotseatIconSize; 61 62 boolean isLandscape; 63 boolean isTablet; 64 boolean isLargeTablet; 65 boolean transposeLayoutWithOrientation; 66 67 int edgeMarginPx; 68 69 int widthPx; 70 int heightPx; 71 int availableWidthPx; 72 int availableHeightPx; 73 int iconSizePx; 74 int iconTextSizePx; 75 int cellWidthPx; 76 int cellHeightPx; 77 int folderBackgroundOffset; 78 int folderIconSizePx; 79 int folderCellWidthPx; 80 int folderCellHeightPx; 81 int hotseatCellWidthPx; 82 int hotseatCellHeightPx; 83 int hotseatIconSizePx; 84 int hotseatBarHeightPx; 85 int hotseatAllAppsRank; 86 int allAppsNumRows; 87 int allAppsNumCols; 88 int searchBarSpaceWidthPx; 89 int searchBarSpaceMaxWidthPx; 90 int searchBarSpaceHeightPx; 91 int searchBarHeightPx; 92 int pageIndicatorHeightPx; 93 94 DeviceProfile(String n, float w, float h, float r, float c, 95 float is, float its, float hs, float his) { 96 // Ensure that we have an odd number of hotseat items (since we need to place all apps) 97 if (!AppsCustomizePagedView.DISABLE_ALL_APPS && hs % 2 == 0) { 98 throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); 99 } 100 101 name = n; 102 minWidthDps = w; 103 minHeightDps = h; 104 numRows = r; 105 numColumns = c; 106 iconSize = is; 107 iconTextSize = its; 108 numHotseatIcons = hs; 109 hotseatIconSize = his; 110 } 111 112 DeviceProfile(ArrayList<DeviceProfile> profiles, 113 float minWidth, float minHeight, 114 int wPx, int hPx, 115 int awPx, int ahPx, 116 Resources resources) { 117 DisplayMetrics dm = resources.getDisplayMetrics(); 118 ArrayList<DeviceProfileQuery> points = 119 new ArrayList<DeviceProfileQuery>(); 120 transposeLayoutWithOrientation = 121 resources.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); 122 minWidthDps = minWidth; 123 minHeightDps = minHeight; 124 125 edgeMarginPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); 126 pageIndicatorHeightPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height); 127 128 // Interpolate the rows 129 for (DeviceProfile p : profiles) { 130 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numRows)); 131 } 132 numRows = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); 133 // Interpolate the columns 134 points.clear(); 135 for (DeviceProfile p : profiles) { 136 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numColumns)); 137 } 138 numColumns = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); 139 // Interpolate the icon size 140 points.clear(); 141 for (DeviceProfile p : profiles) { 142 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconSize)); 143 } 144 iconSize = invDistWeightedInterpolate(minWidth, minHeight, points); 145 iconSizePx = DynamicGrid.pxFromDp(iconSize, dm); 146 147 // Interpolate the icon text size 148 points.clear(); 149 for (DeviceProfile p : profiles) { 150 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconTextSize)); 151 } 152 iconTextSize = invDistWeightedInterpolate(minWidth, minHeight, points); 153 iconTextSizePx = DynamicGrid.pxFromSp(iconTextSize, dm); 154 155 // Interpolate the hotseat size 156 points.clear(); 157 for (DeviceProfile p : profiles) { 158 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numHotseatIcons)); 159 } 160 numHotseatIcons = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); 161 // Interpolate the hotseat icon size 162 points.clear(); 163 for (DeviceProfile p : profiles) { 164 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.hotseatIconSize)); 165 } 166 // Hotseat 167 hotseatIconSize = invDistWeightedInterpolate(minWidth, minHeight, points); 168 hotseatIconSizePx = DynamicGrid.pxFromDp(hotseatIconSize, dm); 169 hotseatAllAppsRank = (int) (numColumns / 2); 170 171 // Calculate other vars based on Configuration 172 updateFromConfiguration(resources, wPx, hPx, awPx, ahPx); 173 174 // Search Bar 175 searchBarSpaceMaxWidthPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width); 176 searchBarHeightPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); 177 searchBarSpaceWidthPx = Math.min(searchBarSpaceMaxWidthPx, widthPx); 178 searchBarSpaceHeightPx = searchBarHeightPx + 2 * edgeMarginPx; 179 180 // Calculate the actual text height 181 Paint textPaint = new Paint(); 182 textPaint.setTextSize(iconTextSizePx); 183 FontMetrics fm = textPaint.getFontMetrics(); 184 cellWidthPx = iconSizePx; 185 cellHeightPx = iconSizePx + (int) Math.ceil(fm.bottom - fm.top); 186 187 // At this point, if the cells do not fit into the available height, then we need 188 // to shrink the icon size 189 /* 190 Rect padding = getWorkspacePadding(isLandscape ? 191 CellLayout.LANDSCAPE : CellLayout.PORTRAIT); 192 int h = (int) (numRows * cellHeightPx) + padding.top + padding.bottom; 193 if (h > availableHeightPx) { 194 float delta = h - availableHeightPx; 195 int deltaPx = (int) Math.ceil(delta / numRows); 196 iconSizePx -= deltaPx; 197 iconSize = DynamicGrid.dpiFromPx(iconSizePx, dm); 198 cellWidthPx = iconSizePx; 199 cellHeightPx = iconSizePx + (int) Math.ceil(fm.bottom - fm.top); 200 } 201 */ 202 203 // Hotseat 204 hotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx; 205 hotseatCellWidthPx = iconSizePx; 206 hotseatCellHeightPx = iconSizePx; 207 208 // Folder 209 folderCellWidthPx = cellWidthPx + 3 * edgeMarginPx; 210 folderCellHeightPx = cellHeightPx + (int) ((3f/2f) * edgeMarginPx); 211 folderBackgroundOffset = -edgeMarginPx; 212 folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset; 213 } 214 215 void updateFromConfiguration(Resources resources, int wPx, int hPx, 216 int awPx, int ahPx) { 217 isLandscape = (resources.getConfiguration().orientation == 218 Configuration.ORIENTATION_LANDSCAPE); 219 isTablet = resources.getBoolean(R.bool.is_tablet); 220 isLargeTablet = resources.getBoolean(R.bool.is_large_tablet); 221 widthPx = wPx; 222 heightPx = hPx; 223 availableWidthPx = awPx; 224 availableHeightPx = ahPx; 225 226 Rect padding = getWorkspacePadding(isLandscape ? 227 CellLayout.LANDSCAPE : CellLayout.PORTRAIT); 228 int pageIndicatorOffset = 229 resources.getDimensionPixelSize(R.dimen.apps_customize_page_indicator_offset); 230 if (isLandscape) { 231 allAppsNumRows = (availableHeightPx - pageIndicatorOffset - 4 * edgeMarginPx) / 232 (iconSizePx + iconTextSizePx + 2 * edgeMarginPx); 233 } else { 234 allAppsNumRows = (int) numRows + 1; 235 } 236 allAppsNumCols = (availableWidthPx - padding.left - padding.right - 2 * edgeMarginPx) / 237 (iconSizePx + 2 * edgeMarginPx); 238 } 239 240 private float dist(PointF p0, PointF p1) { 241 return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) + 242 (p1.y-p0.y)*(p1.y-p0.y)); 243 } 244 245 private float weight(PointF a, PointF b, 246 float pow) { 247 float d = dist(a, b); 248 if (d == 0f) { 249 return Float.POSITIVE_INFINITY; 250 } 251 return (float) (1f / Math.pow(d, pow)); 252 } 253 254 private float invDistWeightedInterpolate(float width, float height, 255 ArrayList<DeviceProfileQuery> points) { 256 float sum = 0; 257 float weights = 0; 258 float pow = 5; 259 float kNearestNeighbors = 3; 260 final PointF xy = new PointF(width, height); 261 262 ArrayList<DeviceProfileQuery> pointsByNearness = points; 263 Collections.sort(pointsByNearness, new Comparator<DeviceProfileQuery>() { 264 public int compare(DeviceProfileQuery a, DeviceProfileQuery b) { 265 return (int) (dist(xy, a.dimens) - dist(xy, b.dimens)); 266 } 267 }); 268 269 for (int i = 0; i < pointsByNearness.size(); ++i) { 270 DeviceProfileQuery p = pointsByNearness.get(i); 271 if (i < kNearestNeighbors) { 272 float w = weight(xy, p.dimens, pow); 273 if (w == Float.POSITIVE_INFINITY) { 274 return p.value; 275 } 276 weights += w; 277 } 278 } 279 280 for (int i = 0; i < pointsByNearness.size(); ++i) { 281 DeviceProfileQuery p = pointsByNearness.get(i); 282 if (i < kNearestNeighbors) { 283 float w = weight(xy, p.dimens, pow); 284 sum += w * p.value / weights; 285 } 286 } 287 288 return sum; 289 } 290 291 Rect getWorkspacePadding(int orientation) { 292 Rect padding = new Rect(); 293 if (orientation == CellLayout.LANDSCAPE && 294 transposeLayoutWithOrientation) { 295 // Pad the left and right of the workspace with search/hotseat bar sizes 296 padding.set(searchBarSpaceHeightPx, edgeMarginPx, 297 hotseatBarHeightPx, edgeMarginPx); 298 } else { 299 if (isTablet()) { 300 // Pad the left and right of the workspace to ensure consistent spacing 301 // between all icons 302 int width = (orientation == CellLayout.LANDSCAPE) 303 ? Math.max(widthPx, heightPx) 304 : Math.min(widthPx, heightPx); 305 // XXX: If the icon size changes across orientations, we will have to take 306 // that into account here too. 307 int gap = (int) ((width - 2 * edgeMarginPx - 308 (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); 309 padding.set(edgeMarginPx + gap, 310 searchBarSpaceHeightPx, 311 edgeMarginPx + gap, 312 hotseatBarHeightPx + pageIndicatorHeightPx); 313 } else { 314 // Pad the top and bottom of the workspace with search/hotseat bar sizes 315 padding.set(edgeMarginPx, 316 searchBarSpaceHeightPx, 317 edgeMarginPx, 318 hotseatBarHeightPx + pageIndicatorHeightPx); 319 } 320 } 321 return padding; 322 } 323 324 // The rect returned will be extended to below the system ui that covers the workspace 325 Rect getHotseatRect() { 326 if (isVerticalBarLayout()) { 327 return new Rect(availableWidthPx - hotseatBarHeightPx, 0, 328 Integer.MAX_VALUE, availableHeightPx); 329 } else { 330 return new Rect(0, availableHeightPx - hotseatBarHeightPx, 331 availableWidthPx, Integer.MAX_VALUE); 332 } 333 } 334 335 int calculateCellWidth(int width, int countX) { 336 return width / countX; 337 } 338 int calculateCellHeight(int height, int countY) { 339 return height / countY; 340 } 341 342 boolean isPhone() { 343 return !isTablet && !isLargeTablet; 344 } 345 boolean isTablet() { 346 return isTablet; 347 } 348 boolean isLargeTablet() { 349 return isLargeTablet; 350 } 351 352 boolean isVerticalBarLayout() { 353 return isLandscape && transposeLayoutWithOrientation; 354 } 355 356 public void layout(Launcher launcher) { 357 FrameLayout.LayoutParams lp; 358 Resources res = launcher.getResources(); 359 boolean hasVerticalBarLayout = isVerticalBarLayout(); 360 361 // Layout the search bar space 362 View searchBar = launcher.getSearchBar(); 363 lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); 364 if (hasVerticalBarLayout) { 365 // Vertical search bar 366 lp.gravity = Gravity.TOP | Gravity.LEFT; 367 lp.width = searchBarSpaceHeightPx; 368 lp.height = LayoutParams.MATCH_PARENT; 369 searchBar.setPadding( 370 0, 2 * edgeMarginPx, 0, 371 2 * edgeMarginPx); 372 } else { 373 // Horizontal search bar 374 lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; 375 lp.width = searchBarSpaceWidthPx; 376 lp.height = searchBarSpaceHeightPx; 377 searchBar.setPadding( 378 2 * edgeMarginPx, 379 2 * edgeMarginPx, 380 2 * edgeMarginPx, 0); 381 } 382 searchBar.setLayoutParams(lp); 383 384 // Layout the search bar 385 View qsbBar = launcher.getQsbBar(); 386 LayoutParams vglp = qsbBar.getLayoutParams(); 387 vglp.width = LayoutParams.MATCH_PARENT; 388 vglp.height = LayoutParams.MATCH_PARENT; 389 qsbBar.setLayoutParams(vglp); 390 391 // Layout the voice proxy 392 View voiceButtonProxy = launcher.findViewById(R.id.voice_button_proxy); 393 if (voiceButtonProxy != null) { 394 if (hasVerticalBarLayout) { 395 // TODO: MOVE THIS INTO SEARCH BAR MEASURE 396 } else { 397 lp = (FrameLayout.LayoutParams) voiceButtonProxy.getLayoutParams(); 398 lp.gravity = Gravity.TOP | Gravity.END; 399 lp.width = (widthPx - searchBarSpaceWidthPx) / 2 + 400 2 * iconSizePx; 401 lp.height = searchBarSpaceHeightPx; 402 } 403 } 404 405 // Layout the workspace 406 View workspace = launcher.findViewById(R.id.workspace); 407 lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); 408 lp.gravity = Gravity.CENTER; 409 Rect padding = getWorkspacePadding(isLandscape 410 ? CellLayout.LANDSCAPE 411 : CellLayout.PORTRAIT); 412 workspace.setPadding(padding.left, padding.top, 413 padding.right, padding.bottom); 414 workspace.setLayoutParams(lp); 415 416 // Layout the hotseat 417 View hotseat = launcher.findViewById(R.id.hotseat); 418 lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams(); 419 if (hasVerticalBarLayout) { 420 // Vertical hotseat 421 lp.gravity = Gravity.RIGHT; 422 lp.width = hotseatBarHeightPx; 423 lp.height = LayoutParams.MATCH_PARENT; 424 hotseat.setPadding(0, 2 * edgeMarginPx, 425 2 * edgeMarginPx, 2 * edgeMarginPx); 426 } else if (isTablet()) { 427 // Pad the hotseat with the grid gap calculated above 428 int gridGap = (int) ((widthPx - 2 * edgeMarginPx - 429 (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); 430 int gridWidth = (int) ((numColumns * cellWidthPx) + 431 ((numColumns - 1) * gridGap)); 432 int hotseatGap = (int) Math.max(0, 433 (gridWidth - (numHotseatIcons * hotseatCellWidthPx)) 434 / (numHotseatIcons - 1)); 435 lp.gravity = Gravity.BOTTOM; 436 lp.width = LayoutParams.MATCH_PARENT; 437 lp.height = hotseatBarHeightPx; 438 hotseat.setPadding(2 * edgeMarginPx + gridGap + hotseatGap, 0, 439 2 * edgeMarginPx + gridGap + hotseatGap, 440 2 * edgeMarginPx); 441 } else { 442 // For phones, layout the hotseat without any bottom margin 443 // to ensure that we have space for the folders 444 lp.gravity = Gravity.BOTTOM; 445 lp.width = LayoutParams.MATCH_PARENT; 446 lp.height = hotseatBarHeightPx; 447 hotseat.findViewById(R.id.layout).setPadding(2 * edgeMarginPx, 0, 448 2 * edgeMarginPx, 0); 449 } 450 hotseat.setLayoutParams(lp); 451 452 // Layout the page indicators 453 View pageIndicator = launcher.findViewById(R.id.page_indicator); 454 if (pageIndicator != null) { 455 if (hasVerticalBarLayout) { 456 // Hide the page indicators when we have vertical search/hotseat 457 pageIndicator.setVisibility(View.GONE); 458 } else { 459 // Put the page indicators above the hotseat 460 lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams(); 461 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 462 lp.width = LayoutParams.WRAP_CONTENT; 463 lp.height = pageIndicatorHeightPx; 464 lp.bottomMargin = hotseatBarHeightPx; 465 pageIndicator.setLayoutParams(lp); 466 } 467 } 468 } 469} 470 471public class DynamicGrid { 472 @SuppressWarnings("unused") 473 private static final String TAG = "DynamicGrid"; 474 475 private DeviceProfile mProfile; 476 private float mMinWidth; 477 private float mMinHeight; 478 479 public static float dpiFromPx(int size, DisplayMetrics metrics){ 480 float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; 481 return (size / densityRatio); 482 } 483 public static int pxFromDp(float size, DisplayMetrics metrics) { 484 return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 485 size, metrics)); 486 } 487 public static int pxFromSp(float size, DisplayMetrics metrics) { 488 return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 489 size, metrics)); 490 } 491 492 public DynamicGrid(Resources resources, int minWidthPx, int minHeightPx, 493 int widthPx, int heightPx, 494 int awPx, int ahPx) { 495 DisplayMetrics dm = resources.getDisplayMetrics(); 496 ArrayList<DeviceProfile> deviceProfiles = 497 new ArrayList<DeviceProfile>(); 498 boolean hasAA = !AppsCustomizePagedView.DISABLE_ALL_APPS; 499 // Our phone profiles include the bar sizes in each orientation 500 deviceProfiles.add(new DeviceProfile("Super Short Stubby", 501 255, 300, 2, 3, 48, 13, (hasAA ? 5 : 4), 48)); 502 deviceProfiles.add(new DeviceProfile("Shorter Stubby", 503 255, 400, 3, 3, 48, 13, (hasAA ? 5 : 4), 48)); 504 deviceProfiles.add(new DeviceProfile("Short Stubby", 505 275, 420, 3, 4, 48, 13, (hasAA ? 5 : 4), 48)); 506 deviceProfiles.add(new DeviceProfile("Stubby", 507 255, 450, 3, 4, 48, 13, (hasAA ? 5 : 4), 48)); 508 deviceProfiles.add(new DeviceProfile("Nexus S", 509 296, 491.33f, 4, 4, 48, 13, (hasAA ? 5 : 4), 48)); 510 deviceProfiles.add(new DeviceProfile("Nexus 4", 511 359, 518, 4, 4, 60, 13, (hasAA ? 5 : 4), 56)); 512 // The tablet profile is odd in that the landscape orientation 513 // also includes the nav bar on the side 514 deviceProfiles.add(new DeviceProfile("Nexus 7", 515 575, 904, 6, 6, 72, 14.4f, 7, 60)); 516 // Larger tablet profiles always have system bars on the top & bottom 517 deviceProfiles.add(new DeviceProfile("Nexus 10", 518 727, 1207, 5, 8, 80, 14.4f, 9, 64)); 519 /* 520 deviceProfiles.add(new DeviceProfile("Nexus 7", 521 600, 960, 5, 5, 72, 14.4f, 5, 60)); 522 deviceProfiles.add(new DeviceProfile("Nexus 10", 523 800, 1280, 5, 5, 80, 14.4f, (hasAA ? 7 : 6), 64)); 524 */ 525 deviceProfiles.add(new DeviceProfile("20-inch Tablet", 526 1527, 2527, 7, 7, 100, 20, 7, 72)); 527 mMinWidth = dpiFromPx(minWidthPx, dm); 528 mMinHeight = dpiFromPx(minHeightPx, dm); 529 mProfile = new DeviceProfile(deviceProfiles, 530 mMinWidth, mMinHeight, 531 widthPx, heightPx, 532 awPx, ahPx, 533 resources); 534 } 535 536 DeviceProfile getDeviceProfile() { 537 return mProfile; 538 } 539 540 public String toString() { 541 return "-------- DYNAMIC GRID ------- \n" + 542 "Wd: " + mProfile.minWidthDps + ", Hd: " + mProfile.minHeightDps + 543 ", W: " + mProfile.widthPx + ", H: " + mProfile.heightPx + 544 " [r: " + mProfile.numRows + ", c: " + mProfile.numColumns + 545 ", is: " + mProfile.iconSizePx + ", its: " + mProfile.iconTextSize + 546 ", cw: " + mProfile.cellWidthPx + ", ch: " + mProfile.cellHeightPx + 547 ", hc: " + mProfile.numHotseatIcons + ", his: " + mProfile.hotseatIconSizePx + "]"; 548 } 549} 550