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