DeviceProfile.java revision ab946a19d41f245e27580281c3904e6c6636bcdd
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.Resources;
23import android.graphics.Paint;
24import android.graphics.Paint.FontMetrics;
25import android.graphics.Point;
26import android.graphics.Rect;
27import android.util.DisplayMetrics;
28import android.view.Gravity;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.ViewGroup.LayoutParams;
32import android.view.ViewGroup.MarginLayoutParams;
33import android.widget.FrameLayout;
34import android.widget.LinearLayout;
35
36public class DeviceProfile {
37
38    public final InvariantDeviceProfile inv;
39
40    // Device properties
41    public final boolean isTablet;
42    public final boolean isLargeTablet;
43    public final boolean isPhone;
44    public final boolean transposeLayoutWithOrientation;
45
46    // Device properties in current orientation
47    public final boolean isLandscape;
48    public final int widthPx;
49    public final int heightPx;
50    public final int availableWidthPx;
51    public final int availableHeightPx;
52
53    // Overview mode
54    private final int overviewModeMinIconZoneHeightPx;
55    private final int overviewModeMaxIconZoneHeightPx;
56    private final int overviewModeBarItemWidthPx;
57    private final int overviewModeBarSpacerWidthPx;
58    private final float overviewModeIconZoneRatio;
59
60    // Workspace
61    private int desiredWorkspaceLeftRightMarginPx;
62    public final int edgeMarginPx;
63    public final Rect defaultWidgetPadding;
64    private final int pageIndicatorHeightPx;
65    private final int defaultPageSpacingPx;
66    private float dragViewScale;
67
68    // Workspace icons
69    public int iconSizePx;
70    public int iconTextSizePx;
71    public int iconDrawablePaddingPx;
72    public int iconDrawablePaddingOriginalPx;
73
74    public int cellWidthPx;
75    public int cellHeightPx;
76
77    // Folder
78    public int folderBackgroundOffset;
79    public int folderIconSizePx;
80    public int folderCellWidthPx;
81    public int folderCellHeightPx;
82
83    // Hotseat
84    public int hotseatCellWidthPx;
85    public int hotseatCellHeightPx;
86    public int hotseatIconSizePx;
87    private int hotseatBarHeightPx;
88
89    // All apps
90    public int allAppsNumCols;
91    public int allAppsNumPredictiveCols;
92    public int allAppsButtonVisualSize;
93    public final int allAppsIconSizePx;
94    public final int allAppsIconTextSizePx;
95
96    // QSB
97    private int searchBarSpaceWidthPx;
98    private int searchBarSpaceHeightPx;
99
100    public DeviceProfile(Context context, InvariantDeviceProfile inv,
101            Point minSize, Point maxSize,
102            int width, int height, boolean isLandscape) {
103
104        this.inv = inv;
105        this.isLandscape = isLandscape;
106
107        Resources res = context.getResources();
108        DisplayMetrics dm = res.getDisplayMetrics();
109
110        // Constants from resources
111        isTablet = res.getBoolean(R.bool.is_tablet);
112        isLargeTablet = res.getBoolean(R.bool.is_large_tablet);
113        isPhone = !isTablet && !isLargeTablet;
114
115        // Some more constants
116        transposeLayoutWithOrientation =
117                res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
118
119        ComponentName cn = new ComponentName(context.getPackageName(),
120                this.getClass().getName());
121        defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
122        edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
123        desiredWorkspaceLeftRightMarginPx = 2 * edgeMarginPx;
124        pageIndicatorHeightPx =
125                res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height);
126        defaultPageSpacingPx =
127                res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing);
128        overviewModeMinIconZoneHeightPx =
129                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height);
130        overviewModeMaxIconZoneHeightPx =
131                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_max_icon_zone_height);
132        overviewModeBarItemWidthPx =
133                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_item_width);
134        overviewModeBarSpacerWidthPx =
135                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_spacer_width);
136        overviewModeIconZoneRatio =
137                res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f;
138        iconDrawablePaddingOriginalPx =
139                res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);
140
141        // AllApps uses the original non-scaled icon text size
142        allAppsIconTextSizePx = Utilities.pxFromDp(inv.iconTextSize, dm);
143
144        // AllApps uses the original non-scaled icon size
145        allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm);
146
147        // Determine sizes.
148        widthPx = width;
149        heightPx = height;
150        if (isLandscape) {
151            availableWidthPx = maxSize.x;
152            availableHeightPx = minSize.y;
153        } else {
154            availableWidthPx = minSize.x;
155            availableHeightPx = maxSize.y;
156        }
157
158        // Calculate the remaining vars
159        updateAvailableDimensions(dm, res);
160        computeAllAppsButtonSize(context);
161    }
162
163    /**
164     * Determine the exact visual footprint of the all apps button, taking into account scaling
165     * and internal padding of the drawable.
166     */
167    private void computeAllAppsButtonSize(Context context) {
168        Resources res = context.getResources();
169        float padding = res.getInteger(R.integer.config_allAppsButtonPaddingPercent) / 100f;
170        allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding));
171    }
172
173    private void updateAvailableDimensions(DisplayMetrics dm, Resources res) {
174        // Check to see if the icons fit in the new available height.  If not, then we need to
175        // shrink the icon size.
176        float scale = 1f;
177        int drawablePadding = iconDrawablePaddingOriginalPx;
178        updateIconSize(1f, drawablePadding, res, dm);
179        float usedHeight = (cellHeightPx * inv.numRows);
180
181        // We only care about the top and bottom workspace padding, which is not affected by RTL.
182        Rect workspacePadding = getWorkspacePadding(false /* isLayoutRtl */);
183        int maxHeight = (availableHeightPx - workspacePadding.top - workspacePadding.bottom);
184        if (usedHeight > maxHeight) {
185            scale = maxHeight / usedHeight;
186            drawablePadding = 0;
187        }
188        updateIconSize(scale, drawablePadding, res, dm);
189    }
190
191    private void updateIconSize(float scale, int drawablePadding, Resources res,
192                                DisplayMetrics dm) {
193        iconSizePx = (int) (Utilities.pxFromDp(inv.iconSize, dm) * scale);
194        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale);
195        iconDrawablePaddingPx = drawablePadding;
196        hotseatIconSizePx = (int) (Utilities.pxFromDp(inv.hotseatIconSize, dm) * scale);
197
198        // Search Bar
199        searchBarSpaceWidthPx = Math.min(widthPx,
200                res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
201        searchBarSpaceHeightPx = getSearchBarTopOffset()
202                + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
203
204        // Calculate the actual text height
205        Paint textPaint = new Paint();
206        textPaint.setTextSize(iconTextSizePx);
207        FontMetrics fm = textPaint.getFontMetrics();
208        cellWidthPx = iconSizePx;
209        cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top);
210        final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale);
211        dragViewScale = (iconSizePx + scaleDps) / iconSizePx;
212
213        // Hotseat
214        hotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx;
215        hotseatCellWidthPx = iconSizePx;
216        hotseatCellHeightPx = iconSizePx;
217
218        // Folder
219        folderCellWidthPx = cellWidthPx + 3 * edgeMarginPx;
220        folderCellHeightPx = cellHeightPx + edgeMarginPx;
221        folderBackgroundOffset = -edgeMarginPx;
222        folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset;
223    }
224
225    /**
226     * @param recyclerViewWidth the available width of the AllAppsRecyclerView
227     */
228    public void updateAppsViewNumCols(Resources res, int recyclerViewWidth) {
229        int appsViewLeftMarginPx =
230                res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
231        int allAppsCellWidthGap =
232                res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap);
233        int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx;
234        int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
235                (allAppsIconSizePx + allAppsCellWidthGap);
236        int numPredictiveAppCols = Math.max(inv.minAllAppsPredictionColumns, numAppsCols);
237        allAppsNumCols = numAppsCols;
238        allAppsNumPredictiveCols = numPredictiveAppCols;
239    }
240
241    /** Returns the search bar top offset */
242    private int getSearchBarTopOffset() {
243        if (isTablet && !isVerticalBarLayout()) {
244            return 4 * edgeMarginPx;
245        } else {
246            return 2 * edgeMarginPx;
247        }
248    }
249
250    /** Returns the search bar bounds in the current orientation */
251    public Rect getSearchBarBounds(boolean isLayoutRtl) {
252        Rect bounds = new Rect();
253        if (isLandscape && transposeLayoutWithOrientation) {
254            if (isLayoutRtl) {
255                bounds.set(availableWidthPx - searchBarSpaceHeightPx, edgeMarginPx,
256                        availableWidthPx, availableHeightPx - edgeMarginPx);
257            } else {
258                bounds.set(0, edgeMarginPx, searchBarSpaceHeightPx,
259                        availableHeightPx - edgeMarginPx);
260            }
261        } else {
262            if (isTablet) {
263                // Pad the left and right of the workspace to ensure consistent spacing
264                // between all icons
265                int width = getCurrentWidth();
266                // XXX: If the icon size changes across orientations, we will have to take
267                //      that into account here too.
268                int gap = (int) ((width - 2 * edgeMarginPx -
269                        (inv.numColumns * cellWidthPx)) / (2 * (inv.numColumns + 1)));
270                bounds.set(edgeMarginPx + gap, getSearchBarTopOffset(),
271                        availableWidthPx - (edgeMarginPx + gap),
272                        searchBarSpaceHeightPx);
273            } else {
274                bounds.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left,
275                        getSearchBarTopOffset(),
276                        availableWidthPx - (desiredWorkspaceLeftRightMarginPx -
277                        defaultWidgetPadding.right), searchBarSpaceHeightPx);
278            }
279        }
280        return bounds;
281    }
282
283    public Point getCellSize() {
284        Point result = new Point();
285        // Since we are only concerned with the overall padding, layout direction does
286        // not matter.
287        Rect padding = getWorkspacePadding(false /* isLayoutRtl */ );
288        result.x = calculateCellWidth(availableWidthPx - padding.left - padding.right,
289                inv.numColumns);
290        result.y = calculateCellHeight(availableHeightPx - padding.top - padding.bottom,
291                inv.numRows);
292        return result;
293    }
294
295    /** Returns the workspace padding in the specified orientation */
296    Rect getWorkspacePadding(boolean isLayoutRtl) {
297        Rect searchBarBounds = getSearchBarBounds(isLayoutRtl);
298        Rect padding = new Rect();
299        if (isLandscape && transposeLayoutWithOrientation) {
300            // Pad the left and right of the workspace with search/hotseat bar sizes
301            if (isLayoutRtl) {
302                padding.set(hotseatBarHeightPx, edgeMarginPx,
303                        searchBarBounds.width(), edgeMarginPx);
304            } else {
305                padding.set(searchBarBounds.width(), edgeMarginPx,
306                        hotseatBarHeightPx, edgeMarginPx);
307            }
308        } else {
309            if (isTablet) {
310                // Pad the left and right of the workspace to ensure consistent spacing
311                // between all icons
312                float gapScale = 1f + (dragViewScale - 1f) / 2f;
313                int width = getCurrentWidth();
314                int height = getCurrentHeight();
315                int paddingTop = searchBarBounds.bottom;
316                int paddingBottom = hotseatBarHeightPx + pageIndicatorHeightPx;
317                int availableWidth = Math.max(0, width - (int) ((inv.numColumns * cellWidthPx) +
318                        (inv.numColumns * gapScale * cellWidthPx)));
319                int availableHeight = Math.max(0, height - paddingTop - paddingBottom
320                        - (int) (2 * inv.numRows * cellHeightPx));
321                padding.set(availableWidth / 2, paddingTop + availableHeight / 2,
322                        availableWidth / 2, paddingBottom + availableHeight / 2);
323            } else {
324                // Pad the top and bottom of the workspace with search/hotseat bar sizes
325                padding.set(desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.left,
326                        searchBarBounds.bottom,
327                        desiredWorkspaceLeftRightMarginPx - defaultWidgetPadding.right,
328                        hotseatBarHeightPx + pageIndicatorHeightPx);
329            }
330        }
331        return padding;
332    }
333
334    private int getWorkspacePageSpacing(boolean isLayoutRtl) {
335        if ((isLandscape && transposeLayoutWithOrientation) || isLargeTablet) {
336            // In landscape mode the page spacing is set to the default.
337            return defaultPageSpacingPx;
338        } else {
339            // In portrait, we want the pages spaced such that there is no
340            // overhang of the previous / next page into the current page viewport.
341            // We assume symmetrical padding in portrait mode.
342            return Math.max(defaultPageSpacingPx, 2 * getWorkspacePadding(isLayoutRtl).left);
343        }
344    }
345
346    int getOverviewModeButtonBarHeight() {
347        int zoneHeight = (int) (overviewModeIconZoneRatio * availableHeightPx);
348        zoneHeight = Math.min(overviewModeMaxIconZoneHeightPx,
349                Math.max(overviewModeMinIconZoneHeightPx, zoneHeight));
350        return zoneHeight;
351    }
352
353    // The rect returned will be extended to below the system ui that covers the workspace
354    public boolean isInHotseatRect(int x, int y) {
355        if (isVerticalBarLayout()) {
356            return (x >= (availableWidthPx - hotseatBarHeightPx))
357                    && (y >= 0) && (y <= availableHeightPx);
358        } else {
359            return (x >= 0) && (x <= availableWidthPx)
360                    && (y >= (availableHeightPx - hotseatBarHeightPx));
361        }
362    }
363
364    public static int calculateCellWidth(int width, int countX) {
365        return width / countX;
366    }
367    public static int calculateCellHeight(int height, int countY) {
368        return height / countY;
369    }
370
371    /**
372     * When {@code true}, hotseat is on the bottom row when in landscape mode.
373     * If {@code false}, hotseat is on the right column when in landscape mode.
374     */
375    public boolean isVerticalBarLayout() {
376        return isLandscape && transposeLayoutWithOrientation;
377    }
378
379    boolean shouldFadeAdjacentWorkspaceScreens() {
380        return isVerticalBarLayout() || isLargeTablet;
381    }
382
383    private int getVisibleChildCount(ViewGroup parent) {
384        int visibleChildren = 0;
385        for (int i = 0; i < parent.getChildCount(); i++) {
386            if (parent.getChildAt(i).getVisibility() != View.GONE) {
387                visibleChildren++;
388            }
389        }
390        return visibleChildren;
391    }
392
393    public void layout(Launcher launcher) {
394        FrameLayout.LayoutParams lp;
395        boolean hasVerticalBarLayout = isVerticalBarLayout();
396        final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources());
397
398        // Layout the search bar space
399        View searchBar = launcher.getSearchDropTargetBar();
400        lp = getDropTargetBarLayoutParams(hasVerticalBarLayout, searchBar, Gravity.TOP);
401        searchBar.setLayoutParams(lp);
402
403        // Layout the app info bar space
404        View appInfoBar = launcher.getAppInfoDropTargetBar();
405        lp = getDropTargetBarLayoutParams(hasVerticalBarLayout, appInfoBar, Gravity.BOTTOM);
406        lp.bottomMargin = hotseatBarHeightPx;
407        appInfoBar.setLayoutParams(lp);
408
409        // Layout the workspace
410        PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace);
411        lp = (FrameLayout.LayoutParams) workspace.getLayoutParams();
412        lp.gravity = Gravity.CENTER;
413        Rect padding = getWorkspacePadding(isLayoutRtl);
414        workspace.setLayoutParams(lp);
415        workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom);
416        workspace.setPageSpacing(getWorkspacePageSpacing(isLayoutRtl));
417
418        // Layout the hotseat
419        View hotseat = launcher.findViewById(R.id.hotseat);
420        lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams();
421        if (hasVerticalBarLayout) {
422            // Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the
423            //                     screen regardless of RTL
424            lp.gravity = Gravity.RIGHT;
425            lp.width = hotseatBarHeightPx;
426            lp.height = LayoutParams.MATCH_PARENT;
427            hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx);
428        } else if (isTablet) {
429            // Pad the hotseat with the workspace padding calculated above
430            lp.gravity = Gravity.BOTTOM;
431            lp.width = LayoutParams.MATCH_PARENT;
432            lp.height = hotseatBarHeightPx;
433            hotseat.setPadding(edgeMarginPx + padding.left, 0,
434                    edgeMarginPx + padding.right,
435                    2 * edgeMarginPx);
436        } else {
437            // For phones, layout the hotseat without any bottom margin
438            // to ensure that we have space for the folders
439            lp.gravity = Gravity.BOTTOM;
440            lp.width = LayoutParams.MATCH_PARENT;
441            lp.height = hotseatBarHeightPx;
442            hotseat.findViewById(R.id.layout).setPadding(2 * edgeMarginPx, 0,
443                    2 * edgeMarginPx, 0);
444        }
445        hotseat.setLayoutParams(lp);
446
447        // Layout the page indicators
448        View pageIndicator = launcher.findViewById(R.id.page_indicator);
449        if (pageIndicator != null) {
450            if (hasVerticalBarLayout) {
451                // Hide the page indicators when we have vertical search/hotseat
452                pageIndicator.setVisibility(View.GONE);
453            } else {
454                // Put the page indicators above the hotseat
455                lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams();
456                lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
457                lp.width = LayoutParams.WRAP_CONTENT;
458                lp.height = LayoutParams.WRAP_CONTENT;
459                lp.bottomMargin = hotseatBarHeightPx;
460                pageIndicator.setLayoutParams(lp);
461            }
462        }
463
464        // Layout the Overview Mode
465        ViewGroup overviewMode = launcher.getOverviewPanel();
466        if (overviewMode != null) {
467            lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams();
468            lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
469
470            int visibleChildCount = getVisibleChildCount(overviewMode);
471            int totalItemWidth = visibleChildCount * overviewModeBarItemWidthPx;
472            int maxWidth = totalItemWidth + (visibleChildCount-1) * overviewModeBarSpacerWidthPx;
473
474            lp.width = Math.min(availableWidthPx, maxWidth);
475            lp.height = getOverviewModeButtonBarHeight();
476            overviewMode.setLayoutParams(lp);
477
478            if (lp.width > totalItemWidth && visibleChildCount > 1) {
479                // We have enough space. Lets add some margin too.
480                int margin = (lp.width - totalItemWidth) / (visibleChildCount-1);
481                View lastChild = null;
482
483                // Set margin of all visible children except the last visible child
484                for (int i = 0; i < visibleChildCount; i++) {
485                    if (lastChild != null) {
486                        MarginLayoutParams clp = (MarginLayoutParams) lastChild.getLayoutParams();
487                        if (isLayoutRtl) {
488                            clp.leftMargin = margin;
489                        } else {
490                            clp.rightMargin = margin;
491                        }
492                        lastChild.setLayoutParams(clp);
493                        lastChild = null;
494                    }
495                    View thisChild = overviewMode.getChildAt(i);
496                    if (thisChild.getVisibility() != View.GONE) {
497                        lastChild = thisChild;
498                    }
499                }
500            }
501        }
502    }
503
504    private FrameLayout.LayoutParams getDropTargetBarLayoutParams(boolean hasVerticalBarLayout,
505            View dropTargetBar, int verticalGravity) {
506        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) dropTargetBar.getLayoutParams();
507        if (hasVerticalBarLayout) {
508            // Vertical drop target bar space -- The drop target bar is fixed in the layout to be on
509            //                                   the left of the screen regardless of RTL
510            lp.gravity = Gravity.LEFT;
511            lp.width = searchBarSpaceHeightPx;
512
513            LinearLayout targets = (LinearLayout) dropTargetBar.findViewById(R.id.drag_target_bar);
514            targets.setOrientation(LinearLayout.VERTICAL);
515            FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams();
516            targetsLp.gravity = verticalGravity;
517            targetsLp.height = LayoutParams.WRAP_CONTENT;
518        } else {
519            // Horizontal drop target bar space
520            lp.gravity = verticalGravity;
521            lp.height = searchBarSpaceHeightPx;
522
523            LinearLayout targets = (LinearLayout) dropTargetBar.findViewById(R.id.drag_target_bar);
524            targets.getLayoutParams().width = searchBarSpaceWidthPx;
525        }
526        return lp;
527    }
528
529    private int getCurrentWidth() {
530        return isLandscape
531                ? Math.max(widthPx, heightPx)
532                : Math.min(widthPx, heightPx);
533    }
534
535    private int getCurrentHeight() {
536        return isLandscape
537                ? Math.min(widthPx, heightPx)
538                : Math.max(widthPx, heightPx);
539    }
540}
541