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