Android M Launcher3屏幕适配
前言
我们知道Launcher3在不同的设备上,都能够很好的适配屏幕,包括桌面图标大小、字体大小、桌面及应用列表的行列数等。那么它是怎么做到的呢?
LauncherAppState初始化
要想知道Launcher3是如何做到屏幕适配的,我们首先从Launcher的初始化开始分析。我们知道Launcher的初始化是从Launcher.java的onCreate方法开始的,我们先看下onCreate最先做了什么事情。
super.onCreate(savedInstanceState);
LauncherAppState.setApplicationContext(getApplicationContext());
LauncherAppState app = LauncherAppState.getInstance();
...
我们看到Launcher在onCreate方法开始首先初始化LauncherAppState,这一初始化过程主要做了两件事情,1)设置应用上下文;2)获取LauncherAppState的单例对象。在获取取LauncherAppState的单例对象过程中,如果LauncherAppState的单例对象不存在,则会初始化一个。在LauncherAppState的构造方法中有一系列的初始化。
private LauncherAppState() {
if (sContext == null) {
throw new IllegalStateException("LauncherAppState inited before app context set");
}
Log.v(Launcher.TAG, "LauncherAppState inited");
if (sContext.getResources().getBoolean(R.bool.debug_memory_enabled)) {
MemoryTracker.startTrackingMe(sContext, "L");
}
mInvariantDeviceProfile = new InvariantDeviceProfile(sContext);
mIconCache = new IconCache(sContext, mInvariantDeviceProfile);
mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache);
mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
mBuildInfo = BuildInfo.loadByName(sContext.getString(R.string.build_info_class));
mModel = new LauncherModel(this, mIconCache, mAppFilter);
LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel);
// Register intent receivers
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_LOCALE_CHANGED);
filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
// For handling managed profiles
filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED);
filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED);
sContext.registerReceiver(mModel, filter);
UserManagerCompat.getInstance(sContext).enableAndResetCache();
}
在上面代码中我们看到有mInvariantDeviceProfile的初始化,接下来我们分析下它是如何加载一些默认屏幕配置的。
InvariantDeviceProfile的初始化
从上面代码我们可以看到在LauncherAppState的构造方法中通过new InvariantDeviceProfile的方式得到一个mInvariantDeviceProfile对象,我们来看下这个new的过程中做了什么事情。先上代码,我们再一步一步分析。
InvariantDeviceProfile(Context context) {
//获取WindowManager服务
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics dm = new DisplayMetrics();
display.getMetrics(dm);
Point smallestSize = new Point();
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<InvariantDeviceProfile> closestProfiles =
findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles());
//获取一个差值计算过的配置文件,用于配置图标及图标字体的大小
InvariantDeviceProfile interpolatedDeviceProfileOut =
invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles);
InvariantDeviceProfile closestProfile = closestProfiles.get(0);
numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns;
numHotseatIcons = closestProfile.numHotseatIcons;
hotseatAllAppsRank = (int) (numHotseatIcons / 2);
defaultLayoutId = closestProfile.defaultLayoutId;
numFolderRows = closestProfile.numFolderRows;
numFolderColumns = closestProfile.numFolderColumns;
minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns;
iconSize = interpolatedDeviceProfileOut.iconSize;
iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
// If the partner customization apk contains any grid overrides, apply them
// Supported overrides: numRows, numColumns, iconSize
applyPartnerDeviceProfileOverrides(context, dm);
Point realSize = new Point();
display.getRealSize(realSize);
// 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);
landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
largeSide, smallSide, true /* isLandscape */);
portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
smallSide, largeSide, false /* isLandscape */);
}
通过上述代码我们可以看到首先通过context.getSystemService获取一个WindowMnager实例,WindowManager是继承自ViewManager的一个接口,其实现类是WindowManagerImpl.java。
WindowManager中存在成员变量Display,可以通过wm.getDefaultDisplay获取Display实例,其实DefaulDisplay就是手机的默认屏幕(其他的屏幕可以是通过HDMI连接的屏幕)。
获取默认屏幕之后,根据屏幕最小宽高之后,从px转换成Dp,得到minWidthDps,minHeightDps这两个变量。然后根据最小的宽、高及预定义的配置文件getPredefinedDeviceProfiles(),得到一个最接近的配置文件列表。我们先来看下这个预定义的配置文件是什么?
预定义配置文件
其实这个预定义的配置文件,是google默认添加的一些设备Launcher的具体显示参数。如下所示:
predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
这条配置信息就是显示的Nexus S的Launcher显示的具体参数,依次为:最小的宽296、高491.33,桌面列表图标的行4、列数4,文件夹中图标的行4、列4,应用列表中图标的预设行数4,图标的大小48px,图标下方字体大小13px,dock栏图标个数及图标大小,最后一个xml是默认桌面的配置文件。
通过上边的一条配置信息我们就可以看出它规定了桌面显示的个个具体信息。有了这些显示的具体信息我们是如何得到最接近的配置列表的呢?
获取与当前屏幕最近进的配置文件
其实获取最接近的配置文件很简单就是通过最小的宽高和预制进去的宽高做一个平方和的平方根得到一个值,根据这个值讲预制的列表做一个从小到大的排序。
//平方和的平方根运算
float dist(float x0, float y0, float x1, float y1) {
return (float) Math.hypot(x1 - x0, y1 - y0);
}
//通过排序得到一个最近配置列表,最接近的排在最前边
ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
final float width, final float height, ArrayList<InvariantDeviceProfile> points) {
// Sort the profiles by their closeness to the dimensions
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;
}
配置当前屏幕的显示参数
得到最接近的配置列表(closestProfiles.get(0);)之后,设置如下参数:
numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns;
numHotseatIcons = closestProfile.numHotseatIcons;
hotseatAllAppsRank = (int) (numHotseatIcons / 2);
defaultLayoutId = closestProfile.defaultLayoutId;
numFolderRows = closestProfile.numFolderRows;
numFolderColumns = closestProfile.numFolderColumns;
minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns;
通过上边的代码我们发现并不是通过closestProfile设置图标及图标字体的大小。而是另外的配置文件interpolatedDeviceProfileOut。
iconSize = interpolatedDeviceProfileOut.iconSize;
iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
那么这个interpolatedDeviceProfileOut是从哪里来的呢?我们回到上边InvariantDeviceProfile初始化的代码。我们可以看到InvariantDeviceProfile又是通过一些插值运算得到的。
InvariantDeviceProfile invDistWeightedInterpolate(float width, float height,
ArrayList<InvariantDeviceProfile> points) {
float weights = 0;
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);
}
我们可以简单理解为当我们的屏幕无法在预制列表中找到最佳(屏幕完全一致)配置时,为了找到最合适显示当前屏幕的图标的大小,我们需要找到接近的(KNEARESTNEIGHBOR)三个配置,然后通过一个加权运算(weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);)得到一个平均值,以达到最合适的图标及图标字体大小。至此Launcher已经完成了显示的一系列参数,最后我们再回到closestProfile.minAllAppsPredictionColumns,这是一个预设的参数,在实际的显示中会根据图标的大小由launcher动态调整。
总结
我们看到Launcher的屏幕适配其实就是得到一些预制的配置参数,通过计算得到一个最接近的配置文件,通过该配置文件为当前屏幕的显示做一些参数设置,已达到适配的目的。