DeviceProfile.java revision 47328fd53f43fd0c2ed14ad925dd04483f9229a0
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.appwidget.AppWidgetHostView; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.res.Resources; 23import android.graphics.Paint; 24import android.graphics.Paint.FontMetrics; 25import android.graphics.Point; 26import android.graphics.Rect; 27import android.util.DisplayMetrics; 28import android.view.Gravity; 29import android.view.View; 30import android.view.ViewGroup; 31import android.view.ViewGroup.LayoutParams; 32import android.view.ViewGroup.MarginLayoutParams; 33import android.widget.FrameLayout; 34 35import com.android.launcher3.config.FeatureFlags; 36 37public class DeviceProfile { 38 39 public final InvariantDeviceProfile inv; 40 41 // Device properties 42 public final boolean isTablet; 43 public final boolean isLargeTablet; 44 public final boolean isPhone; 45 public final boolean transposeLayoutWithOrientation; 46 47 // Device properties in current orientation 48 public final boolean isLandscape; 49 public final int widthPx; 50 public final int heightPx; 51 public final int availableWidthPx; 52 public final int availableHeightPx; 53 /** 54 * The maximum amount of left/right workspace padding as a percentage of the screen width. 55 * To be clear, this means that up to 7% of the screen width can be used as left padding, and 56 * 7% of the screen width can be used as right padding. 57 */ 58 private static final float MAX_HORIZONTAL_PADDING_PERCENT = 0.14f; 59 60 // Overview mode 61 private final int overviewModeMinIconZoneHeightPx; 62 private final int overviewModeMaxIconZoneHeightPx; 63 private final int overviewModeBarItemWidthPx; 64 private final int overviewModeBarSpacerWidthPx; 65 private final float overviewModeIconZoneRatio; 66 67 // Workspace 68 private int desiredWorkspaceLeftRightMarginPx; 69 public final int edgeMarginPx; 70 public final Rect defaultWidgetPadding; 71 private final int pageIndicatorHeightPx; 72 private final int defaultPageSpacingPx; 73 private final int topWorkspacePadding; 74 private float dragViewScale; 75 public float workspaceSpringLoadShrinkFactor; 76 public final int workspaceSpringLoadedBottomSpace; 77 78 // Workspace icons 79 public int iconSizePx; 80 public int iconTextSizePx; 81 public int iconDrawablePaddingPx; 82 public int iconDrawablePaddingOriginalPx; 83 84 public int cellWidthPx; 85 public int cellHeightPx; 86 87 // Folder 88 public int folderBackgroundOffset; 89 public int folderIconSizePx; 90 public int folderIconPreviewPadding; 91 public int folderCellWidthPx; 92 public int folderCellHeightPx; 93 94 // Hotseat 95 public int hotseatCellWidthPx; 96 public int hotseatCellHeightPx; 97 public int hotseatIconSizePx; 98 private int hotseatBarHeightPx; 99 100 // All apps 101 public int allAppsNumCols; 102 public int allAppsNumPredictiveCols; 103 public int allAppsButtonVisualSize; 104 public final int allAppsIconSizePx; 105 public final float allAppsIconTextSizeSp; 106 107 // Drop Target 108 public int dropTargetBarSizePx; 109 110 public DeviceProfile(Context context, InvariantDeviceProfile inv, 111 Point minSize, Point maxSize, 112 int width, int height, boolean isLandscape) { 113 114 this.inv = inv; 115 this.isLandscape = isLandscape; 116 117 Resources res = context.getResources(); 118 DisplayMetrics dm = res.getDisplayMetrics(); 119 120 // Constants from resources 121 isTablet = res.getBoolean(R.bool.is_tablet); 122 isLargeTablet = res.getBoolean(R.bool.is_large_tablet); 123 isPhone = !isTablet && !isLargeTablet; 124 125 // Some more constants 126 transposeLayoutWithOrientation = 127 res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); 128 129 ComponentName cn = new ComponentName(context.getPackageName(), 130 this.getClass().getName()); 131 defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null); 132 edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); 133 desiredWorkspaceLeftRightMarginPx = 2 * edgeMarginPx; 134 pageIndicatorHeightPx = 135 res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height); 136 defaultPageSpacingPx = 137 res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing); 138 topWorkspacePadding = 139 res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_top_padding); 140 overviewModeMinIconZoneHeightPx = 141 res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height); 142 overviewModeMaxIconZoneHeightPx = 143 res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_max_icon_zone_height); 144 overviewModeBarItemWidthPx = 145 res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_item_width); 146 overviewModeBarSpacerWidthPx = 147 res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_spacer_width); 148 overviewModeIconZoneRatio = 149 res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f; 150 iconDrawablePaddingOriginalPx = 151 res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding); 152 dropTargetBarSizePx = res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size); 153 workspaceSpringLoadedBottomSpace = 154 res.getDimensionPixelSize(R.dimen.dynamic_grid_min_spring_loaded_space); 155 156 // AllApps uses the original non-scaled icon text size 157 allAppsIconTextSizeSp = inv.iconTextSize; 158 159 // AllApps uses the original non-scaled icon size 160 allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm); 161 162 // Determine sizes. 163 widthPx = width; 164 heightPx = height; 165 if (isLandscape) { 166 availableWidthPx = maxSize.x; 167 availableHeightPx = minSize.y; 168 } else { 169 availableWidthPx = minSize.x; 170 availableHeightPx = maxSize.y; 171 } 172 173 // Calculate the remaining vars 174 updateAvailableDimensions(dm, res); 175 computeAllAppsButtonSize(context); 176 } 177 178 /** 179 * Determine the exact visual footprint of the all apps button, taking into account scaling 180 * and internal padding of the drawable. 181 */ 182 private void computeAllAppsButtonSize(Context context) { 183 Resources res = context.getResources(); 184 float padding = res.getInteger(R.integer.config_allAppsButtonPaddingPercent) / 100f; 185 allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding)) - context.getResources() 186 .getDimensionPixelSize(R.dimen.all_apps_button_scale_down); 187 } 188 189 private void updateAvailableDimensions(DisplayMetrics dm, Resources res) { 190 // Check to see if the icons fit in the new available height. If not, then we need to 191 // shrink the icon size. 192 float scale = 1f; 193 int drawablePadding = iconDrawablePaddingOriginalPx; 194 updateIconSize(1f, drawablePadding, res, dm); 195 float usedHeight = (cellHeightPx * inv.numRows); 196 197 // We only care about the top and bottom workspace padding, which is not affected by RTL. 198 Rect workspacePadding = getWorkspacePadding(); 199 int maxHeight = (availableHeightPx - workspacePadding.top - workspacePadding.bottom); 200 if (usedHeight > maxHeight) { 201 scale = maxHeight / usedHeight; 202 drawablePadding = 0; 203 } 204 updateIconSize(scale, drawablePadding, res, dm); 205 } 206 207 private void updateIconSize(float scale, int drawablePadding, Resources res, 208 DisplayMetrics dm) { 209 iconSizePx = (int) (Utilities.pxFromDp(inv.iconSize, dm) * scale); 210 iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale); 211 iconDrawablePaddingPx = drawablePadding; 212 hotseatIconSizePx = (int) (Utilities.pxFromDp(inv.hotseatIconSize, dm) * scale); 213 214 // Calculate the actual text height 215 Paint textPaint = new Paint(); 216 textPaint.setTextSize(iconTextSizePx); 217 FontMetrics fm = textPaint.getFontMetrics(); 218 cellWidthPx = iconSizePx; 219 cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top); 220 final float scaleDps = !FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND ? 0f 221 : res.getDimensionPixelSize(R.dimen.dragViewScale); 222 dragViewScale = (iconSizePx + scaleDps) / iconSizePx; 223 224 // Hotseat 225 hotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx; 226 hotseatCellWidthPx = iconSizePx; 227 hotseatCellHeightPx = iconSizePx; 228 229 if (!isVerticalBarLayout()) { 230 int expectedWorkspaceHeight = availableHeightPx - hotseatBarHeightPx 231 - pageIndicatorHeightPx - topWorkspacePadding; 232 float minRequiredHeight = dropTargetBarSizePx + workspaceSpringLoadedBottomSpace; 233 workspaceSpringLoadShrinkFactor = Math.min( 234 res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f, 235 1 - (minRequiredHeight / expectedWorkspaceHeight)); 236 } else { 237 workspaceSpringLoadShrinkFactor = 238 res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; 239 } 240 241 // Folder 242 int folderCellPadding = isTablet || isLandscape ? 6 * edgeMarginPx : 3 * edgeMarginPx; 243 // Don't let the folder get too close to the edges of the screen. 244 folderCellWidthPx = Math.min(cellWidthPx + folderCellPadding, 245 (availableWidthPx - 4 * edgeMarginPx) / inv.numFolderColumns); 246 folderCellHeightPx = cellHeightPx + edgeMarginPx; 247 folderBackgroundOffset = -edgeMarginPx; 248 folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset; 249 folderIconPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding); 250 } 251 252 /** 253 * @param recyclerViewWidth the available width of the AllAppsRecyclerView 254 */ 255 public void updateAppsViewNumCols(Resources res, int recyclerViewWidth) { 256 int appsViewLeftMarginPx = 257 res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); 258 int allAppsCellWidthGap = 259 res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap); 260 int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx; 261 int numAppsCols = (availableAppsWidthPx + allAppsCellWidthGap - appsViewLeftMarginPx) / 262 (allAppsIconSizePx + allAppsCellWidthGap); 263 int numPredictiveAppCols = Math.max(inv.minAllAppsPredictionColumns, numAppsCols); 264 allAppsNumCols = numAppsCols; 265 allAppsNumPredictiveCols = numPredictiveAppCols; 266 } 267 268 /** Returns the width and height of the search bar, ignoring any padding. */ 269 public Point getSearchBarDimensForWidgetOpts() { 270 if (isVerticalBarLayout()) { 271 return new Point(dropTargetBarSizePx, availableHeightPx - 2 * edgeMarginPx); 272 } else { 273 int gap; 274 if (isTablet) { 275 // Pad the left and right of the workspace to ensure consistent spacing 276 // between all icons 277 int width = getCurrentWidth(); 278 // XXX: If the icon size changes across orientations, we will have to take 279 // that into account here too. 280 gap = ((width - 2 * edgeMarginPx 281 - (inv.numColumns * cellWidthPx)) / (2 * (inv.numColumns + 1))) 282 + edgeMarginPx; 283 } else { 284 gap = desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.right; 285 } 286 return new Point(availableWidthPx - 2 * gap, dropTargetBarSizePx); 287 } 288 } 289 290 public Point getCellSize() { 291 Point result = new Point(); 292 // Since we are only concerned with the overall padding, layout direction does 293 // not matter. 294 Rect padding = getWorkspacePadding(); 295 result.x = calculateCellWidth(availableWidthPx - padding.left - padding.right, 296 inv.numColumns); 297 result.y = calculateCellHeight(availableHeightPx - padding.top - padding.bottom, 298 inv.numRows); 299 return result; 300 } 301 302 /** Returns the workspace padding in the specified orientation */ 303 public Rect getWorkspacePadding() { 304 Rect padding = new Rect(); 305 if (isVerticalBarLayout()) { 306 // in case of isVerticalBarLayout, the hotseat is always on the right and the drop 307 // target bar is on the left, independent of the layout direction. 308 padding.set(dropTargetBarSizePx, edgeMarginPx, hotseatBarHeightPx, edgeMarginPx); 309 } else { 310 int paddingBottom = hotseatBarHeightPx + pageIndicatorHeightPx; 311 if (isTablet) { 312 // Pad the left and right of the workspace to ensure consistent spacing 313 // between all icons 314 float gapScale = 1f + (dragViewScale - 1f) / 2f; 315 int width = getCurrentWidth(); 316 int height = getCurrentHeight(); 317 // The amount of screen space available for left/right padding. 318 int availablePaddingX = Math.max(0, width - (int) ((inv.numColumns * cellWidthPx) + 319 ((inv.numColumns - 1) * gapScale * cellWidthPx))); 320 availablePaddingX = (int) Math.min(availablePaddingX, 321 width * MAX_HORIZONTAL_PADDING_PERCENT); 322 int availablePaddingY = Math.max(0, height - topWorkspacePadding - paddingBottom 323 - (int) (2 * inv.numRows * cellHeightPx)); 324 padding.set(availablePaddingX / 2, topWorkspacePadding + availablePaddingY / 2, 325 availablePaddingX / 2, paddingBottom + availablePaddingY / 2); 326 } else { 327 // Pad the top and bottom of the workspace with search/hotseat bar sizes 328 padding.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left, 329 topWorkspacePadding, 330 desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.right, 331 paddingBottom); 332 } 333 } 334 return padding; 335 } 336 337 private int getWorkspacePageSpacing() { 338 if (isVerticalBarLayout() || isLargeTablet) { 339 // In landscape mode the page spacing is set to the default. 340 return defaultPageSpacingPx; 341 } else { 342 // In portrait, we want the pages spaced such that there is no 343 // overhang of the previous / next page into the current page viewport. 344 // We assume symmetrical padding in portrait mode. 345 return Math.max(defaultPageSpacingPx, 2 * getWorkspacePadding().left); 346 } 347 } 348 349 int getOverviewModeButtonBarHeight() { 350 int zoneHeight = (int) (overviewModeIconZoneRatio * availableHeightPx); 351 zoneHeight = Math.min(overviewModeMaxIconZoneHeightPx, 352 Math.max(overviewModeMinIconZoneHeightPx, zoneHeight)); 353 return zoneHeight; 354 } 355 356 // The rect returned will be extended to below the system ui that covers the workspace 357 public boolean isInHotseatRect(int x, int y) { 358 if (isVerticalBarLayout()) { 359 return (x >= (availableWidthPx - hotseatBarHeightPx)) 360 && (y >= 0) && (y <= availableHeightPx); 361 } else { 362 return (x >= 0) && (x <= availableWidthPx) 363 && (y >= (availableHeightPx - hotseatBarHeightPx)); 364 } 365 } 366 367 public static int calculateCellWidth(int width, int countX) { 368 return width / countX; 369 } 370 public static int calculateCellHeight(int height, int countY) { 371 return height / countY; 372 } 373 374 /** 375 * When {@code true}, the device is in landscape mode and the hotseat is on the right column. 376 * When {@code false}, either device is in portrait mode or the device is in landscape mode and 377 * the hotseat is on the bottom row. 378 */ 379 public boolean isVerticalBarLayout() { 380 return isLandscape && transposeLayoutWithOrientation; 381 } 382 383 boolean shouldFadeAdjacentWorkspaceScreens() { 384 return isVerticalBarLayout() || isLargeTablet; 385 } 386 387 private int getVisibleChildCount(ViewGroup parent) { 388 int visibleChildren = 0; 389 for (int i = 0; i < parent.getChildCount(); i++) { 390 if (parent.getChildAt(i).getVisibility() != View.GONE) { 391 visibleChildren++; 392 } 393 } 394 return visibleChildren; 395 } 396 397 public void layout(Launcher launcher) { 398 FrameLayout.LayoutParams lp; 399 boolean hasVerticalBarLayout = isVerticalBarLayout(); 400 final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); 401 402 // Layout the search bar space 403 Point searchBarBounds = getSearchBarDimensForWidgetOpts(); 404 View searchBar = launcher.getDropTargetBar(); 405 lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); 406 lp.width = searchBarBounds.x; 407 lp.height = searchBarBounds.y; 408 lp.topMargin = edgeMarginPx; 409 searchBar.setLayoutParams(lp); 410 411 // Layout the workspace 412 PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace); 413 lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); 414 lp.gravity = Gravity.CENTER; 415 Rect padding = getWorkspacePadding(); 416 workspace.setLayoutParams(lp); 417 workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom); 418 workspace.setPageSpacing(getWorkspacePageSpacing()); 419 420 // Layout the hotseat 421 View hotseat = launcher.findViewById(R.id.hotseat); 422 lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams(); 423 // We want the edges of the hotseat to line up with the edges of the workspace, but the 424 // icons in the hotseat are a different size, and so don't line up perfectly. To account for 425 // this, we pad the left and right of the hotseat with half of the difference of a workspace 426 // cell vs a hotseat cell. 427 float workspaceCellWidth = (float) getCurrentWidth() / inv.numColumns; 428 float hotseatCellWidth = (float) getCurrentWidth() / inv.numHotseatIcons; 429 int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2); 430 if (hasVerticalBarLayout) { 431 // Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the 432 // screen regardless of RTL 433 lp.gravity = Gravity.RIGHT; 434 lp.width = hotseatBarHeightPx; 435 lp.height = LayoutParams.MATCH_PARENT; 436 hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx); 437 } else if (isTablet) { 438 // Pad the hotseat with the workspace padding calculated above 439 lp.gravity = Gravity.BOTTOM; 440 lp.width = LayoutParams.MATCH_PARENT; 441 lp.height = hotseatBarHeightPx; 442 hotseat.findViewById(R.id.layout).setPadding( 443 hotseatAdjustment + padding.left, 0, 444 hotseatAdjustment + padding.right, 2 * edgeMarginPx); 445 } else { 446 // For phones, layout the hotseat without any bottom margin 447 // to ensure that we have space for the folders 448 lp.gravity = Gravity.BOTTOM; 449 lp.width = LayoutParams.MATCH_PARENT; 450 lp.height = hotseatBarHeightPx; 451 hotseat.findViewById(R.id.layout).setPadding( 452 hotseatAdjustment + padding.left, 0, 453 hotseatAdjustment + padding.right, 0); 454 } 455 hotseat.setLayoutParams(lp); 456 457 // Layout the page indicators 458 View pageIndicator = launcher.findViewById(R.id.page_indicator); 459 if (pageIndicator != null) { 460 if (hasVerticalBarLayout) { 461 // Hide the page indicators when we have vertical search/hotseat 462 pageIndicator.setVisibility(View.GONE); 463 } else { 464 // Put the page indicators above the hotseat 465 lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams(); 466 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 467 lp.width = LayoutParams.WRAP_CONTENT; 468 lp.bottomMargin = hotseatBarHeightPx; 469 pageIndicator.setLayoutParams(lp); 470 } 471 } 472 473 // Layout the Overview Mode 474 ViewGroup overviewMode = launcher.getOverviewPanel(); 475 if (overviewMode != null) { 476 lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams(); 477 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 478 479 int visibleChildCount = getVisibleChildCount(overviewMode); 480 int totalItemWidth = visibleChildCount * overviewModeBarItemWidthPx; 481 int maxWidth = totalItemWidth + (visibleChildCount-1) * overviewModeBarSpacerWidthPx; 482 483 lp.width = Math.min(availableWidthPx, maxWidth); 484 lp.height = getOverviewModeButtonBarHeight(); 485 overviewMode.setLayoutParams(lp); 486 487 if (lp.width > totalItemWidth && visibleChildCount > 1) { 488 // We have enough space. Lets add some margin too. 489 int margin = (lp.width - totalItemWidth) / (visibleChildCount-1); 490 View lastChild = null; 491 492 // Set margin of all visible children except the last visible child 493 for (int i = 0; i < visibleChildCount; i++) { 494 if (lastChild != null) { 495 MarginLayoutParams clp = (MarginLayoutParams) lastChild.getLayoutParams(); 496 if (isLayoutRtl) { 497 clp.leftMargin = margin; 498 } else { 499 clp.rightMargin = margin; 500 } 501 lastChild.setLayoutParams(clp); 502 lastChild = null; 503 } 504 View thisChild = overviewMode.getChildAt(i); 505 if (thisChild.getVisibility() != View.GONE) { 506 lastChild = thisChild; 507 } 508 } 509 } 510 } 511 } 512 513 private int getCurrentWidth() { 514 return isLandscape 515 ? Math.max(widthPx, heightPx) 516 : Math.min(widthPx, heightPx); 517 } 518 519 private int getCurrentHeight() { 520 return isLandscape 521 ? Math.min(widthPx, heightPx) 522 : Math.max(widthPx, heightPx); 523 } 524 525 526 public static final int getContainerPadding(Context context, int availableWidth) { 527 Resources res = context.getResources(); 528 529 int maxSize = res.getDimensionPixelSize(R.dimen.container_max_width); 530 int minMargin = res.getDimensionPixelSize(R.dimen.container_min_margin); 531 532 if (maxSize > 0) { 533 return Math.max(minMargin, (availableWidth - maxSize) / 2); 534 } else { 535 return Math.max(minMargin, 536 (int) res.getFraction(R.fraction.container_margin, availableWidth, 1)); 537 } 538 } 539} 540