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