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