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