基于Android7.0的Launcher3源码分析(3)——显示相关参数的初始化
前面第一篇讲解了Launcher从初始化到显示的大概流程。接下来的文章将会一步步详细分析这些流程。今天这篇文章讲下Launcher显示相关参数的初始化和处理过程。
初始化入口
从onCreate()进入,首先判断了Launcher的排列方向,一般都是纵向的,所以本文就以纵向为例。通过app.getInvariantDeviceProfile().portraitProfile 获取到mDeviceProfile的实例对象,该对象中以成员变量的形式存储了显示相关参数。然后,根据这些参数,通过 mDeviceProfile.layout() 对Launcher的控件进行布局的调整。Launcher主要有哪些控件组成,详见文章 Launcher3源码分析(2)——UI显示控件的组成
下面围绕两个方面展开。1、mDeviceProfile 对象如何初始化;2、如何根据 mDeviceProfile 对象中保存的数据调整布局参数。
public class Launcher extends Activity
implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks,
View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener {
......
private DeviceProfile mDeviceProfile;
@Override
protected void onCreate(Bundle savedInstanceState) {
......
LauncherAppState app = LauncherAppState.getInstance();
// Load configuration-specific DeviceProfile
mDeviceProfile = getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE ?
app.getInvariantDeviceProfile().landscapeProfile
: app.getInvariantDeviceProfile().portraitProfile;
......
setContentView(R.layout.launcher);
setupViews();
mDeviceProfile.layout(this);
......
}
}
1、mDeviceProfile 对象初始化
1.1 LauncherAppState初始化
可以看到,这里获取了LauncherAppState的一个单例对象,看下它的初始化。这里最重要的是成员对象 mInvariantDeviceProfile 的初始化,因为 app.getInvariantDeviceProfile() 就是获取的该对象。
public class LauncherAppState {
......
private static LauncherAppState INSTANCE;
private InvariantDeviceProfile mInvariantDeviceProfile;
public static LauncherAppState getInstance() {
if (INSTANCE == null) {
INSTANCE = new LauncherAppState();
}
return INSTANCE;
}
......
private LauncherAppState() {
......
mInvariantDeviceProfile = new InvariantDeviceProfile(sContext);
mIconCache = new IconCache(sContext, mInvariantDeviceProfile);
mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache);
mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
mModel = new LauncherModel(this, mIconCache, mAppFilter);
......
}
public InvariantDeviceProfile getInvariantDeviceProfile() {
return mInvariantDeviceProfile;
}
......
}
1.2 InvariantDeviceProfile初始化
来到InvariantDeviceProfile的单个参数的构造方法。可以看到,之前在Launcher中app.getInvariantDeviceProfile().portraitProfile 的 portraitProfile 就是在这里初始化的。下面一步步来看。
public class InvariantDeviceProfile {
......
DeviceProfile portraitProfile;
@TargetApi(23)
InvariantDeviceProfile(Context context) {
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); //px转dp
minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); //px转dp
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 */);
}
1.2.1 获取屏幕的最大和最小尺寸
通过getCurrentSizeRange方法返回应用程序可以操作的最大和最小尺寸范围,赋值给largestSize和smallestSize。
largestSize.x:屏幕最大可用宽度; largestSize.y:屏幕最大可用高度
smallestSize.x:屏幕最小可用宽度; smallestSize.y:屏幕最小可用高度
什么是最大最小尺寸呢?我们知道,Android系统界面上会显示系统控件,如statusBar和navigationBar,当这些控件都显示时,可用的最小屏幕高度将会比屏幕的实际高度小,也就是可用最小范围。当statusBar这些系统控件都被隐藏时,就可能返回屏幕可用高度的最大值。
上面说的largestSize.x等都是以px为单位的,为了更好的适配分辨率,需要将其转化为dp为单位。通过dpiFromPx方法将其进行转化,然后把屏幕可用宽度的dp值赋值给minWidthDps,将屏幕可用高度的dp值赋值给minHeightDps。下面会根据这两个值来确定一部分参数的值。
1.2.2 根据设备配置参数获得当前图标和文字大小
可以看到,这里针对一些手机机型,进行了一些适配,定义了图标大小,默认的桌面行列布局等参数。并以ArrayList的形式进行存储,并返回给findClosestDeviceProfiles方法当做传入的参数。
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, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby",
255, 400, 3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby",
275, 420, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby",
255, 450, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4",
359, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
335, 567, 4, 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, 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, 4, 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, 4, 76, 14.4f, 7, 76, R.xml.default_workspace_5x6));
predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet",
1527, 2527, 7, 7, 6, 6, 4, 100, 20, 7, 72, R.xml.default_workspace_5x6));
return predefinedDeviceProfiles;
}
来到findClosestDeviceProfiles方法。该方法根据之前获取的屏幕可用宽高,来把getPredefinedDeviceProfiles获取到的不同机型的参数进行排序。这里采取一个升序的形式排序,即与屏幕可用宽高相差越小的机型配置排在越前面。这样返回的pointsByNearness链表中,排在第一个的就是与当前屏幕可用宽高最为接近的一组配置。
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 Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
dist(width, height, b.minWidthDps, b.minHeightDps));
}
});
return pointsByNearness;
}
来到invDistWeightedInterpolate方法。拿到排在第一组的机型参数,如果与当前屏幕可用宽高完全吻合,即直接返回该组机型的参数。如果没有完全吻合,则经过一系列的计算,来得到与当前屏幕可用宽高最为合适的各参数大小。具体算法这边就不细究了。
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);
}
计算完毕后,对重要参数进行赋值,留待使用。这里的iconSize等参数只是一个基础大小,并不是最终大小,后面可能根据需要还会有一些处理。
iconSize = interpolatedDeviceProfileOut.iconSize;
iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
1.2.3 获取屏幕的实际尺寸
通过getRealSize返回屏幕可用大小的实际尺寸。
为了方便理解,这里以屏幕物理大小为 720*1280 的手机为例,得到的输出参数如下:
- 最小可用尺寸:smallestSize.x = 720 smallestSize.y = 670
- 最大可用尺寸:largestSize.x = 1196 largestSize.y = 1134
- 实际尺寸:realSize.x = 720 realSize.y = 1280
1.2.4 完成实例化
将InvariantDeviceProfile对象, smallestSize, largestSize, smallSide, largeSide作为参数传入,就到了DeviceProfile对象的实例化的最后一步。
public DeviceProfile(Context context, InvariantDeviceProfile inv,
Point minSize, Point maxSize,
int width, int height, boolean isLandscape) {
this.inv = inv;
this.isLandscape = isLandscape;
Resources res = context.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
// Constants from resources
isTablet = res.getBoolean(R.bool.is_tablet);
isLargeTablet = res.getBoolean(R.bool.is_large_tablet);
isPhone = !isTablet && !isLargeTablet;
// Some more constants
transposeLayoutWithOrientation =
res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
ComponentName cn = new ComponentName(context.getPackageName(),
this.getClass().getName());
defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
desiredWorkspaceLeftRightMarginPx = 2 * edgeMarginPx;
pageIndicatorHeightPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height);
defaultPageSpacingPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing);
overviewModeMinIconZoneHeightPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height);
overviewModeMaxIconZoneHeightPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_max_icon_zone_height);
overviewModeBarItemWidthPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_item_width);
overviewModeBarSpacerWidthPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_spacer_width);
overviewModeIconZoneRatio =
res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f;
iconDrawablePaddingOriginalPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);
// AllApps uses the original non-scaled icon text size
allAppsIconTextSizeSp = inv.iconTextSize;
// AllApps uses the original non-scaled icon size
allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm);
// Determine sizes.
widthPx = width;
heightPx = height;
if (isLandscape) {
availableWidthPx = maxSize.x;
availableHeightPx = minSize.y;
} else {
availableWidthPx = minSize.x;
availableHeightPx = maxSize.y;
}
// Calculate the remaining vars
updateAvailableDimensions(dm, res);
computeAllAppsButtonSize(context);
}
构造方法进行了参数最终的赋值过程。将之前保存了图标基础大小等参数的InvariantDeviceProfile对象,赋给成员对象 this.inv = inv; 留待后面取用。如:allAppsIconTextSizeSp 就是直接取的inv对象中的数值进行赋值。
allAppsIconTextSizeSp = inv.iconTextSize
后面就是通过updateAvailableDimensions(dm, res)和computeAllAppsButtonSize(context)进行一些赋值操作,如下,就不细说了。
iconSizePx = (int) (Utilities.pxFromDp(inv.iconSize, dm) * scale);
iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale);
初始化完成后,本文最开始通过app.getInvariantDeviceProfile().portraitProfile返回的就是该实例对象。 Controller 层就可以通过该对象获取到显示的相关参数。
2、根据 mDeviceProfile 对象中保存的数据调整布局参数
通过传入Launcher的实例,来获取显示控件的实例对象,最后通过修改对应的 LayoutParams 进行布局的调整。
可以看到,这里对SearchDropTargetBar、Workspace、Hotseat、PageIndicator和长按桌面显示的OverviewPanel都进行了布局调整。这些控件的介绍详见 Launcher3源码分析(2)——UI显示控件的组成
public class DeviceProfile {
......
public void layout(Launcher launcher) {
FrameLayout.LayoutParams lp;
boolean hasVerticalBarLayout = isVerticalBarLayout();
final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources());
// Layout the search bar space
Rect searchBarBounds = getSearchBarBounds(isLayoutRtl);
View searchBar = launcher.getSearchDropTargetBar();
lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams();
lp.width = searchBarBounds.width();
lp.height = searchBarBounds.height();
lp.topMargin = searchBarTopExtraPaddingPx;
if (hasVerticalBarLayout) {
lp.gravity = Gravity.LEFT;
LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar);
targets.setOrientation(LinearLayout.VERTICAL);
FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams();
targetsLp.gravity = Gravity.TOP;
targetsLp.height = LayoutParams.WRAP_CONTENT;
} else {
// Horizontal search bar space
lp.gravity = Gravity.TOP|Gravity.CENTER_HORIZONTAL;
}
searchBar.setLayoutParams(lp);
// Layout the workspace
PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace);
lp = (FrameLayout.LayoutParams) workspace.getLayoutParams();
lp.gravity = Gravity.CENTER;
Rect padding = getWorkspacePadding(isLayoutRtl);
workspace.setLayoutParams(lp);
workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom);
workspace.setPageSpacing(getWorkspacePageSpacing(isLayoutRtl));
// Layout the hotseat
View hotseat = launcher.findViewById(R.id.hotseat);
lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams();
float workspaceCellWidth = (float) getCurrentWidth() / inv.numColumns;
float hotseatCellWidth = (float) getCurrentWidth() / inv.numHotseatIcons;
int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2);
if (hasVerticalBarLayout) {
// Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the
// screen regardless of RTL
lp.gravity = Gravity.RIGHT;
lp.width = normalHotseatBarHeightPx;
lp.height = LayoutParams.MATCH_PARENT;
hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx);
} else if (isTablet) {
// Pad the hotseat with the workspace padding calculated above
lp.gravity = Gravity.BOTTOM;
lp.width = LayoutParams.MATCH_PARENT;
lp.height = hotseatBarHeightPx;
hotseat.findViewById(R.id.layout).setPadding(
hotseatAdjustment + padding.left, 0,
hotseatAdjustment + padding.right, 2 * edgeMarginPx);
} else {
// For phones, layout the hotseat without any bottom margin
// to ensure that we have space for the folders
lp.gravity = Gravity.BOTTOM;
lp.width = LayoutParams.MATCH_PARENT;
lp.height = hotseatBarHeightPx;
hotseat.findViewById(R.id.layout).setPadding(
hotseatAdjustment + padding.left, 0,
hotseatAdjustment + padding.right, 0);
}
hotseat.setLayoutParams(lp);
// Layout the page indicators
View pageIndicator = launcher.findViewById(R.id.page_indicator);
if (pageIndicator != null) {
if (hasVerticalBarLayout) {
// Hide the page indicators when we have vertical search/hotseat
pageIndicator.setVisibility(View.GONE);
} else {
// Put the page indicators above the hotseat
lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams();
lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
lp.width = LayoutParams.WRAP_CONTENT;
lp.height = LayoutParams.WRAP_CONTENT;
lp.bottomMargin = hotseatBarHeightPx;
pageIndicator.setLayoutParams(lp);
}
}
// Layout the Overview Mode
ViewGroup overviewMode = launcher.getOverviewPanel();
if (overviewMode != null) {
int overviewButtonBarHeight = getOverviewModeButtonBarHeight();
lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams();
lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
int visibleChildCount = getVisibleChildCount(overviewMode);
int totalItemWidth = visibleChildCount * overviewModeBarItemWidthPx;
int maxWidth = totalItemWidth + (visibleChildCount-1) * overviewModeBarSpacerWidthPx;
lp.width = Math.min(availableWidthPx, maxWidth);
lp.height = overviewButtonBarHeight;
overviewMode.setLayoutParams(lp);
if (lp.width > totalItemWidth && visibleChildCount > 1) {
// We have enough space. Lets add some margin too.
int margin = (lp.width - totalItemWidth) / (visibleChildCount-1);
View lastChild = null;
// Set margin of all visible children except the last visible child
for (int i = 0; i < visibleChildCount; i++) {
if (lastChild != null) {
MarginLayoutParams clp = (MarginLayoutParams) lastChild.getLayoutParams();
if (isLayoutRtl) {
clp.leftMargin = margin;
} else {
clp.rightMargin = margin;
}
lastChild.setLayoutParams(clp);
lastChild = null;
}
View thisChild = overviewMode.getChildAt(i);
if (thisChild.getVisibility() != View.GONE) {
lastChild = thisChild;
}
}
}
}
}
}
扩展
这边列举了初始化的一些显示相关参数。
public int numRows; //桌面每行图标数
public int numColumns; //桌面每列图标数
// Device properties in current orientation
public final int availableWidthPx; //屏幕可用宽度
public final int availableHeightPx; //屏幕可用高度
// Workspace
private final int pageIndicatorHeightPx; //指示器高度
// Workspace icons
public int iconSizePx; //图标大小
public int iconTextSizePx; //图标文字大小
// Folder
public int folderIconSizePx;
public int folderCellWidthPx;
public int folderCellHeightPx;
// Hotseat
public int hotseatCellWidthPx;
public int hotseatCellHeightPx;
public int hotseatIconSizePx;
// All apps
public int allAppsButtonVisualSize; //allApp入口按钮大小
public final int allAppsIconSizePx;
public final float allAppsIconTextSizeSp;
如果想修改图标默认大小。一般有两种方式,
修改InvariantDeviceProfile对象中图标原始基础大小:成员变量 iconSize
修改DeviceProfile对象中最终生效的图标大小:成员变量 iconSizePx
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库