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