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