Loading src/com/android/launcher3/InvariantDeviceProfile.java +119 −129 Original line number Diff line number Diff line Loading @@ -19,12 +19,13 @@ package com.android.launcher3; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Point; import android.graphics.PointF; import android.os.Build; import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; Loading @@ -34,61 +35,36 @@ public class InvariantDeviceProfile { // This is a static that we use for the default icon size on a 4/5-inch phone private static float DEFAULT_ICON_SIZE_DP = 60; private static final ArrayList<InvariantDeviceProfile> sDeviceProfiles = new ArrayList<>(); static { sDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", 255, 300, 2, 3, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", 255, 400, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", 275, 420, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Stubby", 255, 450, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", 296, 491.33f, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", 335, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", 359, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", 406, 694, 5, 5, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", 575, 904, 5, 6, 4, 5, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", 727, 1207, 5, 6, 4, 5, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); sDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", 1527, 2527, 7, 7, 6, 6, 100, 20, 7, 72, R.xml.default_workspace_4x4)); } // Constants that affects the interpolation curve between statically defined device profile // buckets. private static float KNEARESTNEIGHBOR = 3; private static float WEIGHT_POWER = 5; private class DeviceProfileQuery { InvariantDeviceProfile profile; float widthDps; float heightDps; float value; PointF dimens; DeviceProfileQuery(InvariantDeviceProfile p, float v) { widthDps = p.minWidthDps; heightDps = p.minHeightDps; value = v; dimens = new PointF(widthDps, heightDps); profile = p; } } // used to offset float not being able to express extremely small weights in extreme cases. private static float WEIGHT_EFFICIENT = 100000f; // Profile-defining invariant properties String name; float minWidthDps; float minHeightDps; /** * Number of icons per row and column in the workspace. */ public int numRows; public int numColumns; /** * Number of icons per row and column in the folder. */ public int numFolderRows; public int numFolderColumns; float iconSize; float iconTextSize; /** * Number of icons inside the hotseat area. */ float numHotseatIcons; float hotseatIconSize; int defaultLayoutId; Loading @@ -102,6 +78,12 @@ public class InvariantDeviceProfile { InvariantDeviceProfile() { } public InvariantDeviceProfile(InvariantDeviceProfile p) { this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, p.numFolderRows, p.numFolderColumns, p.iconSize, p.iconTextSize, p.numHotseatIcons, p.hotseatIconSize, p.defaultLayoutId); } InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, float is, float its, float hs, float his, int dlId) { // Ensure that we have an odd number of hotseat items (since we need to place all apps) Loading Loading @@ -134,21 +116,16 @@ public class InvariantDeviceProfile { Point largestSize = new Point(); display.getCurrentSizeRange(smallestSize, largestSize); // This guarantees that width < height minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); ArrayList<DeviceProfileQuery> points = new ArrayList<DeviceProfileQuery>(); ArrayList<InvariantDeviceProfile> closestProfiles = findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles()); InvariantDeviceProfile interpolatedDeviceProfileOut = invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); // Find the closes profile given the width/height for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, 0f)); } InvariantDeviceProfile closestProfile = findClosestDeviceProfile(minWidthDps, minHeightDps, points); // The following properties are inherited directly from the nearest archetypal profile InvariantDeviceProfile closestProfile = closestProfiles.get(0); numRows = closestProfile.numRows; numColumns = closestProfile.numColumns; numHotseatIcons = closestProfile.numHotseatIcons; Loading @@ -157,24 +134,9 @@ public class InvariantDeviceProfile { numFolderRows = closestProfile.numFolderRows; numFolderColumns = closestProfile.numFolderColumns; // The following properties are interpolated based on proximity to nearby archetypal // profiles points.clear(); for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, p.iconSize)); } iconSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points); points.clear(); for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, p.iconTextSize)); } iconTextSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points); points.clear(); for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, p.hotseatIconSize)); } hotseatIconSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points); iconSize = interpolatedDeviceProfileOut.iconSize; iconTextSize = interpolatedDeviceProfileOut.iconTextSize; hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize; // If the partner customization apk contains any grid overrides, apply them // Supported overrides: numRows, numColumns, iconSize Loading @@ -182,7 +144,7 @@ public class InvariantDeviceProfile { Point realSize = new Point(); display.getRealSize(realSize); // The real size never changes. smallSide and largeSize will remain the // The real size never changes. smallSide and largeSide will remain the // same in any orientation. int smallSide = Math.min(realSize.x, realSize.y); int largeSide = Math.max(realSize.x, realSize.y); Loading @@ -193,84 +155,112 @@ public class InvariantDeviceProfile { smallSide, largeSide, false /* isLandscape */); } ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() { ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>(); // width, height, #rows, #columns, #folder rows, #folder columns, // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId. predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", 255, 300, 2, 3, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", 255, 400, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", 275, 420, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby", 255, 450, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", 296, 491.33f, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", 335, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", 359, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", 406, 694, 5, 5, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", 575, 904, 5, 6, 4, 5, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", 727, 1207, 5, 6, 4, 5, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", 1527, 2527, 7, 7, 6, 6, 100, 20, 7, 72, R.xml.default_workspace_4x4)); return predefinedDeviceProfiles; } /** * Apply any Partner customization grid overrides. * * Currently we support: all apps row / column count. */ private void applyPartnerDeviceProfileOverrides(Context ctx, DisplayMetrics dm) { Partner p = Partner.get(ctx.getPackageManager()); private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { Partner p = Partner.get(context.getPackageManager()); if (p != null) { p.applyInvariantDeviceProfileOverrides(this, dm); } } @Thunk 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)); } /** Returns the closest device profile given the width and height and a list of profiles */ private InvariantDeviceProfile findClosestDeviceProfile(float width, float height, ArrayList<DeviceProfileQuery> points) { return findClosestDeviceProfiles(width, height, points).get(0).profile; @Thunk float dist(float x0, float y0, float x1, float y1) { return (float) Math.hypot(x1 - x0, y1 - y0); } /** Returns the closest device profiles ordered by closeness to the specified width and height */ private ArrayList<DeviceProfileQuery> findClosestDeviceProfiles(float width, float height, ArrayList<DeviceProfileQuery> points) { final PointF xy = new PointF(width, height); /** * Returns the closest device profiles ordered by closeness to the specified width and height */ // Package private visibility for testing. ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles( final float width, final float height, ArrayList<InvariantDeviceProfile> points) { // Sort the profiles by their closeness to the dimensions 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)); ArrayList<InvariantDeviceProfile> pointsByNearness = points; Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() { public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) { return (int) (dist(width, height, a.minWidthDps, a.minHeightDps) - dist(width, height, b.minWidthDps, b.minHeightDps)); } }); return pointsByNearness; } private float invDistWeightedInterpolate(float width, float height, ArrayList<DeviceProfileQuery> points) { float sum = 0; // Package private visibility for testing. InvariantDeviceProfile invDistWeightedInterpolate(float width, float height, ArrayList<InvariantDeviceProfile> points) { float weights = 0; float pow = 5; float kNearestNeighbors = 3; final PointF xy = new PointF(width, height); ArrayList<DeviceProfileQuery> pointsByNearness = findClosestDeviceProfiles(width, height, points); 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; InvariantDeviceProfile p = points.get(0); if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) { return p; } InvariantDeviceProfile out = new InvariantDeviceProfile(); for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { p = new InvariantDeviceProfile(points.get(i)); float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); weights += w; out.add(p.multiply(w)); } return out.multiply(1.0f/weights); } 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; private void add(InvariantDeviceProfile p) { iconSize += p.iconSize; iconTextSize += p.iconTextSize; hotseatIconSize += p.hotseatIconSize; } private InvariantDeviceProfile multiply(float w) { iconSize *= w; iconTextSize *= w; hotseatIconSize *= w; return this; } return sum; private float weight(float x0, float y0, float x1, float y1, float pow) { float d = dist(x0, y0, x1, y1); if (Float.compare(d, 0f) == 0) { return Float.POSITIVE_INFINITY; } return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); } } No newline at end of file tests/src/com/android/launcher3/InvariantDeviceProfileTest.java 0 → 100644 +123 −0 Original line number Diff line number Diff line /* * Copyright (C) 2015 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.graphics.PointF; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.util.FocusLogic; import java.util.ArrayList; /** * Tests the {@link DeviceProfile} and {@link InvariantDeviceProfile}. */ @SmallTest public class InvariantDeviceProfileTest extends AndroidTestCase { private static final String TAG = "DeviceProfileTest"; private static final boolean DEBUG = false; private InvariantDeviceProfile mInvariantProfile; private ArrayList<InvariantDeviceProfile> mPredefinedDeviceProfiles; @Override protected void setUp() throws Exception { super.setUp(); mInvariantProfile = new InvariantDeviceProfile(getContext()); mPredefinedDeviceProfiles = mInvariantProfile.getPredefinedDeviceProfiles(); } @Override protected void tearDown() throws Exception { // Nothing to tear down as this class only tests static methods. } public void testFindClosestDeviceProfile2() { for (InvariantDeviceProfile idf: mPredefinedDeviceProfiles) { ArrayList<InvariantDeviceProfile> closestProfiles = mInvariantProfile.findClosestDeviceProfiles( idf.minWidthDps, idf.minHeightDps, mPredefinedDeviceProfiles); assertTrue(closestProfiles.get(0).equals(idf)); } } /** * Used to print out how the invDistWeightedInterpolate works between device profiles to * tweak the two constants that control how the interpolation curve is shaped. */ public void testInvInterpolation() { InvariantDeviceProfile p1 = mPredefinedDeviceProfiles.get(7); // e.g., Large Phone InvariantDeviceProfile p2 = mPredefinedDeviceProfiles.get(8); // e.g., Nexus 7 ArrayList<PointF> pts = createInterpolatedPoints( new PointF(p1.minWidthDps, p1.minHeightDps), new PointF(p2.minWidthDps, p2.minHeightDps), 20f); for (int i = 0; i < pts.size(); i++) { ArrayList<InvariantDeviceProfile> closestProfiles = mInvariantProfile.findClosestDeviceProfiles( pts.get(i).x, pts.get(i).y, mPredefinedDeviceProfiles); InvariantDeviceProfile result = mInvariantProfile.invDistWeightedInterpolate( pts.get(i).x, pts.get(i).y, closestProfiles); if (DEBUG) { Log.d(TAG, String.format("width x height = (%f, %f)] iconSize = %f", pts.get(i).x, pts.get(i).y, result.iconSize)); } } } private ArrayList<PointF> createInterpolatedPoints(PointF a, PointF b, float numPts) { ArrayList<PointF> result = new ArrayList<PointF>(); result.add(a); for (float i = 1; i < numPts; i = i + 1.0f) { result.add(new PointF((b.x * i + a.x * (numPts - i)) / numPts, (b.y * i + a.y * (numPts - i)) / numPts)); } result.add(b); return result; } /** * Ensures that system calls (e.g., WindowManager, DisplayMetrics) that require contexts are * properly working to generate minimum width and height of the display. */ public void test_hammerhead() { if (!android.os.Build.DEVICE.equals("hammerhead")) { return; } assertEquals(mInvariantProfile.numRows, 4); assertEquals(mInvariantProfile.numColumns, 4); assertEquals((int) mInvariantProfile.numHotseatIcons, 5); DeviceProfile landscapeProfile = mInvariantProfile.landscapeProfile; DeviceProfile portraitProfile = mInvariantProfile.portraitProfile; assertEquals(portraitProfile.allAppsNumCols, 3); assertEquals(landscapeProfile.allAppsNumCols, 5); // not used } // Add more tests for other devices, however, running them once on a single device is enough // for verifying that for a platform version, the WindowManager and DisplayMetrics is // working as intended. } Loading
src/com/android/launcher3/InvariantDeviceProfile.java +119 −129 Original line number Diff line number Diff line Loading @@ -19,12 +19,13 @@ package com.android.launcher3; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Point; import android.graphics.PointF; import android.os.Build; import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; Loading @@ -34,61 +35,36 @@ public class InvariantDeviceProfile { // This is a static that we use for the default icon size on a 4/5-inch phone private static float DEFAULT_ICON_SIZE_DP = 60; private static final ArrayList<InvariantDeviceProfile> sDeviceProfiles = new ArrayList<>(); static { sDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", 255, 300, 2, 3, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", 255, 400, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", 275, 420, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Stubby", 255, 450, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", 296, 491.33f, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", 335, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", 359, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); sDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", 406, 694, 5, 5, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", 575, 904, 5, 6, 4, 5, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", 727, 1207, 5, 6, 4, 5, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); sDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", 1527, 2527, 7, 7, 6, 6, 100, 20, 7, 72, R.xml.default_workspace_4x4)); } // Constants that affects the interpolation curve between statically defined device profile // buckets. private static float KNEARESTNEIGHBOR = 3; private static float WEIGHT_POWER = 5; private class DeviceProfileQuery { InvariantDeviceProfile profile; float widthDps; float heightDps; float value; PointF dimens; DeviceProfileQuery(InvariantDeviceProfile p, float v) { widthDps = p.minWidthDps; heightDps = p.minHeightDps; value = v; dimens = new PointF(widthDps, heightDps); profile = p; } } // used to offset float not being able to express extremely small weights in extreme cases. private static float WEIGHT_EFFICIENT = 100000f; // Profile-defining invariant properties String name; float minWidthDps; float minHeightDps; /** * Number of icons per row and column in the workspace. */ public int numRows; public int numColumns; /** * Number of icons per row and column in the folder. */ public int numFolderRows; public int numFolderColumns; float iconSize; float iconTextSize; /** * Number of icons inside the hotseat area. */ float numHotseatIcons; float hotseatIconSize; int defaultLayoutId; Loading @@ -102,6 +78,12 @@ public class InvariantDeviceProfile { InvariantDeviceProfile() { } public InvariantDeviceProfile(InvariantDeviceProfile p) { this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, p.numFolderRows, p.numFolderColumns, p.iconSize, p.iconTextSize, p.numHotseatIcons, p.hotseatIconSize, p.defaultLayoutId); } InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, float is, float its, float hs, float his, int dlId) { // Ensure that we have an odd number of hotseat items (since we need to place all apps) Loading Loading @@ -134,21 +116,16 @@ public class InvariantDeviceProfile { Point largestSize = new Point(); display.getCurrentSizeRange(smallestSize, largestSize); // This guarantees that width < height minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); ArrayList<DeviceProfileQuery> points = new ArrayList<DeviceProfileQuery>(); ArrayList<InvariantDeviceProfile> closestProfiles = findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles()); InvariantDeviceProfile interpolatedDeviceProfileOut = invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); // Find the closes profile given the width/height for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, 0f)); } InvariantDeviceProfile closestProfile = findClosestDeviceProfile(minWidthDps, minHeightDps, points); // The following properties are inherited directly from the nearest archetypal profile InvariantDeviceProfile closestProfile = closestProfiles.get(0); numRows = closestProfile.numRows; numColumns = closestProfile.numColumns; numHotseatIcons = closestProfile.numHotseatIcons; Loading @@ -157,24 +134,9 @@ public class InvariantDeviceProfile { numFolderRows = closestProfile.numFolderRows; numFolderColumns = closestProfile.numFolderColumns; // The following properties are interpolated based on proximity to nearby archetypal // profiles points.clear(); for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, p.iconSize)); } iconSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points); points.clear(); for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, p.iconTextSize)); } iconTextSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points); points.clear(); for (InvariantDeviceProfile p : sDeviceProfiles) { points.add(new DeviceProfileQuery(p, p.hotseatIconSize)); } hotseatIconSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points); iconSize = interpolatedDeviceProfileOut.iconSize; iconTextSize = interpolatedDeviceProfileOut.iconTextSize; hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize; // If the partner customization apk contains any grid overrides, apply them // Supported overrides: numRows, numColumns, iconSize Loading @@ -182,7 +144,7 @@ public class InvariantDeviceProfile { Point realSize = new Point(); display.getRealSize(realSize); // The real size never changes. smallSide and largeSize will remain the // The real size never changes. smallSide and largeSide will remain the // same in any orientation. int smallSide = Math.min(realSize.x, realSize.y); int largeSide = Math.max(realSize.x, realSize.y); Loading @@ -193,84 +155,112 @@ public class InvariantDeviceProfile { smallSide, largeSide, false /* isLandscape */); } ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() { ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>(); // width, height, #rows, #columns, #folder rows, #folder columns, // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId. predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", 255, 300, 2, 3, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", 255, 400, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", 275, 420, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby", 255, 450, 3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", 296, 491.33f, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", 335, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", 359, 567, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", 406, 694, 5, 5, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", 575, 904, 5, 6, 4, 5, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); // Larger tablet profiles always have system bars on the top & bottom predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", 727, 1207, 5, 6, 4, 5, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", 1527, 2527, 7, 7, 6, 6, 100, 20, 7, 72, R.xml.default_workspace_4x4)); return predefinedDeviceProfiles; } /** * Apply any Partner customization grid overrides. * * Currently we support: all apps row / column count. */ private void applyPartnerDeviceProfileOverrides(Context ctx, DisplayMetrics dm) { Partner p = Partner.get(ctx.getPackageManager()); private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { Partner p = Partner.get(context.getPackageManager()); if (p != null) { p.applyInvariantDeviceProfileOverrides(this, dm); } } @Thunk 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)); } /** Returns the closest device profile given the width and height and a list of profiles */ private InvariantDeviceProfile findClosestDeviceProfile(float width, float height, ArrayList<DeviceProfileQuery> points) { return findClosestDeviceProfiles(width, height, points).get(0).profile; @Thunk float dist(float x0, float y0, float x1, float y1) { return (float) Math.hypot(x1 - x0, y1 - y0); } /** Returns the closest device profiles ordered by closeness to the specified width and height */ private ArrayList<DeviceProfileQuery> findClosestDeviceProfiles(float width, float height, ArrayList<DeviceProfileQuery> points) { final PointF xy = new PointF(width, height); /** * Returns the closest device profiles ordered by closeness to the specified width and height */ // Package private visibility for testing. ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles( final float width, final float height, ArrayList<InvariantDeviceProfile> points) { // Sort the profiles by their closeness to the dimensions 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)); ArrayList<InvariantDeviceProfile> pointsByNearness = points; Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() { public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) { return (int) (dist(width, height, a.minWidthDps, a.minHeightDps) - dist(width, height, b.minWidthDps, b.minHeightDps)); } }); return pointsByNearness; } private float invDistWeightedInterpolate(float width, float height, ArrayList<DeviceProfileQuery> points) { float sum = 0; // Package private visibility for testing. InvariantDeviceProfile invDistWeightedInterpolate(float width, float height, ArrayList<InvariantDeviceProfile> points) { float weights = 0; float pow = 5; float kNearestNeighbors = 3; final PointF xy = new PointF(width, height); ArrayList<DeviceProfileQuery> pointsByNearness = findClosestDeviceProfiles(width, height, points); 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; InvariantDeviceProfile p = points.get(0); if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) { return p; } InvariantDeviceProfile out = new InvariantDeviceProfile(); for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { p = new InvariantDeviceProfile(points.get(i)); float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); weights += w; out.add(p.multiply(w)); } return out.multiply(1.0f/weights); } 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; private void add(InvariantDeviceProfile p) { iconSize += p.iconSize; iconTextSize += p.iconTextSize; hotseatIconSize += p.hotseatIconSize; } private InvariantDeviceProfile multiply(float w) { iconSize *= w; iconTextSize *= w; hotseatIconSize *= w; return this; } return sum; private float weight(float x0, float y0, float x1, float y1, float pow) { float d = dist(x0, y0, x1, y1); if (Float.compare(d, 0f) == 0) { return Float.POSITIVE_INFINITY; } return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); } } No newline at end of file
tests/src/com/android/launcher3/InvariantDeviceProfileTest.java 0 → 100644 +123 −0 Original line number Diff line number Diff line /* * Copyright (C) 2015 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.graphics.PointF; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.util.FocusLogic; import java.util.ArrayList; /** * Tests the {@link DeviceProfile} and {@link InvariantDeviceProfile}. */ @SmallTest public class InvariantDeviceProfileTest extends AndroidTestCase { private static final String TAG = "DeviceProfileTest"; private static final boolean DEBUG = false; private InvariantDeviceProfile mInvariantProfile; private ArrayList<InvariantDeviceProfile> mPredefinedDeviceProfiles; @Override protected void setUp() throws Exception { super.setUp(); mInvariantProfile = new InvariantDeviceProfile(getContext()); mPredefinedDeviceProfiles = mInvariantProfile.getPredefinedDeviceProfiles(); } @Override protected void tearDown() throws Exception { // Nothing to tear down as this class only tests static methods. } public void testFindClosestDeviceProfile2() { for (InvariantDeviceProfile idf: mPredefinedDeviceProfiles) { ArrayList<InvariantDeviceProfile> closestProfiles = mInvariantProfile.findClosestDeviceProfiles( idf.minWidthDps, idf.minHeightDps, mPredefinedDeviceProfiles); assertTrue(closestProfiles.get(0).equals(idf)); } } /** * Used to print out how the invDistWeightedInterpolate works between device profiles to * tweak the two constants that control how the interpolation curve is shaped. */ public void testInvInterpolation() { InvariantDeviceProfile p1 = mPredefinedDeviceProfiles.get(7); // e.g., Large Phone InvariantDeviceProfile p2 = mPredefinedDeviceProfiles.get(8); // e.g., Nexus 7 ArrayList<PointF> pts = createInterpolatedPoints( new PointF(p1.minWidthDps, p1.minHeightDps), new PointF(p2.minWidthDps, p2.minHeightDps), 20f); for (int i = 0; i < pts.size(); i++) { ArrayList<InvariantDeviceProfile> closestProfiles = mInvariantProfile.findClosestDeviceProfiles( pts.get(i).x, pts.get(i).y, mPredefinedDeviceProfiles); InvariantDeviceProfile result = mInvariantProfile.invDistWeightedInterpolate( pts.get(i).x, pts.get(i).y, closestProfiles); if (DEBUG) { Log.d(TAG, String.format("width x height = (%f, %f)] iconSize = %f", pts.get(i).x, pts.get(i).y, result.iconSize)); } } } private ArrayList<PointF> createInterpolatedPoints(PointF a, PointF b, float numPts) { ArrayList<PointF> result = new ArrayList<PointF>(); result.add(a); for (float i = 1; i < numPts; i = i + 1.0f) { result.add(new PointF((b.x * i + a.x * (numPts - i)) / numPts, (b.y * i + a.y * (numPts - i)) / numPts)); } result.add(b); return result; } /** * Ensures that system calls (e.g., WindowManager, DisplayMetrics) that require contexts are * properly working to generate minimum width and height of the display. */ public void test_hammerhead() { if (!android.os.Build.DEVICE.equals("hammerhead")) { return; } assertEquals(mInvariantProfile.numRows, 4); assertEquals(mInvariantProfile.numColumns, 4); assertEquals((int) mInvariantProfile.numHotseatIcons, 5); DeviceProfile landscapeProfile = mInvariantProfile.landscapeProfile; DeviceProfile portraitProfile = mInvariantProfile.portraitProfile; assertEquals(portraitProfile.allAppsNumCols, 3); assertEquals(landscapeProfile.allAppsNumCols, 5); // not used } // Add more tests for other devices, however, running them once on a single device is enough // for verifying that for a platform version, the WindowManager and DisplayMetrics is // working as intended. }