前言
我們知道Launcher3在不同的設(shè)備上,都能夠很好的適配屏幕,包括桌面圖標(biāo)大小、字體大小、桌面及應(yīng)用列表的行列數(shù)等。那么它是怎么做到的呢?
LauncherAppState初始化
要想知道Launcher3是如何做到屏幕適配的,我們首先從Launcher的初始化開始分析。我們知道Launcher的初始化是從Launcher.java的onCreate方法開始的,我們先看下onCreate最先做了什么事情。
super.onCreate(savedInstanceState);
LauncherAppState.setApplicationContext(getApplicationContext());
LauncherAppState app = LauncherAppState.getInstance();
...
我們看到Launcher在onCreate方法開始首先初始化LauncherAppState,這一初始化過程主要做了兩件事情,1)設(shè)置應(yīng)用上下文;2)獲取LauncherAppState的單例對(duì)象。在獲取取LauncherAppState的單例對(duì)象過程中,如果LauncherAppState的單例對(duì)象不存在,則會(huì)初始化一個(gè)。在LauncherAppState的構(gòu)造方法中有一系列的初始化。
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的初始化,接下來我們分析下它是如何加載一些默認(rèn)屏幕配置的。
InvariantDeviceProfile的初始化
從上面代碼我們可以看到在LauncherAppState的構(gòu)造方法中通過new InvariantDeviceProfile的方式得到一個(gè)mInvariantDeviceProfile對(duì)象,我們來看下這個(gè)new的過程中做了什么事情。先上代碼,我們?cè)僖徊揭徊椒治觥?/p>
InvariantDeviceProfile(Context context) {
//獲取WindowManager服務(wù)
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);
//通過最小寬高和預(yù)定義的配置文件獲取一個(gè)最接近的配置文件列表
ArrayList<InvariantDeviceProfile> closestProfiles =
findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles());
//獲取一個(gè)差值計(jì)算過的配置文件,用于配置圖標(biāo)及圖標(biāo)字體的大小
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獲取一個(gè)WindowMnager實(shí)例,WindowManager是繼承自ViewManager的一個(gè)接口,其實(shí)現(xiàn)類是WindowManagerImpl.java。
WindowManager中存在成員變量Display,可以通過wm.getDefaultDisplay獲取Display實(shí)例,其實(shí)DefaulDisplay就是手機(jī)的默認(rèn)屏幕(其他的屏幕可以是通過HDMI連接的屏幕)。
獲取默認(rèn)屏幕之后,根據(jù)屏幕最小寬高之后,從px轉(zhuǎn)換成Dp,得到minWidthDps,minHeightDps這兩個(gè)變量。然后根據(jù)最小的寬、高及預(yù)定義的配置文件getPredefinedDeviceProfiles(),得到一個(gè)最接近的配置文件列表。我們先來看下這個(gè)預(yù)定義的配置文件是什么?
預(yù)定義配置文件
其實(shí)這個(gè)預(yù)定義的配置文件,是google默認(rèn)添加的一些設(shè)備Launcher的具體顯示參數(shù)。如下所示:
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顯示的具體參數(shù),依次為:最小的寬296、高491.33,桌面列表圖標(biāo)的行4、列數(shù)4,文件夾中圖標(biāo)的行4、列4,應(yīng)用列表中圖標(biāo)的預(yù)設(shè)行數(shù)4,圖標(biāo)的大小48px,圖標(biāo)下方字體大小13px,dock欄圖標(biāo)個(gè)數(shù)及圖標(biāo)大小,最后一個(gè)xml是默認(rèn)桌面的配置文件。
通過上邊的一條配置信息我們就可以看出它規(guī)定了桌面顯示的個(gè)個(gè)具體信息。有了這些顯示的具體信息我們是如何得到最接近的配置列表的呢?
獲取與當(dāng)前屏幕最近進(jìn)的配置文件
其實(shí)獲取最接近的配置文件很簡(jiǎn)單就是通過最小的寬高和預(yù)制進(jìn)去的寬高做一個(gè)平方和的平方根得到一個(gè)值,根據(jù)這個(gè)值講預(yù)制的列表做一個(gè)從小到大的排序。
//平方和的平方根運(yùn)算
float dist(float x0, float y0, float x1, float y1) {
return (float) Math.hypot(x1 - x0, y1 - y0);
}
//通過排序得到一個(gè)最近配置列表,最接近的排在最前邊
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;
}
配置當(dāng)前屏幕的顯示參數(shù)
得到最接近的配置列表(closestProfiles.get(0);)之后,設(shè)置如下參數(shù):
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;
通過上邊的代碼我們發(fā)現(xiàn)并不是通過closestProfile設(shè)置圖標(biāo)及圖標(biāo)字體的大小。而是另外的配置文件interpolatedDeviceProfileOut。
iconSize = interpolatedDeviceProfileOut.iconSize;
iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
那么這個(gè)interpolatedDeviceProfileOut是從哪里來的呢?我們回到上邊InvariantDeviceProfile初始化的代碼。我們可以看到InvariantDeviceProfile又是通過一些插值運(yùn)算得到的。
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);
}
我們可以簡(jiǎn)單理解為當(dāng)我們的屏幕無法在預(yù)制列表中找到最佳(屏幕完全一致)配置時(shí),為了找到最合適顯示當(dāng)前屏幕的圖標(biāo)的大小,我們需要找到接近的(KNEARESTNEIGHBOR)三個(gè)配置,然后通過一個(gè)加權(quán)運(yùn)算(weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);)得到一個(gè)平均值,以達(dá)到最合適的圖標(biāo)及圖標(biāo)字體大小。至此Launcher已經(jīng)完成了顯示的一系列參數(shù),最后我們?cè)倩氐絚losestProfile.minAllAppsPredictionColumns,這是一個(gè)預(yù)設(shè)的參數(shù),在實(shí)際的顯示中會(huì)根據(jù)圖標(biāo)的大小由launcher動(dòng)態(tài)調(diào)整。
總結(jié)
我們看到Launcher的屏幕適配其實(shí)就是得到一些預(yù)制的配置參數(shù),通過計(jì)算得到一個(gè)最接近的配置文件,通過該配置文件為當(dāng)前屏幕的顯示做一些參數(shù)設(shè)置,已達(dá)到適配的目的。