blob: 37cccfbd304305eb2083cb067cad4a7b161c2bdc [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;
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}