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