mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-19 10:48:19 +00:00
Start adding unit tests for the invariant device profile / Refactor
- removed redundant code to sort the device profiles - removed DeviceProfileQuery class - Added a helper method inside the test to easily generate interpolation graph looks like: https://docs.google.com/a/google.com/spreadsheets/d/1a1fdemrOqIDixiql77h0anWzUD3GlYfGsbP2FfIhyPM/edit?usp=sharing Change-Id: Ia4c54a8d59a049c418c08d1b766f07ac6e1d0944
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@Thunk float dist(float x0, float y0, float x1, float y1) {
|
||||
return (float) Math.hypot(x1 - x0, y1 - y0);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
weights += w;
|
||||
}
|
||||
InvariantDeviceProfile p = points.get(0);
|
||||
if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
|
||||
return p;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 sum;
|
||||
return out.multiply(1.0f/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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user