diff options
Diffstat (limited to 'src/com/android/launcher3/DynamicGrid.java')
-rw-r--r-- | src/com/android/launcher3/DynamicGrid.java | 478 |
1 files changed, 478 insertions, 0 deletions
diff --git a/src/com/android/launcher3/DynamicGrid.java b/src/com/android/launcher3/DynamicGrid.java new file mode 100644 index 000000000..37cccfbd3 --- /dev/null +++ b/src/com/android/launcher3/DynamicGrid.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Paint.FontMetrics; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + + +class DeviceProfileQuery { + float widthDps; + float heightDps; + float value; + PointF dimens; + + DeviceProfileQuery(float w, float h, float v) { + widthDps = w; + heightDps = h; + value = v; + dimens = new PointF(w, h); + } +} + +class DeviceProfile { + String name; + float minWidthDps; + float minHeightDps; + float numRows; + float numColumns; + float iconSize; + float iconTextSize; + float numHotseatIcons; + float hotseatIconSize; + + boolean isLandscape; + boolean isTablet; + boolean isLargeTablet; + boolean transposeLayoutWithOrientation; + + int edgeMarginPx; + + int widthPx; + int heightPx; + int iconSizePx; + int iconTextSizePx; + int cellWidthPx; + int cellHeightPx; + int folderBackgroundOffset; + int folderIconSizePx; + int folderCellWidthPx; + int folderCellHeightPx; + int hotseatCellWidthPx; + int hotseatCellHeightPx; + int hotseatIconSizePx; + int hotseatBarHeightPx; + int searchBarSpaceWidthPx; + int searchBarSpaceMaxWidthPx; + int searchBarSpaceHeightPx; + int searchBarHeightPx; + int pageIndicatorHeightPx; + + DeviceProfile(String n, float w, float h, float r, float c, + float is, float its, float hs, float his) { + name = n; + minWidthDps = w; + minHeightDps = h; + numRows = r; + numColumns = c; + iconSize = is; + iconTextSize = its; + numHotseatIcons = hs; + hotseatIconSize = his; + } + + DeviceProfile(ArrayList<DeviceProfile> profiles, + float minWidth, int minWidthPx, + float minHeight, int minHeightPx, + int wPx, int hPx, + Resources resources) { + DisplayMetrics dm = resources.getDisplayMetrics(); + ArrayList<DeviceProfileQuery> points = + new ArrayList<DeviceProfileQuery>(); + transposeLayoutWithOrientation = + resources.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); + updateFromConfiguration(resources, wPx, hPx); + minWidthDps = minWidth; + minHeightDps = minHeight; + + edgeMarginPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); + pageIndicatorHeightPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height); + + // Interpolate the rows + for (DeviceProfile p : profiles) { + points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numRows)); + } + numRows = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); + // Interpolate the columns + points.clear(); + for (DeviceProfile p : profiles) { + points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numColumns)); + } + numColumns = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); + // Interpolate the icon size + points.clear(); + for (DeviceProfile p : profiles) { + points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconSize)); + } + iconSize = invDistWeightedInterpolate(minWidth, minHeight, points); + iconSizePx = (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + iconSize, dm)); + // Interpolate the icon text size + points.clear(); + for (DeviceProfile p : profiles) { + points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconTextSize)); + } + iconTextSize = invDistWeightedInterpolate(minWidth, minHeight, points); + iconTextSizePx = (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + iconTextSize, dm)); + // Interpolate the hotseat size + points.clear(); + for (DeviceProfile p : profiles) { + points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numHotseatIcons)); + } + numHotseatIcons = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); + // Interpolate the hotseat icon size + points.clear(); + for (DeviceProfile p : profiles) { + points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.hotseatIconSize)); + } + + // Hotseat + hotseatIconSize = invDistWeightedInterpolate(minWidth, minHeight, points); + hotseatIconSizePx = (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + hotseatIconSize, dm)); + hotseatBarHeightPx = iconSizePx + 4 * edgeMarginPx; + hotseatCellWidthPx = iconSizePx; + hotseatCellHeightPx = iconSizePx; + + // Search Bar + searchBarSpaceMaxWidthPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width); + searchBarHeightPx = resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + searchBarSpaceWidthPx = Math.min(searchBarSpaceMaxWidthPx, widthPx); + searchBarSpaceHeightPx = searchBarHeightPx + 2 * edgeMarginPx; + + // Calculate the actual text height + Paint textPaint = new Paint(); + textPaint.setTextSize(iconTextSizePx); + FontMetrics fm = textPaint.getFontMetrics(); + cellWidthPx = iconSizePx; + cellHeightPx = iconSizePx + (int) Math.ceil(fm.bottom - fm.top); + + // Folder + folderCellWidthPx = cellWidthPx + 3 * edgeMarginPx; + folderCellHeightPx = cellHeightPx + edgeMarginPx; + folderBackgroundOffset = -edgeMarginPx; + folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset; + } + + void updateFromConfiguration(Resources resources, int wPx, int hPx) { + isLandscape = (resources.getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE); + isTablet = resources.getBoolean(R.bool.is_tablet); + isLargeTablet = resources.getBoolean(R.bool.is_large_tablet); + widthPx = wPx; + heightPx = hPx; + } + + private float dist(PointF p0, PointF p1) { + return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) + + (p1.y-p0.y)*(p1.y-p0.y)); + } + + private float weight(PointF a, PointF b, + float pow) { + float d = dist(a, b); + if (d == 0f) { + return Float.POSITIVE_INFINITY; + } + return (float) (1f / Math.pow(d, pow)); + } + + private float invDistWeightedInterpolate(float width, float height, + ArrayList<DeviceProfileQuery> points) { + float sum = 0; + float weights = 0; + float pow = 5; + float kNearestNeighbors = 3; + final PointF xy = new PointF(width, height); + + ArrayList<DeviceProfileQuery> pointsByNearness = points; + Collections.sort(pointsByNearness, new Comparator<DeviceProfileQuery>() { + public int compare(DeviceProfileQuery a, DeviceProfileQuery b) { + return (int) (dist(xy, a.dimens) - dist(xy, b.dimens)); + } + }); + + for (int i = 0; i < pointsByNearness.size(); ++i) { + DeviceProfileQuery p = pointsByNearness.get(i); + if (i < kNearestNeighbors) { + float w = weight(xy, p.dimens, pow); + if (w == Float.POSITIVE_INFINITY) { + return p.value; + } + weights += w; + } + } + + for (int i = 0; i < pointsByNearness.size(); ++i) { + DeviceProfileQuery p = pointsByNearness.get(i); + if (i < kNearestNeighbors) { + float w = weight(xy, p.dimens, pow); + sum += w * p.value / weights; + } + } + + return sum; + } + + Rect getWorkspacePadding(int orientation) { + Rect padding = new Rect(); + if (orientation == CellLayout.LANDSCAPE && + transposeLayoutWithOrientation) { + // Pad the left and right of the workspace with search/hotseat bar sizes + padding.set(searchBarSpaceHeightPx, edgeMarginPx, + hotseatBarHeightPx, edgeMarginPx); + } else { + if (isTablet()) { + // Pad the left and right of the workspace to ensure consistent spacing + // between all icons + int width = (orientation == CellLayout.LANDSCAPE) + ? Math.max(widthPx, heightPx) + : Math.min(widthPx, heightPx); + // XXX: If the icon size changes across orientations, we will have to take + // that into account here too. + int gap = (int) ((width - 2 * edgeMarginPx - + (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); + padding.set(edgeMarginPx + gap, + searchBarSpaceHeightPx, + edgeMarginPx + gap, + hotseatBarHeightPx + pageIndicatorHeightPx); + } else { + // Pad the top and bottom of the workspace with search/hotseat bar sizes + padding.set(edgeMarginPx, + searchBarSpaceHeightPx, + edgeMarginPx, + hotseatBarHeightPx + pageIndicatorHeightPx); + } + } + return padding; + } + + int calculateCellWidth(int width, int countX) { + return width / countX; + } + int calculateCellHeight(int height, int countY) { + return height / countY; + } + + boolean isTablet() { + return isTablet; + } + + boolean isLargeTablet() { + return isLargeTablet; + } + + public void layout(Launcher launcher) { + FrameLayout.LayoutParams lp; + Resources res = launcher.getResources(); + boolean hasVerticalBarLayout = isLandscape && + res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); + + // Layout the search bar space + View searchBarSpace = launcher.findViewById(R.id.qsb_bar); + lp = (FrameLayout.LayoutParams) searchBarSpace.getLayoutParams(); + if (hasVerticalBarLayout) { + // Vertical search bar + lp.gravity = Gravity.TOP | Gravity.LEFT; + lp.width = searchBarSpaceHeightPx; + lp.height = LayoutParams.MATCH_PARENT; + searchBarSpace.setPadding( + 0, 2 * edgeMarginPx, 0, + 2 * edgeMarginPx); + } else { + // Horizontal search bar + lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + lp.width = searchBarSpaceWidthPx; + lp.height = searchBarSpaceHeightPx; + searchBarSpace.setPadding( + 2 * edgeMarginPx, + 2 * edgeMarginPx, + 2 * edgeMarginPx, 0); + } + searchBarSpace.setLayoutParams(lp); + + // Layout the search bar + View searchBar = searchBarSpace.findViewById(R.id.qsb_search_bar); + lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); + lp.width = LayoutParams.MATCH_PARENT; + lp.height = LayoutParams.MATCH_PARENT; + searchBar.setLayoutParams(lp); + + // Layout the voice proxy + View voiceButtonProxy = launcher.findViewById(R.id.voice_button_proxy); + if (voiceButtonProxy != null) { + if (hasVerticalBarLayout) { + // TODO: MOVE THIS INTO SEARCH BAR MEASURE + } else { + lp = (FrameLayout.LayoutParams) voiceButtonProxy.getLayoutParams(); + lp.gravity = Gravity.TOP | Gravity.END; + lp.width = (widthPx - searchBarSpaceWidthPx) / 2 + + 2 * iconSizePx; + lp.height = searchBarSpaceHeightPx; + } + } + + // Layout the workspace + View workspace = launcher.findViewById(R.id.workspace); + lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); + lp.gravity = Gravity.CENTER; + Rect padding = getWorkspacePadding(isLandscape + ? CellLayout.LANDSCAPE + : CellLayout.PORTRAIT); + workspace.setPadding(padding.left, padding.top, + padding.right, padding.bottom); + workspace.setLayoutParams(lp); + + // Layout the hotseat + View hotseat = launcher.findViewById(R.id.hotseat); + lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams(); + if (hasVerticalBarLayout) { + // Vertical hotseat + lp.gravity = Gravity.RIGHT; + lp.width = hotseatBarHeightPx; + lp.height = LayoutParams.MATCH_PARENT; + hotseat.setPadding(0, 2 * edgeMarginPx, + 2 * edgeMarginPx, 2 * edgeMarginPx); + } else if (isTablet()) { + // Pad the hotseat with the grid gap calculated above + int gridGap = (int) ((widthPx - 2 * edgeMarginPx - + (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); + int gridWidth = (int) ((numColumns * cellWidthPx) + + ((numColumns - 1) * gridGap)); + int hotseatGap = (int) Math.max(0, + (gridWidth - (numHotseatIcons * hotseatCellWidthPx)) + / (numHotseatIcons - 1)); + lp.gravity = Gravity.BOTTOM; + lp.width = LayoutParams.MATCH_PARENT; + lp.height = hotseatBarHeightPx; + hotseat.setPadding(2 * edgeMarginPx + gridGap + hotseatGap, 0, + 2 * edgeMarginPx + gridGap + hotseatGap, + 2 * edgeMarginPx); + } else { + // For phones, layout the hotseat without any bottom margin + // to ensure that we have space for the folders + lp.gravity = Gravity.BOTTOM; + lp.width = LayoutParams.MATCH_PARENT; + lp.height = hotseatBarHeightPx; + hotseat.setPadding(2 * edgeMarginPx, 0, + 2 * edgeMarginPx, 0); + } + hotseat.setLayoutParams(lp); + + // Layout the page indicators + View pageIndicator = launcher.findViewById(R.id.page_indicator); + if (pageIndicator != null) { + if (hasVerticalBarLayout) { + // Hide the page indicators when we have vertical search/hotseat + pageIndicator.setVisibility(View.GONE); + } else { + // Put the page indicators above the hotseat + lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams(); + lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + lp.width = LayoutParams.WRAP_CONTENT; + lp.height = pageIndicatorHeightPx; + lp.bottomMargin = hotseatBarHeightPx; + pageIndicator.setLayoutParams(lp); + } + } + } +} + +public class DynamicGrid { + @SuppressWarnings("unused") + private static final String TAG = "DynamicGrid"; + + private DeviceProfile mProfile; + private float mMinWidth; + private float mMinHeight; + + public static int dpiFromPx(int size, DisplayMetrics metrics){ + float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; + return (int) Math.round(size / densityRatio); + } + + public DynamicGrid(Resources resources, int minWidthPx, int minHeightPx, + int widthPx, int heightPx) { + DisplayMetrics dm = resources.getDisplayMetrics(); + ArrayList<DeviceProfile> deviceProfiles = + new ArrayList<DeviceProfile>(); + // Our phone profiles include the bar sizes in each orientation + deviceProfiles.add(new DeviceProfile("Super Short Stubby", + 255, 300, 2, 3, 48, 12, 4, 48)); + deviceProfiles.add(new DeviceProfile("Shorter Stubby", + 255, 400, 3, 3, 48, 12, 4, 48)); + deviceProfiles.add(new DeviceProfile("Short Stubby", + 275, 420, 3, 4, 48, 12, 4, 48)); + deviceProfiles.add(new DeviceProfile("Stubby", + 255, 450, 3, 4, 48, 12, 4, 48)); + deviceProfiles.add(new DeviceProfile("Nexus S", + 296, 491.33f, 4, 4, 48, 12, 4, 48)); + deviceProfiles.add(new DeviceProfile("Nexus 4", + 359, 518, 4, 4, 60, 12, 5, 56)); + // The tablet profile is odd in that the landscape orientation + // also includes the nav bar on the side + deviceProfiles.add(new DeviceProfile("Nexus 7", + 575, 904, 6, 6, 72, 14.4f, 7, 60)); + // Larger tablet profiles always have system bars on the top & bottom + deviceProfiles.add(new DeviceProfile("Nexus 10", + 727, 1207, 5, 8, 80, 14.4f, 9, 64)); + /* + deviceProfiles.add(new DeviceProfile("Nexus 7", + 600, 960, 5, 5, 72, 14.4f, 5, 60)); + deviceProfiles.add(new DeviceProfile("Nexus 10", + 800, 1280, 5, 5, 80, 14.4f, 6, 64)); + */ + deviceProfiles.add(new DeviceProfile("20-inch Tablet", + 1527, 2527, 7, 7, 100, 20, 7, 72)); + mMinWidth = dpiFromPx(minWidthPx, dm); + mMinHeight = dpiFromPx(minHeightPx, dm); + mProfile = new DeviceProfile(deviceProfiles, + mMinWidth, minWidthPx, + mMinHeight, minHeightPx, + widthPx, heightPx, + resources); + } + + DeviceProfile getDeviceProfile() { + return mProfile; + } + + public String toString() { + return "-------- DYNAMIC GRID ------- \n" + + "Wd: " + mProfile.minWidthDps + ", Hd: " + mProfile.minHeightDps + + ", W: " + mProfile.widthPx + ", H: " + mProfile.heightPx + + " [r: " + mProfile.numRows + ", c: " + mProfile.numColumns + + ", is: " + mProfile.iconSizePx + ", its: " + mProfile.iconTextSize + + ", cw: " + mProfile.cellWidthPx + ", ch: " + mProfile.cellHeightPx + + ", hc: " + mProfile.numHotseatIcons + ", his: " + mProfile.hotseatIconSizePx + "]"; + } +} |