Android M Launcher3屏幕適配
前言
我們知道Launcher3在不同的設備上,都能夠很好的適配屏幕,包括桌面圖標大小、字體大小、桌面及應用列表的行列數等。那么它是怎么做到的呢?
LauncherAppState初始化
要想知道Launcher3是如何做到屏幕適配的,我們首先從Launcher的初始化開始分析。我們知道Launcher的初始化是從Launcher.java的onCreate方法開始的,我們先看下onCreate最先做了什么事情。
super.onCreate(savedInstanceState);
LauncherAppState.setApplicationContext(getApplicationContext());
LauncherAppState app = LauncherAppState.getInstance();
...</code></pre>
我們看到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的屏幕適配其實就是得到一些預制的配置參數,通過計算得到一個最接近的配置文件,通過該配置文件為當前屏幕的顯示做一些參數設置,已達到適配的目的。
來自:http://www.jianshu.com/p/d52b134652aa