DynamicGrid.java revision 5f8afe6280eae34620067696173e71943e1a30a3
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 iconSizePx; 72 int iconTextSizePx; 73 int cellWidthPx; 74 int cellHeightPx; 75 int folderBackgroundOffset; 76 int folderIconSizePx; 77 int folderCellWidthPx; 78 int folderCellHeightPx; 79 int hotseatCellWidthPx; 80 int hotseatCellHeightPx; 81 int hotseatIconSizePx; 82 int hotseatBarHeightPx; 83 int searchBarSpaceWidthPx; 84 int searchBarSpaceMaxWidthPx; 85 int searchBarSpaceHeightPx; 86 int searchBarHeightPx; 87 int pageIndicatorHeightPx; 88 89 DeviceProfile(String n, float w, float h, float r, float c, 90 float is, float its, float hs, float his) { 91 name = n; 92 minWidthDps = w; 93 minHeightDps = h; 94 numRows = r; 95 numColumns = c; 96 iconSize = is; 97 iconTextSize = its; 98 numHotseatIcons = hs; 99 hotseatIconSize = his; 100 } 101 102 DeviceProfile(ArrayList<DeviceProfile> profiles, 103 float minWidth, int minWidthPx, 104 float minHeight, int minHeightPx, 105 int wPx, int hPx, 106 Resources resources) { 107 DisplayMetrics dm = resources.getDisplayMetrics(); 108 ArrayList<DeviceProfileQuery> points = 109 new ArrayList<DeviceProfileQuery>(); 110 transposeLayoutWithOrientation = 111 resources.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); 112 updateFromConfiguration(resources, wPx, hPx); 113 minWidthDps = minWidth; 114 minHeightDps = minHeight; 115 116 edgeMarginPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); 117 pageIndicatorHeightPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height); 118 119 // Interpolate the rows 120 for (DeviceProfile p : profiles) { 121 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numRows)); 122 } 123 numRows = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); 124 // Interpolate the columns 125 points.clear(); 126 for (DeviceProfile p : profiles) { 127 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numColumns)); 128 } 129 numColumns = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); 130 // Interpolate the icon size 131 points.clear(); 132 for (DeviceProfile p : profiles) { 133 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconSize)); 134 } 135 iconSize = invDistWeightedInterpolate(minWidth, minHeight, points); 136 iconSizePx = (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 137 iconSize, dm)); 138 // Interpolate the icon text size 139 points.clear(); 140 for (DeviceProfile p : profiles) { 141 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconTextSize)); 142 } 143 iconTextSize = invDistWeightedInterpolate(minWidth, minHeight, points); 144 iconTextSizePx = (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 145 iconTextSize, dm)); 146 // Interpolate the hotseat size 147 points.clear(); 148 for (DeviceProfile p : profiles) { 149 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numHotseatIcons)); 150 } 151 numHotseatIcons = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); 152 // Interpolate the hotseat icon size 153 points.clear(); 154 for (DeviceProfile p : profiles) { 155 points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.hotseatIconSize)); 156 } 157 158 // Hotseat 159 hotseatIconSize = invDistWeightedInterpolate(minWidth, minHeight, points); 160 hotseatIconSizePx = (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 161 hotseatIconSize, dm)); 162 hotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx; 163 hotseatCellWidthPx = iconSizePx; 164 hotseatCellHeightPx = iconSizePx; 165 166 // Search Bar 167 searchBarSpaceMaxWidthPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width); 168 searchBarHeightPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); 169 searchBarSpaceWidthPx = Math.min(searchBarSpaceMaxWidthPx, widthPx); 170 searchBarSpaceHeightPx = searchBarHeightPx + 2 * edgeMarginPx; 171 172 // Calculate the actual text height 173 Paint textPaint = new Paint(); 174 textPaint.setTextSize(iconTextSizePx); 175 FontMetrics fm = textPaint.getFontMetrics(); 176 cellWidthPx = iconSizePx; 177 cellHeightPx = iconSizePx + (int) Math.ceil(fm.bottom - fm.top); 178 179 // Folder 180 folderCellWidthPx = cellWidthPx + 3 * edgeMarginPx; 181 folderCellHeightPx = cellHeightPx + edgeMarginPx; 182 folderBackgroundOffset = -edgeMarginPx; 183 folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset; 184 } 185 186 void updateFromConfiguration(Resources resources, int wPx, int hPx) { 187 isLandscape = (resources.getConfiguration().orientation == 188 Configuration.ORIENTATION_LANDSCAPE); 189 isTablet = resources.getBoolean(R.bool.is_tablet); 190 isLargeTablet = resources.getBoolean(R.bool.is_large_tablet); 191 widthPx = wPx; 192 heightPx = hPx; 193 } 194 195 private float dist(PointF p0, PointF p1) { 196 return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) + 197 (p1.y-p0.y)*(p1.y-p0.y)); 198 } 199 200 private float weight(PointF a, PointF b, 201 float pow) { 202 float d = dist(a, b); 203 if (d == 0f) { 204 return Float.POSITIVE_INFINITY; 205 } 206 return (float) (1f / Math.pow(d, pow)); 207 } 208 209 private float invDistWeightedInterpolate(float width, float height, 210 ArrayList<DeviceProfileQuery> points) { 211 float sum = 0; 212 float weights = 0; 213 float pow = 5; 214 float kNearestNeighbors = 3; 215 final PointF xy = new PointF(width, height); 216 217 ArrayList<DeviceProfileQuery> pointsByNearness = points; 218 Collections.sort(pointsByNearness, new Comparator<DeviceProfileQuery>() { 219 public int compare(DeviceProfileQuery a, DeviceProfileQuery b) { 220 return (int) (dist(xy, a.dimens) - dist(xy, b.dimens)); 221 } 222 }); 223 224 for (int i = 0; i < pointsByNearness.size(); ++i) { 225 DeviceProfileQuery p = pointsByNearness.get(i); 226 if (i < kNearestNeighbors) { 227 float w = weight(xy, p.dimens, pow); 228 if (w == Float.POSITIVE_INFINITY) { 229 return p.value; 230 } 231 weights += w; 232 } 233 } 234 235 for (int i = 0; i < pointsByNearness.size(); ++i) { 236 DeviceProfileQuery p = pointsByNearness.get(i); 237 if (i < kNearestNeighbors) { 238 float w = weight(xy, p.dimens, pow); 239 sum += w * p.value / weights; 240 } 241 } 242 243 return sum; 244 } 245 246 Rect getWorkspacePadding(int orientation) { 247 Rect padding = new Rect(); 248 if (orientation == CellLayout.LANDSCAPE && 249 transposeLayoutWithOrientation) { 250 // Pad the left and right of the workspace with search/hotseat bar sizes 251 padding.set(searchBarSpaceHeightPx, edgeMarginPx, 252 hotseatBarHeightPx, edgeMarginPx); 253 } else { 254 if (isTablet()) { 255 // Pad the left and right of the workspace to ensure consistent spacing 256 // between all icons 257 int width = (orientation == CellLayout.LANDSCAPE) 258 ? Math.max(widthPx, heightPx) 259 : Math.min(widthPx, heightPx); 260 // XXX: If the icon size changes across orientations, we will have to take 261 // that into account here too. 262 int gap = (int) ((width - 2 * edgeMarginPx - 263 (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); 264 padding.set(edgeMarginPx + gap, 265 searchBarSpaceHeightPx, 266 edgeMarginPx + gap, 267 hotseatBarHeightPx + pageIndicatorHeightPx); 268 } else { 269 // Pad the top and bottom of the workspace with search/hotseat bar sizes 270 padding.set(edgeMarginPx, 271 searchBarSpaceHeightPx, 272 edgeMarginPx, 273 hotseatBarHeightPx + pageIndicatorHeightPx); 274 } 275 } 276 return padding; 277 } 278 279 int calculateCellWidth(int width, int countX) { 280 return width / countX; 281 } 282 int calculateCellHeight(int height, int countY) { 283 return height / countY; 284 } 285 286 boolean isTablet() { 287 return isTablet; 288 } 289 290 boolean isLargeTablet() { 291 return isLargeTablet; 292 } 293 294 public void layout(Launcher launcher) { 295 FrameLayout.LayoutParams lp; 296 Resources res = launcher.getResources(); 297 boolean hasVerticalBarLayout = isLandscape && 298 res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); 299 300 // Layout the search bar space 301 View searchBarSpace = launcher.findViewById(R.id.qsb_bar); 302 lp = (FrameLayout.LayoutParams) searchBarSpace.getLayoutParams(); 303 if (hasVerticalBarLayout) { 304 // Vertical search bar 305 lp.gravity = Gravity.TOP | Gravity.LEFT; 306 lp.width = searchBarSpaceHeightPx; 307 lp.height = LayoutParams.MATCH_PARENT; 308 searchBarSpace.setPadding( 309 0, 2 * edgeMarginPx, 0, 310 2 * edgeMarginPx); 311 } else { 312 // Horizontal search bar 313 lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; 314 lp.width = searchBarSpaceWidthPx; 315 lp.height = searchBarSpaceHeightPx; 316 searchBarSpace.setPadding( 317 2 * edgeMarginPx, 318 2 * edgeMarginPx, 319 2 * edgeMarginPx, 0); 320 } 321 searchBarSpace.setLayoutParams(lp); 322 323 // Layout the search bar 324 View searchBar = searchBarSpace.findViewById(R.id.qsb_search_bar); 325 lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); 326 lp.width = LayoutParams.MATCH_PARENT; 327 lp.height = LayoutParams.MATCH_PARENT; 328 searchBar.setLayoutParams(lp); 329 330 // Layout the voice proxy 331 View voiceButtonProxy = launcher.findViewById(R.id.voice_button_proxy); 332 if (voiceButtonProxy != null) { 333 if (hasVerticalBarLayout) { 334 // TODO: MOVE THIS INTO SEARCH BAR MEASURE 335 } else { 336 lp = (FrameLayout.LayoutParams) voiceButtonProxy.getLayoutParams(); 337 lp.gravity = Gravity.TOP | Gravity.END; 338 lp.width = (widthPx - searchBarSpaceWidthPx) / 2 + 339 2 * iconSizePx; 340 lp.height = searchBarSpaceHeightPx; 341 } 342 } 343 344 // Layout the workspace 345 View workspace = launcher.findViewById(R.id.workspace); 346 lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); 347 lp.gravity = Gravity.CENTER; 348 Rect padding = getWorkspacePadding(isLandscape 349 ? CellLayout.LANDSCAPE 350 : CellLayout.PORTRAIT); 351 workspace.setPadding(padding.left, padding.top, 352 padding.right, padding.bottom); 353 workspace.setLayoutParams(lp); 354 355 // Layout the hotseat 356 View hotseat = launcher.findViewById(R.id.hotseat); 357 lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams(); 358 if (hasVerticalBarLayout) { 359 // Vertical hotseat 360 lp.gravity = Gravity.RIGHT; 361 lp.width = hotseatBarHeightPx; 362 lp.height = LayoutParams.MATCH_PARENT; 363 hotseat.setPadding(0, 2 * edgeMarginPx, 364 2 * edgeMarginPx, 2 * edgeMarginPx); 365 } else if (isTablet()) { 366 // Pad the hotseat with the grid gap calculated above 367 int gridGap = (int) ((widthPx - 2 * edgeMarginPx - 368 (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); 369 int gridWidth = (int) ((numColumns * cellWidthPx) + 370 ((numColumns - 1) * gridGap)); 371 int hotseatGap = (int) Math.max(0, 372 (gridWidth - (numHotseatIcons * hotseatCellWidthPx)) 373 / (numHotseatIcons - 1)); 374 lp.gravity = Gravity.BOTTOM; 375 lp.width = LayoutParams.MATCH_PARENT; 376 lp.height = hotseatBarHeightPx; 377 hotseat.setPadding(2 * edgeMarginPx + gridGap + hotseatGap, 0, 378 2 * edgeMarginPx + gridGap + hotseatGap, 379 2 * edgeMarginPx); 380 } else { 381 // For phones, layout the hotseat without any bottom margin 382 // to ensure that we have space for the folders 383 lp.gravity = Gravity.BOTTOM; 384 lp.width = LayoutParams.MATCH_PARENT; 385 lp.height = hotseatBarHeightPx; 386 hotseat.setPadding(2 * edgeMarginPx, 0, 387 2 * edgeMarginPx, 0); 388 } 389 hotseat.setLayoutParams(lp); 390 391 // Layout the page indicators 392 View pageIndicator = launcher.findViewById(R.id.page_indicator); 393 if (pageIndicator != null) { 394 if (hasVerticalBarLayout) { 395 // Hide the page indicators when we have vertical search/hotseat 396 pageIndicator.setVisibility(View.GONE); 397 } else { 398 // Put the page indicators above the hotseat 399 lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams(); 400 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 401 lp.width = LayoutParams.WRAP_CONTENT; 402 lp.height = pageIndicatorHeightPx; 403 lp.bottomMargin = hotseatBarHeightPx; 404 pageIndicator.setLayoutParams(lp); 405 } 406 } 407 } 408} 409 410public class DynamicGrid { 411 @SuppressWarnings("unused") 412 private static final String TAG = "DynamicGrid"; 413 414 private DeviceProfile mProfile; 415 private float mMinWidth; 416 private float mMinHeight; 417 418 public static int dpiFromPx(int size, DisplayMetrics metrics){ 419 float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; 420 return (int) Math.round(size / densityRatio); 421 } 422 423 public DynamicGrid(Resources resources, int minWidthPx, int minHeightPx, 424 int widthPx, int heightPx) { 425 DisplayMetrics dm = resources.getDisplayMetrics(); 426 ArrayList<DeviceProfile> deviceProfiles = 427 new ArrayList<DeviceProfile>(); 428 // Our phone profiles include the bar sizes in each orientation 429 deviceProfiles.add(new DeviceProfile("Super Short Stubby", 430 255, 300, 2, 3, 48, 12, 4, 48)); 431 deviceProfiles.add(new DeviceProfile("Shorter Stubby", 432 255, 400, 3, 3, 48, 12, 4, 48)); 433 deviceProfiles.add(new DeviceProfile("Short Stubby", 434 275, 420, 3, 4, 48, 12, 4, 48)); 435 deviceProfiles.add(new DeviceProfile("Stubby", 436 255, 450, 3, 4, 48, 12, 4, 48)); 437 deviceProfiles.add(new DeviceProfile("Nexus S", 438 296, 491.33f, 4, 4, 48, 12, 4, 48)); 439 deviceProfiles.add(new DeviceProfile("Nexus 4", 440 359, 518, 4, 4, 60, 12, 5, 56)); 441 // The tablet profile is odd in that the landscape orientation 442 // also includes the nav bar on the side 443 deviceProfiles.add(new DeviceProfile("Nexus 7", 444 575, 904, 6, 6, 72, 14.4f, 7, 60)); 445 // Larger tablet profiles always have system bars on the top & bottom 446 deviceProfiles.add(new DeviceProfile("Nexus 10", 447 727, 1207, 5, 8, 80, 14.4f, 9, 64)); 448 /* 449 deviceProfiles.add(new DeviceProfile("Nexus 7", 450 600, 960, 5, 5, 72, 14.4f, 5, 60)); 451 deviceProfiles.add(new DeviceProfile("Nexus 10", 452 800, 1280, 5, 5, 80, 14.4f, 6, 64)); 453 */ 454 deviceProfiles.add(new DeviceProfile("20-inch Tablet", 455 1527, 2527, 7, 7, 100, 20, 7, 72)); 456 mMinWidth = dpiFromPx(minWidthPx, dm); 457 mMinHeight = dpiFromPx(minHeightPx, dm); 458 mProfile = new DeviceProfile(deviceProfiles, 459 mMinWidth, minWidthPx, 460 mMinHeight, minHeightPx, 461 widthPx, heightPx, 462 resources); 463 } 464 465 DeviceProfile getDeviceProfile() { 466 return mProfile; 467 } 468 469 public String toString() { 470 return "-------- DYNAMIC GRID ------- \n" + 471 "Wd: " + mProfile.minWidthDps + ", Hd: " + mProfile.minHeightDps + 472 ", W: " + mProfile.widthPx + ", H: " + mProfile.heightPx + 473 " [r: " + mProfile.numRows + ", c: " + mProfile.numColumns + 474 ", is: " + mProfile.iconSizePx + ", its: " + mProfile.iconTextSize + 475 ", cw: " + mProfile.cellWidthPx + ", ch: " + mProfile.cellHeightPx + 476 ", hc: " + mProfile.numHotseatIcons + ", his: " + mProfile.hotseatIconSizePx + "]"; 477 } 478} 479