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