/* * 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 profiles, float minWidth, int minWidthPx, float minHeight, int minHeightPx, int wPx, int hPx, Resources resources) { DisplayMetrics dm = resources.getDisplayMetrics(); ArrayList points = new ArrayList(); 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 points) { float sum = 0; float weights = 0; float pow = 5; float kNearestNeighbors = 3; final PointF xy = new PointF(width, height); ArrayList pointsByNearness = points; Collections.sort(pointsByNearness, new Comparator() { 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 deviceProfiles = new ArrayList(); // 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 + "]"; } }