DeviceProfile.java revision 0f785720667ab8afe4b4620a6c333d382d8659ed
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.appwidget.AppWidgetHostView;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.graphics.Paint;
25import android.graphics.Paint.FontMetrics;
26import android.graphics.Point;
27import android.graphics.PointF;
28import android.graphics.Rect;
29import android.util.DisplayMetrics;
30import android.view.Display;
31import android.view.Gravity;
32import android.view.Surface;
33import android.view.View;
34import android.view.ViewGroup;
35import android.view.ViewGroup.LayoutParams;
36import android.view.ViewGroup.MarginLayoutParams;
37import android.view.WindowManager;
38import android.widget.FrameLayout;
39import android.widget.LinearLayout;
40
41import com.android.launcher3.util.Thunk;
42
43import java.util.ArrayList;
44import java.util.Collections;
45import java.util.Comparator;
46
47
48class DeviceProfileQuery {
49    DeviceProfile profile;
50    float widthDps;
51    float heightDps;
52    float value;
53    PointF dimens;
54
55    DeviceProfileQuery(DeviceProfile p, float v) {
56        widthDps = p.minWidthDps;
57        heightDps = p.minHeightDps;
58        value = v;
59        dimens = new PointF(widthDps, heightDps);
60        profile = p;
61    }
62}
63
64public class DeviceProfile {
65    public static interface DeviceProfileCallbacks {
66        public void onAvailableSizeChanged(DeviceProfile grid);
67    }
68
69    String name;
70    float minWidthDps;
71    float minHeightDps;
72    public float numRows;
73    public float numColumns;
74    float numHotseatIcons;
75    float iconSize;
76    private float iconTextSize;
77    private int iconDrawablePaddingOriginalPx;
78    private float hotseatIconSize;
79
80    int defaultLayoutId;
81
82    boolean isLandscape;
83    boolean isTablet;
84    boolean isLargeTablet;
85    boolean isLayoutRtl;
86
87    boolean transposeLayoutWithOrientation;
88
89    int desiredWorkspaceLeftRightMarginPx;
90    public int edgeMarginPx;
91    Rect defaultWidgetPadding;
92
93    int widthPx;
94    int heightPx;
95    public int availableWidthPx;
96    public int availableHeightPx;
97    int defaultPageSpacingPx;
98
99    int overviewModeMinIconZoneHeightPx;
100    int overviewModeMaxIconZoneHeightPx;
101    int overviewModeBarItemWidthPx;
102    int overviewModeBarSpacerWidthPx;
103    float overviewModeIconZoneRatio;
104    float overviewModeScaleFactor;
105
106    public int cellWidthPx;
107    public int cellHeightPx;
108
109    public int iconSizePx;
110    public int iconTextSizePx;
111    int iconDrawablePaddingPx;
112    int allAppsIconSizePx;
113    int allAppsIconTextSizePx;
114    int allAppsCellWidthPx;
115    int allAppsCellHeightPx;
116    int allAppsCellPaddingPx;
117    int folderBackgroundOffset;
118    int folderIconSizePx;
119    int folderCellWidthPx;
120    int folderCellHeightPx;
121    int hotseatCellWidthPx;
122    int hotseatCellHeightPx;
123    int hotseatIconSizePx;
124    int hotseatBarHeightPx;
125    int hotseatAllAppsRank;
126    int allAppsNumRows;
127    int allAppsNumCols;
128    int appsViewNumCols;
129    int searchBarSpaceWidthPx;
130    int searchBarSpaceHeightPx;
131    int pageIndicatorHeightPx;
132    int allAppsButtonVisualSize;
133
134    float dragViewScale;
135
136    int allAppsShortEdgeCount = -1;
137    int allAppsLongEdgeCount = -1;
138
139    private ArrayList<DeviceProfileCallbacks> mCallbacks = new ArrayList<DeviceProfileCallbacks>();
140
141    DeviceProfile(String n, float w, float h, float r, float c,
142                  float is, float its, float hs, float his, int dlId) {
143        // Ensure that we have an odd number of hotseat items (since we need to place all apps)
144        if (hs % 2 == 0) {
145            throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces");
146        }
147
148        name = n;
149        minWidthDps = w;
150        minHeightDps = h;
151        numRows = r;
152        numColumns = c;
153        iconSize = is;
154        iconTextSize = its;
155        numHotseatIcons = hs;
156        hotseatIconSize = his;
157        defaultLayoutId = dlId;
158    }
159
160    DeviceProfile() {
161    }
162
163    DeviceProfile(Context context,
164                  ArrayList<DeviceProfile> profiles,
165                  float minWidth, float minHeight,
166                  int wPx, int hPx,
167                  int awPx, int ahPx,
168                  Resources res) {
169        DisplayMetrics dm = res.getDisplayMetrics();
170        ArrayList<DeviceProfileQuery> points =
171                new ArrayList<DeviceProfileQuery>();
172        transposeLayoutWithOrientation =
173                res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
174        minWidthDps = minWidth;
175        minHeightDps = minHeight;
176
177        ComponentName cn = new ComponentName(context.getPackageName(),
178                this.getClass().getName());
179        defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
180        edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
181        desiredWorkspaceLeftRightMarginPx = 2 * edgeMarginPx;
182        pageIndicatorHeightPx =
183                res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height);
184        defaultPageSpacingPx =
185                res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing);
186        allAppsCellPaddingPx =
187                res.getDimensionPixelSize(R.dimen.dynamic_grid_all_apps_cell_padding);
188        overviewModeMinIconZoneHeightPx =
189                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height);
190        overviewModeMaxIconZoneHeightPx =
191                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_max_icon_zone_height);
192        overviewModeBarItemWidthPx =
193                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_item_width);
194        overviewModeBarSpacerWidthPx =
195                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_spacer_width);
196        overviewModeIconZoneRatio =
197                res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f;
198        overviewModeScaleFactor =
199                res.getInteger(R.integer.config_dynamic_grid_overview_scale_percentage) / 100f;
200
201        // Find the closes profile given the width/height
202        for (DeviceProfile p : profiles) {
203            points.add(new DeviceProfileQuery(p, 0f));
204        }
205        DeviceProfile closestProfile = findClosestDeviceProfile(minWidth, minHeight, points);
206
207        // Snap to the closest row count
208        numRows = closestProfile.numRows;
209
210        // Snap to the closest column count
211        numColumns = closestProfile.numColumns;
212
213        // Snap to the closest hotseat size
214        numHotseatIcons = closestProfile.numHotseatIcons;
215        hotseatAllAppsRank = (int) (numHotseatIcons / 2);
216
217        // Snap to the closest default layout id
218        defaultLayoutId = closestProfile.defaultLayoutId;
219
220        // Interpolate the icon size
221        points.clear();
222        for (DeviceProfile p : profiles) {
223            points.add(new DeviceProfileQuery(p, p.iconSize));
224        }
225        iconSize = invDistWeightedInterpolate(minWidth, minHeight, points);
226
227        // AllApps uses the original non-scaled icon size
228        allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm);
229
230        // Interpolate the icon text size
231        points.clear();
232        for (DeviceProfile p : profiles) {
233            points.add(new DeviceProfileQuery(p, p.iconTextSize));
234        }
235        iconTextSize = invDistWeightedInterpolate(minWidth, minHeight, points);
236        iconDrawablePaddingOriginalPx =
237                res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);
238        // AllApps uses the original non-scaled icon text size
239        allAppsIconTextSizePx = DynamicGrid.pxFromDp(iconTextSize, dm);
240
241        // Interpolate the hotseat icon size
242        points.clear();
243        for (DeviceProfile p : profiles) {
244            points.add(new DeviceProfileQuery(p, p.hotseatIconSize));
245        }
246        // Hotseat
247        hotseatIconSize = invDistWeightedInterpolate(minWidth, minHeight, points);
248
249        // If the partner customization apk contains any grid overrides, apply them
250        applyPartnerDeviceProfileOverrides(context, dm);
251
252        // Calculate the remaining vars
253        updateFromConfiguration(context, res, wPx, hPx, awPx, ahPx);
254        updateAvailableDimensions(context);
255        computeAllAppsButtonSize(context);
256    }
257
258    /**
259     * Apply any Partner customization grid overrides.
260     *
261     * Currently we support: all apps row / column count.
262     */
263    private void applyPartnerDeviceProfileOverrides(Context ctx, DisplayMetrics dm) {
264        Partner p = Partner.get(ctx.getPackageManager());
265        if (p != null) {
266            DeviceProfile partnerDp = p.getDeviceProfileOverride(dm);
267            if (partnerDp != null) {
268                if (partnerDp.numRows > 0 && partnerDp.numColumns > 0) {
269                    numRows = partnerDp.numRows;
270                    numColumns = partnerDp.numColumns;
271                }
272                if (partnerDp.allAppsShortEdgeCount > 0 && partnerDp.allAppsLongEdgeCount > 0) {
273                    allAppsShortEdgeCount = partnerDp.allAppsShortEdgeCount;
274                    allAppsLongEdgeCount = partnerDp.allAppsLongEdgeCount;
275                }
276                if (partnerDp.iconSize > 0) {
277                    iconSize = partnerDp.iconSize;
278                    // AllApps uses the original non-scaled icon size
279                    allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm);
280                }
281            }
282        }
283    }
284
285    /**
286     * Determine the exact visual footprint of the all apps button, taking into account scaling
287     * and internal padding of the drawable.
288     */
289    private void computeAllAppsButtonSize(Context context) {
290        Resources res = context.getResources();
291        float padding = res.getInteger(R.integer.config_allAppsButtonPaddingPercent) / 100f;
292        LauncherAppState app = LauncherAppState.getInstance();
293        allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding));
294    }
295
296    void addCallback(DeviceProfileCallbacks cb) {
297        mCallbacks.add(cb);
298        cb.onAvailableSizeChanged(this);
299    }
300    void removeCallback(DeviceProfileCallbacks cb) {
301        mCallbacks.remove(cb);
302    }
303
304    private int getDeviceOrientation(Context context) {
305        WindowManager windowManager =  (WindowManager)
306                context.getSystemService(Context.WINDOW_SERVICE);
307        Resources resources = context.getResources();
308        DisplayMetrics dm = resources.getDisplayMetrics();
309        Configuration config = resources.getConfiguration();
310        int rotation = windowManager.getDefaultDisplay().getRotation();
311
312        boolean isLandscape = (config.orientation == Configuration.ORIENTATION_LANDSCAPE) &&
313                (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180);
314        boolean isRotatedPortrait = (config.orientation == Configuration.ORIENTATION_PORTRAIT) &&
315                (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270);
316        if (isLandscape || isRotatedPortrait) {
317            return CellLayout.LANDSCAPE;
318        } else {
319            return CellLayout.PORTRAIT;
320        }
321    }
322
323    private void updateAvailableDimensions(Context context) {
324        WindowManager windowManager =  (WindowManager)
325                context.getSystemService(Context.WINDOW_SERVICE);
326        Display display = windowManager.getDefaultDisplay();
327        Resources resources = context.getResources();
328        DisplayMetrics dm = resources.getDisplayMetrics();
329        Configuration config = resources.getConfiguration();
330
331        // There are three possible configurations that the dynamic grid accounts for, portrait,
332        // landscape with the nav bar at the bottom, and landscape with the nav bar at the side.
333        // To prevent waiting for fitSystemWindows(), we make the observation that in landscape,
334        // the height is the smallest height (either with the nav bar at the bottom or to the
335        // side) and otherwise, the height is simply the largest possible height for a portrait
336        // device.
337        Point size = new Point();
338        Point smallestSize = new Point();
339        Point largestSize = new Point();
340        display.getSize(size);
341        display.getCurrentSizeRange(smallestSize, largestSize);
342        availableWidthPx = size.x;
343        if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
344            availableHeightPx = smallestSize.y;
345        } else {
346            availableHeightPx = largestSize.y;
347        }
348
349        // Check to see if the icons fit in the new available height.  If not, then we need to
350        // shrink the icon size.
351        float scale = 1f;
352        int drawablePadding = iconDrawablePaddingOriginalPx;
353        updateIconSize(1f, drawablePadding, resources, dm);
354        float usedHeight = (cellHeightPx * numRows);
355
356        Rect workspacePadding = getWorkspacePadding();
357        int maxHeight = (availableHeightPx - workspacePadding.top - workspacePadding.bottom);
358        if (usedHeight > maxHeight) {
359            scale = maxHeight / usedHeight;
360            drawablePadding = 0;
361        }
362        updateIconSize(scale, drawablePadding, resources, dm);
363
364        // Make the callbacks
365        for (DeviceProfileCallbacks cb : mCallbacks) {
366            cb.onAvailableSizeChanged(this);
367        }
368    }
369
370    private void updateIconSize(float scale, int drawablePadding, Resources res,
371                                DisplayMetrics dm) {
372        iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale);
373        iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale);
374        iconDrawablePaddingPx = drawablePadding;
375        hotseatIconSizePx = (int) (DynamicGrid.pxFromDp(hotseatIconSize, dm) * scale);
376
377        // Search Bar
378        searchBarSpaceWidthPx = Math.min(widthPx,
379                res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
380        searchBarSpaceHeightPx = getSearchBarTopOffset()
381                + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
382
383        // Calculate the actual text height
384        Paint textPaint = new Paint();
385        textPaint.setTextSize(iconTextSizePx);
386        FontMetrics fm = textPaint.getFontMetrics();
387        cellWidthPx = iconSizePx;
388        cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top);
389        final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale);
390        dragViewScale = (iconSizePx + scaleDps) / iconSizePx;
391
392        // Hotseat
393        hotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx;
394        hotseatCellWidthPx = iconSizePx;
395        hotseatCellHeightPx = iconSizePx;
396
397        // Folder
398        folderCellWidthPx = cellWidthPx + 3 * edgeMarginPx;
399        folderCellHeightPx = cellHeightPx + edgeMarginPx;
400        folderBackgroundOffset = -edgeMarginPx;
401        folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset;
402
403        // All Apps
404        allAppsCellWidthPx = allAppsIconSizePx;
405        allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx;
406        int maxLongEdgeCellCount =
407                res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
408        int maxShortEdgeCellCount =
409                res.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
410        int minEdgeCellCount =
411                res.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
412        int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount);
413        int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount);
414
415        if (allAppsShortEdgeCount > 0 && allAppsLongEdgeCount > 0) {
416            allAppsNumRows = isLandscape ? allAppsShortEdgeCount : allAppsLongEdgeCount;
417            allAppsNumCols = isLandscape ? allAppsLongEdgeCount : allAppsShortEdgeCount;
418        } else {
419            allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) /
420                    (allAppsCellHeightPx + allAppsCellPaddingPx);
421            allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows));
422            allAppsNumCols = (availableWidthPx) / (allAppsCellWidthPx + allAppsCellPaddingPx);
423            allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols));
424        }
425
426        int appsContainerViewWidthPx = res.getDimensionPixelSize(R.dimen.apps_container_width);
427        updateAppsViewNumCols(res, appsContainerViewWidthPx);
428    }
429
430    public boolean updateAppsViewNumCols(Resources res, int containerWidth) {
431        int appsViewLeftMarginPx =
432                res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
433        int availableAppsWidthPx = (containerWidth > 0) ? containerWidth : availableWidthPx;
434        int numCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
435                (allAppsCellWidthPx + 2 * allAppsCellPaddingPx);
436        if (numCols != appsViewNumCols) {
437            appsViewNumCols = numCols;
438            return true;
439        }
440        return false;
441    }
442
443    void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx,
444                                 int awPx, int ahPx) {
445        Configuration configuration = resources.getConfiguration();
446        isLandscape = (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE);
447        isTablet = resources.getBoolean(R.bool.is_tablet);
448        isLargeTablet = resources.getBoolean(R.bool.is_large_tablet);
449        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
450            isLayoutRtl = (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
451        } else {
452            isLayoutRtl = false;
453        }
454        widthPx = wPx;
455        heightPx = hPx;
456        availableWidthPx = awPx;
457        availableHeightPx = ahPx;
458
459        updateAvailableDimensions(context);
460    }
461
462    @Thunk float dist(PointF p0, PointF p1) {
463        return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) +
464                (p1.y-p0.y)*(p1.y-p0.y));
465    }
466
467    private float weight(PointF a, PointF b,
468                        float pow) {
469        float d = dist(a, b);
470        if (d == 0f) {
471            return Float.POSITIVE_INFINITY;
472        }
473        return (float) (1f / Math.pow(d, pow));
474    }
475
476    /** Returns the closest device profile given the width and height and a list of profiles */
477    private DeviceProfile findClosestDeviceProfile(float width, float height,
478                                                   ArrayList<DeviceProfileQuery> points) {
479        return findClosestDeviceProfiles(width, height, points).get(0).profile;
480    }
481
482    /** Returns the closest device profiles ordered by closeness to the specified width and height */
483    private ArrayList<DeviceProfileQuery> findClosestDeviceProfiles(float width, float height,
484                                                   ArrayList<DeviceProfileQuery> points) {
485        final PointF xy = new PointF(width, height);
486
487        // Sort the profiles by their closeness to the dimensions
488        ArrayList<DeviceProfileQuery> pointsByNearness = points;
489        Collections.sort(pointsByNearness, new Comparator<DeviceProfileQuery>() {
490            public int compare(DeviceProfileQuery a, DeviceProfileQuery b) {
491                return (int) (dist(xy, a.dimens) - dist(xy, b.dimens));
492            }
493        });
494
495        return pointsByNearness;
496    }
497
498    private float invDistWeightedInterpolate(float width, float height,
499                ArrayList<DeviceProfileQuery> points) {
500        float sum = 0;
501        float weights = 0;
502        float pow = 5;
503        float kNearestNeighbors = 3;
504        final PointF xy = new PointF(width, height);
505
506        ArrayList<DeviceProfileQuery> pointsByNearness = findClosestDeviceProfiles(width, height,
507                points);
508
509        for (int i = 0; i < pointsByNearness.size(); ++i) {
510            DeviceProfileQuery p = pointsByNearness.get(i);
511            if (i < kNearestNeighbors) {
512                float w = weight(xy, p.dimens, pow);
513                if (w == Float.POSITIVE_INFINITY) {
514                    return p.value;
515                }
516                weights += w;
517            }
518        }
519
520        for (int i = 0; i < pointsByNearness.size(); ++i) {
521            DeviceProfileQuery p = pointsByNearness.get(i);
522            if (i < kNearestNeighbors) {
523                float w = weight(xy, p.dimens, pow);
524                sum += w * p.value / weights;
525            }
526        }
527
528        return sum;
529    }
530
531    /** Returns the search bar top offset */
532    int getSearchBarTopOffset() {
533        if (isTablet() && !isVerticalBarLayout()) {
534            return 4 * edgeMarginPx;
535        } else {
536            return 2 * edgeMarginPx;
537        }
538    }
539
540    /** Returns the search bar bounds in the current orientation */
541    Rect getSearchBarBounds() {
542        return getSearchBarBounds(isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT);
543    }
544    /** Returns the search bar bounds in the specified orientation */
545    Rect getSearchBarBounds(int orientation) {
546        Rect bounds = new Rect();
547        if (orientation == CellLayout.LANDSCAPE &&
548                transposeLayoutWithOrientation) {
549            if (isLayoutRtl) {
550                bounds.set(availableWidthPx - searchBarSpaceHeightPx, edgeMarginPx,
551                        availableWidthPx, availableHeightPx - edgeMarginPx);
552            } else {
553                bounds.set(0, edgeMarginPx, searchBarSpaceHeightPx,
554                        availableHeightPx - edgeMarginPx);
555            }
556        } else {
557            if (isTablet()) {
558                // Pad the left and right of the workspace to ensure consistent spacing
559                // between all icons
560                int width = (orientation == CellLayout.LANDSCAPE)
561                        ? Math.max(widthPx, heightPx)
562                        : Math.min(widthPx, heightPx);
563                // XXX: If the icon size changes across orientations, we will have to take
564                //      that into account here too.
565                int gap = (int) ((width - 2 * edgeMarginPx -
566                        (numColumns * cellWidthPx)) / (2 * (numColumns + 1)));
567                bounds.set(edgeMarginPx + gap, getSearchBarTopOffset(),
568                        availableWidthPx - (edgeMarginPx + gap),
569                        searchBarSpaceHeightPx);
570            } else {
571                bounds.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left,
572                        getSearchBarTopOffset(),
573                        availableWidthPx - (desiredWorkspaceLeftRightMarginPx -
574                        defaultWidgetPadding.right), searchBarSpaceHeightPx);
575            }
576        }
577        return bounds;
578    }
579
580    /** Returns the bounds of the workspace page indicators. */
581    Rect getWorkspacePageIndicatorBounds(Rect insets) {
582        Rect workspacePadding = getWorkspacePadding();
583        if (isLandscape && transposeLayoutWithOrientation) {
584            if (isLayoutRtl) {
585                return new Rect(workspacePadding.left, workspacePadding.top,
586                        workspacePadding.left + pageIndicatorHeightPx,
587                        heightPx - workspacePadding.bottom - insets.bottom);
588            } else {
589                int pageIndicatorLeft = widthPx - workspacePadding.right;
590                return new Rect(pageIndicatorLeft, workspacePadding.top,
591                        pageIndicatorLeft + pageIndicatorHeightPx,
592                        heightPx - workspacePadding.bottom - insets.bottom);
593            }
594        } else {
595            int pageIndicatorTop = heightPx - insets.bottom - workspacePadding.bottom;
596            return new Rect(workspacePadding.left, pageIndicatorTop,
597                    widthPx - workspacePadding.right, pageIndicatorTop + pageIndicatorHeightPx);
598        }
599    }
600
601    public int getWorkspaceGridHeight() {
602        Rect p = getWorkspacePadding();
603        return availableHeightPx - p.top - p.bottom;
604    }
605
606    public int getWorkspaceGridWidth() {
607        Rect p = getWorkspacePadding();
608        return availableWidthPx - p.left - p.right;
609    }
610
611    /** Returns the workspace padding in the specified orientation */
612    Rect getWorkspacePadding() {
613        return getWorkspacePadding(isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT);
614    }
615    Rect getWorkspacePadding(int orientation) {
616        Rect searchBarBounds = getSearchBarBounds(orientation);
617        Rect padding = new Rect();
618        if (orientation == CellLayout.LANDSCAPE &&
619                transposeLayoutWithOrientation) {
620            // Pad the left and right of the workspace with search/hotseat bar sizes
621            if (isLayoutRtl) {
622                padding.set(hotseatBarHeightPx, edgeMarginPx,
623                        searchBarBounds.width(), edgeMarginPx);
624            } else {
625                padding.set(searchBarBounds.width(), edgeMarginPx,
626                        hotseatBarHeightPx, edgeMarginPx);
627            }
628        } else {
629            if (isTablet()) {
630                // Pad the left and right of the workspace to ensure consistent spacing
631                // between all icons
632                float gapScale = 1f + (dragViewScale - 1f) / 2f;
633                int width = (orientation == CellLayout.LANDSCAPE)
634                        ? Math.max(widthPx, heightPx)
635                        : Math.min(widthPx, heightPx);
636                int height = (orientation != CellLayout.LANDSCAPE)
637                        ? Math.max(widthPx, heightPx)
638                        : Math.min(widthPx, heightPx);
639                int paddingTop = searchBarBounds.bottom;
640                int paddingBottom = hotseatBarHeightPx + pageIndicatorHeightPx;
641                int availableWidth = Math.max(0, width - (int) ((numColumns * cellWidthPx) +
642                        (numColumns * gapScale * cellWidthPx)));
643                int availableHeight = Math.max(0, height - paddingTop - paddingBottom
644                        - (int) (2 * numRows * cellHeightPx));
645                padding.set(availableWidth / 2, paddingTop + availableHeight / 2,
646                        availableWidth / 2, paddingBottom + availableHeight / 2);
647            } else {
648                // Pad the top and bottom of the workspace with search/hotseat bar sizes
649                padding.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left,
650                        searchBarBounds.bottom,
651                        desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.right,
652                        hotseatBarHeightPx + pageIndicatorHeightPx);
653            }
654        }
655        return padding;
656    }
657
658    int getWorkspacePageSpacing(int orientation) {
659        if ((orientation == CellLayout.LANDSCAPE &&
660                transposeLayoutWithOrientation) || isLargeTablet()) {
661            // In landscape mode the page spacing is set to the default.
662            return defaultPageSpacingPx;
663        } else {
664            // In portrait, we want the pages spaced such that there is no
665            // overhang of the previous / next page into the current page viewport.
666            // We assume symmetrical padding in portrait mode.
667            return Math.max(defaultPageSpacingPx, 2 * getWorkspacePadding().left);
668        }
669    }
670
671    Rect getOverviewModeButtonBarRect() {
672        int zoneHeight = (int) (overviewModeIconZoneRatio * availableHeightPx);
673        zoneHeight = Math.min(overviewModeMaxIconZoneHeightPx,
674                Math.max(overviewModeMinIconZoneHeightPx, zoneHeight));
675        return new Rect(0, availableHeightPx - zoneHeight, 0, availableHeightPx);
676    }
677
678    float getOverviewModeScale() {
679        Rect workspacePadding = getWorkspacePadding();
680        Rect overviewBar = getOverviewModeButtonBarRect();
681        int pageSpace = availableHeightPx - workspacePadding.top - workspacePadding.bottom;
682        return (overviewModeScaleFactor * (pageSpace - overviewBar.height())) / pageSpace;
683    }
684
685    // The rect returned will be extended to below the system ui that covers the workspace
686    Rect getHotseatRect() {
687        if (isVerticalBarLayout()) {
688            return new Rect(availableWidthPx - hotseatBarHeightPx, 0,
689                    Integer.MAX_VALUE, availableHeightPx);
690        } else {
691            return new Rect(0, availableHeightPx - hotseatBarHeightPx,
692                    availableWidthPx, Integer.MAX_VALUE);
693        }
694    }
695
696    int calculateCellWidth(int width, int countX) {
697        return width / countX;
698    }
699    int calculateCellHeight(int height, int countY) {
700        return height / countY;
701    }
702
703    boolean isPhone() {
704        return !isTablet && !isLargeTablet;
705    }
706    boolean isTablet() {
707        return isTablet;
708    }
709    boolean isLargeTablet() {
710        return isLargeTablet;
711    }
712
713    /**
714     * When {@code true}, hotseat is on the bottom row when in landscape mode.
715     * If {@code false}, hotseat is on the right column when in landscape mode.
716     */
717    boolean isVerticalBarLayout() {
718        return isLandscape && transposeLayoutWithOrientation;
719    }
720
721    boolean shouldFadeAdjacentWorkspaceScreens() {
722        return isVerticalBarLayout() || isLargeTablet();
723    }
724
725    int getVisibleChildCount(ViewGroup parent) {
726        int visibleChildren = 0;
727        for (int i = 0; i < parent.getChildCount(); i++) {
728            if (parent.getChildAt(i).getVisibility() != View.GONE) {
729                visibleChildren++;
730            }
731        }
732        return visibleChildren;
733    }
734
735    public void layout(Launcher launcher) {
736        FrameLayout.LayoutParams lp;
737        Resources res = launcher.getResources();
738        boolean hasVerticalBarLayout = isVerticalBarLayout();
739
740        // Layout the search bar space
741        View searchBar = launcher.getSearchBar();
742        lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams();
743        if (hasVerticalBarLayout) {
744            // Vertical search bar space
745            lp.gravity = Gravity.TOP | Gravity.LEFT;
746            lp.width = searchBarSpaceHeightPx;
747            lp.height = LayoutParams.WRAP_CONTENT;
748
749            LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar);
750            targets.setOrientation(LinearLayout.VERTICAL);
751        } else {
752            // Horizontal search bar space
753            lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
754            lp.width = searchBarSpaceWidthPx;
755            lp.height = searchBarSpaceHeightPx;
756        }
757        searchBar.setLayoutParams(lp);
758
759        // Layout the workspace
760        PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace);
761        lp = (FrameLayout.LayoutParams) workspace.getLayoutParams();
762        lp.gravity = Gravity.CENTER;
763        int orientation = isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT;
764        Rect padding = getWorkspacePadding(orientation);
765        workspace.setLayoutParams(lp);
766        workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom);
767        workspace.setPageSpacing(getWorkspacePageSpacing(orientation));
768
769        // Layout the hotseat
770        View hotseat = launcher.findViewById(R.id.hotseat);
771        lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams();
772        if (hasVerticalBarLayout) {
773            // Vertical hotseat
774            lp.gravity = Gravity.END;
775            lp.width = hotseatBarHeightPx;
776            lp.height = LayoutParams.MATCH_PARENT;
777            hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx);
778        } else if (isTablet()) {
779            // Pad the hotseat with the workspace padding calculated above
780            lp.gravity = Gravity.BOTTOM;
781            lp.width = LayoutParams.MATCH_PARENT;
782            lp.height = hotseatBarHeightPx;
783            hotseat.setPadding(edgeMarginPx + padding.left, 0,
784                    edgeMarginPx + padding.right,
785                    2 * edgeMarginPx);
786        } else {
787            // For phones, layout the hotseat without any bottom margin
788            // to ensure that we have space for the folders
789            lp.gravity = Gravity.BOTTOM;
790            lp.width = LayoutParams.MATCH_PARENT;
791            lp.height = hotseatBarHeightPx;
792            hotseat.findViewById(R.id.layout).setPadding(2 * edgeMarginPx, 0,
793                    2 * edgeMarginPx, 0);
794        }
795        hotseat.setLayoutParams(lp);
796
797        // Layout the page indicators
798        View pageIndicator = launcher.findViewById(R.id.page_indicator);
799        if (pageIndicator != null) {
800            if (hasVerticalBarLayout) {
801                // Hide the page indicators when we have vertical search/hotseat
802                pageIndicator.setVisibility(View.GONE);
803            } else {
804                // Put the page indicators above the hotseat
805                lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams();
806                lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
807                lp.width = LayoutParams.WRAP_CONTENT;
808                lp.height = LayoutParams.WRAP_CONTENT;
809                lp.bottomMargin = hotseatBarHeightPx;
810                pageIndicator.setLayoutParams(lp);
811            }
812        }
813
814        // Layout the Overview Mode
815        ViewGroup overviewMode = launcher.getOverviewPanel();
816        if (overviewMode != null) {
817            Rect r = getOverviewModeButtonBarRect();
818            lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams();
819            lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
820
821            int visibleChildCount = getVisibleChildCount(overviewMode);
822            int totalItemWidth = visibleChildCount * overviewModeBarItemWidthPx;
823            int maxWidth = totalItemWidth + (visibleChildCount-1) * overviewModeBarSpacerWidthPx;
824
825            lp.width = Math.min(availableWidthPx, maxWidth);
826            lp.height = r.height();
827            overviewMode.setLayoutParams(lp);
828
829            if (lp.width > totalItemWidth && visibleChildCount > 1) {
830                // We have enough space. Lets add some margin too.
831                int margin = (lp.width - totalItemWidth) / (visibleChildCount-1);
832                View lastChild = null;
833
834                // Set margin of all visible children except the last visible child
835                for (int i = 0; i < visibleChildCount; i++) {
836                    if (lastChild != null) {
837                        MarginLayoutParams clp = (MarginLayoutParams) lastChild.getLayoutParams();
838                        if (isLayoutRtl) {
839                            clp.leftMargin = margin;
840                        } else {
841                            clp.rightMargin = margin;
842                        }
843                        lastChild.setLayoutParams(clp);
844                        lastChild = null;
845                    }
846                    View thisChild = overviewMode.getChildAt(i);
847                    if (thisChild.getVisibility() != View.GONE) {
848                        lastChild = thisChild;
849                    }
850                }
851            }
852        }
853    }
854}
855