安卓开发- 安卓13 Launcher3 主页结构修改
修改主页结构概述
在 Android 操纵系统中,Launcher (主页应用)是用户与设备交互的核心界面之一,它负责显示应用程序列表、提供快捷方式、管理小部件等功能。其中,Launcher3 是 Android 系统默认的启动器应用程序,我们可以通过修改Launcher3的源码,来改变主页应用的样式和结构。下面将联合源码,分析如何修改Launcher3主页的相关样式和结构。
结构构成
Launcher3最核心的类是一个Launcher.java(可以看作是Launcher中的MainActivity),基本上所有操纵(包括UI的定制)都会合在这个Activity上。在Launcher.java 中,通过setContentView()设置的结构参数是R.layout.launcher,对应的是launcher.xml文件,它定义了启动器界面的整体结构和组件的位置,我们看下这个结构文件里的内容:
<?xml version="1.0" encoding="utf-8"?>
<com.android.launcher3.LauncherRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.android.launcher3.dragndrop.DragLayer
android:id="@+id/drag_layer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:importantForAccessibility="no">
<com.android.launcher3.views.AccessibilityActionsView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/home_screen"
/>
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:theme="@style/HomeScreenElementTheme"
launcher:pageIndicator="@+id/page_indicator" />
<!-- DO NOT CHANGE THE ID -->
<include
android:id="@+id/hotseat"
layout="@layout/hotseat" />
<!-- Keep these behind the workspace so that they are not visible when
we go into AllApps -->
<com.android.launcher3.pageindicators.WorkspacePageIndicator
android:id="@+id/page_indicator"
android:layout_width="match_parent"
android:layout_height="@dimen/workspace_page_indicator_height"
android:layout_gravity="bottom|center_horizontal"
android:theme="@style/HomeScreenElementTheme" />
<include
android:id="@+id/drop_target_bar"
layout="@layout/drop_target_bar" />
<com.android.launcher3.views.ScrimView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/scrim_view"
android:background="@android:color/transparent" />
<include
android:id="@+id/apps_view"
layout="@layout/all_apps"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include
android:id="@+id/overview_panel"
layout="@layout/overview_panel" />
</com.android.launcher3.dragndrop.DragLayer>
</com.android.launcher3.LauncherRootView>
由上面内容可以看到,Launcher3,父结构是一个自定义的LauncherRootView,在这个LauncherRootView中只有一个子结构DragLayer,在DragLayer中放置了Workspace、Hotseat、WorkspacePageIndicator等内容。这里我们先看下Launcher3主页的构造图,以便我们相识各个子结构的相关内容:
https://i-blog.csdnimg.cn/direct/eb8b1cbe94ff44f0b54404197fd17abd.png
[*]Launcher3结构的最外貌是一个自定义的View:LauncherRootView,它是继承自FrameLayout;LauncherRootView内里只有一个叫DragLayer的ViewGroup,它同样继承自FrameLayout,主要功能就是处置惩罚拖拽事件,当你在拖拽一个图标的时候,就相当于是一个view放到了DragLayer内里,这个view会跟随你的手在屏幕上移动。
[*]屏幕上可以左右滑动的整个页面叫做Workspace,Workspace的父类是PagedView,PagedViewk用来处置惩罚左右滑动。
[*]Workspace内里可能含有多个页面(屏),多个页面存在时,可以左右滑动来切换页面;可以滑动的单独一屏就是一个CellLayout,CellLayout负责本身页面图标和小部件的显示和整齐摆放。
[*]在左右滑动屏幕切换页面时 屏幕最下方会出现的指示器PageIndicator(一样平常是几个小圆点,这里图示中被隐藏了),告诉你桌面有几屏,当前在哪一屏上(圆点会高亮)。
[*]在Workspace中,向上滑动屏幕可以唤出所有应用列表(抽屉样式),向下滑动可以唤出状态栏和关照栏。
[*]在CellLayout中可以放置组件、应用、包罗应用的文件夹等。如上图中顶部的搜索框就是谷歌提供的原生搜索框(实在它并不算一个组件,而是一个特别元素),屏幕靠下方的左右两边分别是应用文件夹和单个应用图标。当长按CellLayout上的组件、应用、文件夹或者空缺地方的时候,会出现一个MENU菜单,可以对组件或应用进行配置,或者添加组件等。当长按组件、应用或文件夹,并拖动到屏幕上方时,屏幕上方会显示一个DropTargetBar区域,内里有“移除”按钮,可以对组件进行移除,对app进行卸载等操纵。
[*]底部有五个固定不动的图标所在的区域叫做Hotseat,用来放置比较常用的应用,好比拨号,短信,相机等。
[*]底部右侧有三个白色按钮的区域是导航栏。导航栏可以在所有页面中常显,用于全局控制(返回、回到主页、最近应用)。
这里简单展示下几个控件的结构代码:
hotseat.xml
<com.android.launcher3.Hotseat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/hotseat"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/HomeScreenElementTheme"
android:importantForAccessibility="no"
android:preferKeepClear="true"
launcher:containerType="hotseat" />
all_apps.xml
<com.android.launcher3.allapps.LauncherAllAppsContainerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/apps_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="true"
android:clipToPadding="false"
android:focusable="false"
android:saveEnabled="false" />
drop_target_bar.xml
<com.android.launcher3.DropTargetBar xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/dynamic_grid_drop_target_size"
android:layout_gravity="center_horizontal|top"
android:focusable="false"
android:alpha="0"
android:theme="@style/HomeScreenElementTheme"
android:visibility="invisible">
<!-- Delete target -->
<com.android.launcher3.DeleteDropTarget
android:id="@+id/delete_target_text"
style="@style/DropTargetButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/remove_drop_target_label" />
<!-- Uninstall target -->
<com.android.launcher3.SecondaryDropTarget
android:id="@+id/uninstall_target_text"
style="@style/DropTargetButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/uninstall_drop_target_label" />
</com.android.launcher3.DropTargetBar>
上面这三个控件都是直接include在Launcher.xml的结构中的,其他子结构也是以类似于自定义View的情势被添加到Launcher.xml中的,好比workspace:
<com.android.launcher3.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:theme="@style/HomeScreenElementTheme"
launcher:pageIndicator="@+id/page_indicator" />
Workspace.java是在Launcher的onCreate()阶段被创建并添加到主页内里的:
// Launcher3/src/com/android/launcher3/Launcher.java
@Thunk
Workspace<?> mWorkspace;
@Thunk
DragLayer mDragLayer;
@Override
@TargetApi(Build.VERSION_CODES.S)
protected void onCreate(Bundle savedInstanceState) {
// ...
setupViews();
// ...
}
protected void setupViews() {
inflateRootView(R.layout.launcher);
mDragLayer = findViewById(R.id.drag_layer);
mFocusHandler = mDragLayer.getFocusIndicatorHelper();
// 绑定workspace
mWorkspace = mDragLayer.findViewById(R.id.workspace);
// workspace做一些初始化的工作
mWorkspace.initParentViews(mDragLayer);
mOverviewPanel = findViewById(R.id.overview_panel);
mHotseat = findViewById(R.id.hotseat);
mHotseat.setWorkspace(mWorkspace);
mDragLayer.setup(mDragController, mWorkspace);
mWorkspace.setup(mDragController);
mWorkspace.lockWallpaperToDefaultPage();
mWorkspace.bindAndInitFirstWorkspaceScreen();
mDragController.addDragListener(mWorkspace);
// ...
}
各子结构的添加和加载就不展开介绍了,这里先介绍一个Launcher3很重要的内容:DeviceProfile.java和device_profiles.xml:
DeviceProfile
DeviceProfile.java包罗了多个用于描述设备配置的属性,比方mNumColumns(列数)、mNumRows(行数)、mHotseatHeight(Dock的高度)、mIconSizePx(图标大小)等,这些属性用于确定Launcher的结构和各个组件的位置。DeviceProfile在Launcher启动时被创建,并根据设备的现实配置(device_profiles.xml)进行初始化。
[*]初始化过程中会根据设备的屏幕尺寸、方向等因素盘算出各种关键属性。
[*]当设备的配置发生变化时(比方旋转屏幕),DeviceProfile会被更新以反映新的配置。
[*]更新过程中会重新盘算关键属性,并关照Launcher的各个组件进行相应的调解。
device_profiles.xml是一个XML配置文件,位于Launcher3项目的res/xml目次下。它定义了一系列设备配置,每种配置对应着不同的屏幕尺寸、方向和密度等信息,这一系列配置用于在不同设备上适配Launcher。这样可以确保Launcher在不同设备上都可以大概良好地显示和运行。
当Launcher启动时,它会被DeviceProfile.java动态加载,并根据当前设备的现实配置找到最合适的预设配置,这些预设配置【mNumColumns(列数)、mNumRows(行数)、mIconSizePx(图标大小)等】用于初始化DeviceProfile对象。【各属性的介绍放到下一末节中了】
下面简单介绍下DeviceProfile.java和device_profiles.xml的加载流程:
// Launcher3/src/com/android/launcher3/Launcher.java
@Override
@TargetApi(Build.VERSION_CODES.S)
protected void onCreate(Bundle savedInstanceState) {
// ...
LauncherAppState app = LauncherAppState.getInstance(this);
// 通过LauncherAppState的getInvariantDeviceProfile方法获取InvariantDeviceProfile对象
InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
// 在initDeviceProfile方法里获取DeviceProfile对象
initDeviceProfile(idp);
// ...
}
/**
* Returns {@code true} if a new DeviceProfile is initialized, and {@code false} otherwise.
*/
protected boolean initDeviceProfile(InvariantDeviceProfile idp) {
// 获取DeviceProfile,这里DeviceProfile对象来源是InvariantDeviceProfile.getDeviceProfile()
DeviceProfile deviceProfile = idp.getDeviceProfile(this);
if (mDeviceProfile == deviceProfile) {
return false;
}
mDeviceProfile = deviceProfile;
// ...
}
在Launcher.java的onCreate()方法中,先调用LauncherAppState.getInvariantDeviceProfile()方法获取一个InvariantDeviceProfile对象,然后在initDeviceProfile()方法中,通过InvariantDeviceProfile对象的getDeviceProfile()方法获取到DeviceProfile对象。跟进到LauncherAppState中看下:
// Launcher3/src/com/android/launcher3/LauncherAppState.java
private final InvariantDeviceProfile mInvariantDeviceProfile;
public LauncherAppState(Context context) {
// 调用两个参数的构造方法
this(context, LauncherFiles.APP_ICONS_DB);
Log.v(Launcher.TAG, "LauncherAppState initiated");
// ...
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
mContext = context;
// 获取mInvariantDeviceProfile对象
mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
// ...
}
public InvariantDeviceProfile getInvariantDeviceProfile() {
// 返回InvariantDeviceProfile对象
return mInvariantDeviceProfile;
}
由上面代码可以看出,InvariantDeviceProfile对象是在LauncherAppState的构造方法中创建的,并通过getInvariantDeviceProfile()方法返回一个InvariantDeviceProfile对象。继续跟进到InvariantDeviceProfile.java中:
// Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
// 这里的 INSTANCE可以理解为获取自身的单例对象
// MainThreadInitializedObject是一个用于定义在主线程上启动的单例的实用工具类
public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
@TargetApi(23)
private InvariantDeviceProfile(Context context) {
String gridName = getCurrentGridName(context);
// 调用initGrid()方法
String newGridName = initGrid(context, gridName);
// ...
}
private String initGrid(Context context, String gridName) {
Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
@DeviceType int deviceType = getDeviceType(displayInfo);
// 调用getPredefinedDeviceProfiles方法加载device_profiles.xml文件
ArrayList<DisplayOption> allOptions =
getPredefinedDeviceProfiles(context, gridName, deviceType,
RestoreDbTask.isPending(context));
// 把device_profiles.xml文件中的配置信息记录到displayOption中
DisplayOption displayOption =
invDistWeightedInterpolate(displayInfo, allOptions, deviceType);
// 调用四个参数的initGrid()方法
initGrid(context, displayInfo, displayOption, deviceType);
return displayOption.grid.name;
}
private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
ArrayList<DisplayOption> profiles = new ArrayList<>();
// 加载device_profiles.xml文件
try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
final int depth = parser.getDepth();
// ...
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException(e);
}
// ...
}
private void initGrid(Context context, Info displayInfo, DisplayOption displayOption, @DeviceType int deviceType) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
// 获取displayOption里面配置信息(实际上就是device_profiles.xml中读取的信息)
GridOption closestProfile = displayOption.grid;
numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns;
numSearchContainerColumns = closestProfile.numSearchContainerColumns;
// ...
final List<DeviceProfile> localSupportedProfiles = new ArrayList<>();
defaultWallpaperSize = new Point(displayInfo.currentSize);
SparseArray<DotRenderer> dotRendererCache = new SparseArray<>();
for (WindowBounds bounds : displayInfo.supportedBounds) {
// 通过DeviceProfile.Build()方法创建DeviceProfile对象
localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
.setIsMultiDisplay(deviceType == TYPE_MULTI_DISPLAY)
.setWindowBounds(bounds)
.setDotRendererCache(dotRendererCache)
.build());
// ...
}
// 把DeviceProfile列表存到supportedProfiles中,supportedProfiles是一个类型为List<DeviceProfile>的列表
// 这里创建了DeviceProfile列表,是因为DeviceProfile中有多个不同的配置,一套配置可以视为一个DeviceProfile对象
supportedProfiles = Collections.unmodifiableList(localSupportedProfiles);
// ...
}
在InvariantDeviceProfile.java的构造方法中,调用了initGrid(context, gridName)方法,然后在initGrid()方法中调用getPredefinedDeviceProfiles()方法去加载device_profiles.xml配置文件,随后又调用了四个参数的initGrid()方法创建DeviceProfile对象列表,并把它赋值给supportedProfiles,supportedProfiles是一个类型为List<DeviceProfile>的列表。
在Launcher.java中是是通过idp.getDeviceProfile(this)来获取DeviceProfile对象的,我们看下InvariantDeviceProfile的getDeviceProfile()方法:
// Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
public DeviceProfile getDeviceProfile(Context context) {
Resources res = context.getResources();
Configuration config = context.getResources().getConfiguration();
float screenWidth = config.screenWidthDp * res.getDisplayMetrics().density;
float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density;
int rotation = WindowManagerProxy.INSTANCE.get(context).getRotation(context);
// ...
// 根据屏幕的宽高等信息,获取最适合的DeviceProfile配置文件
return getBestMatch(screenWidth, screenHeight, rotation);
}
/**
* 返回与所提供的屏幕配置相匹配的设备配置文件
*/
public DeviceProfile getBestMatch(float screenWidth, float screenHeight, int rotation) {
DeviceProfile bestMatch = supportedProfiles.get(0);
float minDiff = Float.MAX_VALUE;
// 遍历supportedProfiles列表,找出最匹配的配置文件。这里的supportedProfiles就是前面保存的DeviceProfile列表
for (DeviceProfile profile : supportedProfiles) {
float diff = Math.abs(profile.widthPx - screenWidth) + Math.abs(profile.heightPx - screenHeight);
if (diff < minDiff) {
minDiff = diff;
bestMatch = profile;
} else if (diff == minDiff && profile.rotationHint == rotation) {
bestMatch = profile;
}
}
return bestMatch;
}
所以执行到这里,Launcher.java中就可以拿到与当前屏幕最匹配的DeviceProfile配置了,通过这个DeviceProfile配置,可以去调解桌面的结构和组件,如下:
// Launcher3/src/com/android/launcher3/Launcher.java
public void finishBindingItems(IntSet pagesBoundFirst) {
Object traceToken = TraceHelper.INSTANCE.beginSection("finishBindingItems");
mWorkspace.restoreInstanceStateForRemainingPages();
// ...
// 获取mDeviceProfile配置信息里面的numFolderColumns和numFolderRows去做计算
getViewCache().setCacheSize(R.layout.folder_application,
mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
getViewCache().setCacheSize(R.layout.folder_page, 2);
TraceHelper.INSTANCE.endSection(traceToken);
mWorkspace.removeExtraEmptyScreen(true);
}
实在不但是Launcher.java,在其他的类内里也会调用去做一些UI上的处置惩罚,好比Workspace.java中更新格子的Padding:
// Launcher3/src/com/android/launcher3/Workspace.java
private void updateCellLayoutPadding() {
// 获取配置信息里面的cellLayoutPaddingPx
Rect padding = mLauncher.getDeviceProfile().cellLayoutPaddingPx;
mWorkspaceScreens.forEach(s -> s.setPadding(padding.left, padding.top, padding.right, padding.bottom));
}
到这里DeviceProfile的加载分析流程就结束了,如上面示例所说,在Launcher3中,DeviceProfile.java和device_profiles.xml是两个非常重要的文件,它们对Launcher3主页结构是至关重要的,想要修改Launcher3主页的结构,可以从这个方面入手。
主页结构修改
上面的内容简单介绍了DeviceProfile的功能和加载流程,下面来分析,如何修改主页结构,还是沿着上面的思绪,从DeviceProfile配置文件入手。
Launcher3启动的时候,会加载 Launcher3\res\xml\device_profiles.xml 文件中预设的结构,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
<grid-option
launcher:name="3_by_3"
launcher:numRows="3"
launcher:numColumns="3"
launcher:numFolderRows="2"
launcher:numFolderColumns="3"
launcher:numHotseatIcons="3"
launcher:dbFile="launcher_3_by_3.db"
launcher:defaultLayoutId="@xml/default_workspace_3x3"
launcher:deviceCategory="phone" >
<!--省略多个display-option子标签内容-->
</grid-option>
<grid-option
launcher:name="4_by_4"
launcher:numRows="4"
launcher:numColumns="4"
launcher:numFolderRows="3"
launcher:numFolderColumns="4"
launcher:numHotseatIcons="4"
launcher:numExtendedHotseatIcons="6"
launcher:dbFile="launcher_4_by_4.db"
launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_split"
launcher:defaultLayoutId="@xml/default_workspace_4x4"
launcher:deviceCategory="phone|multi_display" >
<!--省略多个display-option子标签内容-->
</grid-option>
<grid-option
launcher:name="5_by_5"
launcher:numRows="5"
launcher:numColumns="5"
launcher:numFolderRows="4"
launcher:numFolderColumns="4"
launcher:numHotseatIcons="5"
launcher:numExtendedHotseatIcons="6"
launcher:dbFile="launcher.db"
launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_split"
launcher:defaultLayoutId="@xml/default_workspace_5x5"
launcher:deviceCategory="phone|multi_display" >
<!--省略多个display-option子标签内容-->
</grid-option>
<grid-option
launcher:name="6_by_5"
launcher:numRows="6"
launcher:numColumns="7"
launcher:numSearchContainerColumns="5"
launcher:numFolderRows="3"
launcher:numFolderColumns="4"
launcher:numHotseatIcons="0"
launcher:hotseatColumnSpanLandscape="2"
launcher:numAllAppsColumns="6"
launcher:isScalable="true"
launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_6_5"
launcher:devicePaddingId="@xml/paddings_6x5"
launcher:dbFile="launcher_6_by_5.db"
launcher:defaultLayoutId="@xml/default_workspace_6x5"
launcher:deviceCategory="tablet" >
<display-option
launcher:name="Tablet"
launcher:minWidthDps="900"
launcher:minHeightDps="820"
launcher:minCellHeight="120"
launcher:minCellWidth="102"
launcher:minCellHeightLandscape="104"
launcher:minCellWidthLandscape="120"
launcher:iconImageSize="60"
launcher:iconTextSize="14"
launcher:borderSpaceHorizontal="16"
launcher:borderSpaceVertical="64"
launcher:borderSpaceLandscapeHorizontal="64"
launcher:borderSpaceLandscapeVertical="16"
launcher:horizontalMargin="54"
launcher:horizontalMarginLandscape="120"
launcher:allAppsCellWidth="96"
launcher:allAppsCellHeight="142"
launcher:allAppsCellWidthLandscape="126"
launcher:allAppsCellHeightLandscape="126"
launcher:allAppsIconSize="60"
launcher:allAppsIconTextSize="14"
launcher:allAppsBorderSpaceHorizontal="8"
launcher:allAppsBorderSpaceVertical="16"
launcher:allAppsBorderSpaceLandscape="16"
launcher:hotseatBarBottomSpace="30"
launcher:hotseatBarBottomSpaceLandscape="40"
launcher:canBeDefault="true" />
</grid-option>
</profiles>
这个文件配置代码是用于定义Google Launcher在不同设备和显示模式下的结构参数,特别是针对具有特定屏幕尺寸和平板电脑类设备的结构。该文件里定义了4个不同的结构类型,分别是3x3、4x4、5x5、6x5,上面有讲到,在调用InvariantDeviceProfile的getDeviceProfile()方法时,设备会根据当前屏幕的尺寸来加载相应的结构配置。
那我们在开发时如何确定设备加载哪个结构呢?可以在桌面长按应用图标,然后拖动一下,观察在x和y方向上可以移动多少个格子,这样就可以确定本身的设备加载的是哪一个结构类型了。一样平常平板(或者移动大屏等)设备会加载6*5的结构,如今较新的手机也可以支持5x5乃至6x5的结构了。
device_profiles.xml文件中的<grid-option>标签包罗了各种属性,用于定制Launcher界面的外貌和活动。结构内里具体的属性说明如下:
[*]launcher:name="6_by_5": 定义了结构的名称,这里是“6_by_5”,代表的是6行5列的基本结构结构,但现实上后续可以通过launcher:numRows、launcher:numColumns属性设置为其他的结构。因为项目需要,我这里对6 * 5的结构做了改动,改成了 7 * 6的结构。
[*]launcher:numRows="6" 和 launcher:numColumns="7": 指定了主屏幕上行数和列数。
[*]launcher:numSearchContainerColumns="5": 搜索栏容器的列数。
[*]launcher:numFolderRows="3" 和 launcher:numFolderColumns="4": 文件夹中的行数和列数。
[*]launcher:numHotseatIcons="0": Hotseat(快速启动栏)中的图标数目,在这里设置为0,意味着不显示Hotseat。
[*]launcher:hotseatColumnSpanLandscape="2": 在横屏模式下,Hotseat占据的列数。
[*]launcher:numAllAppsColumns="6": 所有应用列表中的列数。
[*]launcher:isScalable="true": 表示是否可以缩放结构。
[*]launcher:inlineNavButtonsEndSpacing 和 launcher:devicePaddingId: 分别定义了内联导航按钮的结束间距和设备的填充空间ID。
[*]launcher:dbFile 和 launcher:defaultLayoutId: 数据库文件名和默认结构ID,用于存储和恢复结构状态。
[*]launcher:deviceCategory="tablet": 设备类别,这里是平板电脑。
接下来的 <display-option> 标签提供了更多细节:
[*]launcher:minWidthDps="900" 和 launcher:minHeightDps="820": 最小宽度和高度(以密度无关像素dp为单元),用于确定此结构适用于哪种屏幕尺寸。
[*]launcher:minCellHeight 和 launcher:minCellWidth: 单个单元格的最小高度和宽度。
[*]背面的属性,如 launcher:iconImageSize、launcher:iconTextSize 等,分别定义了图标、文本、边框空间等元素的尺寸和间距。
[*]launcher:horizontalMargin 和 launcher:horizontalMarginLandscape: 程度方向上的外边距,在竖屏和横屏模式下可能不同。
[*]launcher:hotseatBarBottomSpace 和 launcher:hotseatBarBottomSpaceLandscape: Hotseat在竖屏和横屏模式下的底部间距。
[*]launcher:canBeDefault="true": 表示此结构选项可以作为默认结构。
这里我们重点关注下defaultLayoutId这个属性:在上面的配置文件中,6x5结构的defaultLayoutId=“@xml/default_workspace_6x5”,即指定了default_workspace_6x5.xml文件作为6x5样式的结构资源文件。
default_workspace_MxN.xml这是用于定义用户桌面快捷方式(Favorites)配置的一个资源文件,代表主页结构是M行N列,我们可以在内里定义显示在利用这个结构情况下,worksapce中的各个控件,如下面给出了显示一个Hotseat应用的示例:
<?xml version="1.0" encoding="utf-8"?>
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!-- 配置一个Hotseat应用-->
<resolve
launcher:container="-101"
launcher:screen="0"
launcher:x="0"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
<favorite launcher:uri="mailto:" />
</resolve>
<!-- 其他配置 -->
</favorites>
文件由<favorites>标签括起来,内里可以包罗一个或多个<resolve>标签,每个<resolve>标签都定义了一组快捷方式项,这些快捷方式可以直接链接到特定的应用或者执行特定的操纵(如打开邮件客户端、日历、图库等)。<resolve>的子标签表示快捷方式项的类型,快捷方式项必须放在<resolve>标签内才生效。<resolve>支持的子标签如下:
favorite //应用程序快捷方式
widget //桌面控件(小组件)
shortcut //链接,如网址、本地磁盘路径等
search //搜索框(谷歌搜索框)
clock //桌面上的钟表Widget
folder //桌面文件夹(如谷歌应用的文件夹)
同时<resolve>标签或其子标签通过launcher:XXX来设定快捷方式项的位置等信息,这些属性可以写在<resolve>标签或其子标签中,支持的属性如下:
// resolve标签支持的属性
launcher:title// 图标下面的文字,目前只支持引用,不能直接书写字符串;
launcher:icon // 图标引用(适用于应用快捷方式);
launcher:uri // 链接地址,链接网址用的,使用shortcut标签就可以定义一个超链接,打开某个网址,文件等。
launcher:packageName // 应用程序的包名;
launcher:className // 应用程序的启动类名(要写全路径);
launcher:screen // 图标所在的屏幕编号,0表示第一页
launcher:x // 应用图标所处x位置(从左到右,从0开始),(-1是默认值:第一行或者第一列)
launcher:y // 应用图标所处y位置(从上往下,从0开始)
launcher:container // 定义一个快捷方式(Favorite)或桌面项目应该放置在哪个容器中;-101表示HotSeat、-100表示DeskTop、0表示默认的桌面容器、其他正整数表示App shortcut
launcher:spanX//在x方向上所占格数
launcher:spanY//在y方向上所占格数
这里特别说明下launcher:uri属性:uri 定义了控件点击时的链接操纵,通常以#Intent;...;end的格式编写,指定了一系列操纵(如打开某个应用的主界面)或数据类型(如打开图库应用或欣赏特定网站)。
// 下面列举几个常用launcher:uri的写法:
跳转到网页: "http://www.google.com"
跳转到设置的辅助功能:"#Intent;action=android.settings.ACCESSIBILITY_SETTINGS;end"
打开音乐文件:"file:///mnt/sdcard/song.mp3#Intent;action=android.intent.action.VIEW;type=audio/mp3;end"
指定应用程序打开音乐文件:"file:///mnt/sdcard/song.mp3#Intent;action=android.intent.action.VIEW;type=audio/mp3;component=com.android.music/.MusicBrowserActivity;end"
// 指定应用程序打开音乐文件,在Java中对应的操作如下
Intent it = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.fromFile(new File("/mnt/sdcard/song.mp3" ));
it.setDataAndType(uri, “audio/mp3”);
it.setClassName(“com.android.music”, “com.android.music.MusicBrowserActivity”);
String lancher_uri = it.toUri(0);
常用几种桌面控件的示例:
<?xml version="1.0" encoding="utf-8"?>
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!--定义一个HotSeat-->
<resolve
launcher:container="-101"
launcher:screen="0"
launcher:x="0"
launcher:y="5" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
<favorite launcher:uri="mailto:" />
</resolve>
<!--定义一个时钟小组件-->
<resolve>
<appwidget
launcher:screen="0"
launcher:x="2"
launcher:y="0"
launcher:spanX="3"
launcher:spanY="1"
launcher:packageName="com.google.android.deskclock"
launcher:className="com.android.alarmclock.DigitalAppWidgetProvider"/>
</resolve>
<!--定义一个谷歌搜索框-->
<resolve>
<search
launcher:screen="0"
launcher:x="1"
launcher:y="2"
launcher:spanX="5"
launcher:spanY="1"
launcher:packageName="com.google.android.googlequicksearchbox"
launcher:className="com.google.android.googlequicksearchbox.SearchWidgetProvider"/>
</resolve>
<!--定义一个文件夹,里面包含三个应用-->
<resolve>
<folder
launcher:title="@string/google_folder_title"
launcher:screen="0"
launcher:x="1"
launcher:y="3">
<favorite
launcher:packageName="com.google.android.googlequicksearchbox"
launcher:className="com.google.android.googlequicksearchbox.SearchActivity"/>
<favorite
launcher:packageName="com.android.chrome"
launcher:className="com.google.android.apps.chrome.Main"/>
<favorite
launcher:packageName="com.google.android.gm"
launcher:className="com.google.android.gm.ConversationListActivityGmail"/>
</folder>
</resolve>
<!--定义一个普通应用程序快捷方式-->
<resolve>
<favorite
launcher:screen="0"
launcher:x="2"
launcher:y="3"
launcher:packageName="com.android.vending"
launcher:className="com.android.vending.AssetBrowserActivity"/>
</resolve>
<!--定义一个shortcut链接-->
<resolve>
<shortcut
launcher:title="@string/google"
launcher:icon="@drawable/google"
launcher:uri="http://www.baidu.com"
launcher:screen="0"
launcher:x="3"
launcher:y="3" />
</resolve>
</favorites>
注意:这些属性并不都是resolve标签支持的,大多数的属性可以写在resolve标签中,也可以写在子标签中,但是对于uri、packageName、className等几个特别的属性,只能写在对应的子标签内。
除了上面的方式,还可以利用GMS中的配置文件来处置惩罚(条件是项目是有嵌入GMS框架)。在google_gms包下的配置文件release\vendor\partner_gms\apps\GmsSampleIntegration\res_dhs_full\xml\partner_default_layout.xml(下面是我项目中改的示例):
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 Google Inc. All Rights Reserved. -->
<favorites>
<!--定义应用文件夹-->
<folder title="@string/google_folder_title" screen="0" x="1" y="3">
<favorite packageName="com.google.android.googlequicksearchbox" className="com.google.android.googlequicksearchbox.SearchActivity"/>
<favorite packageName="com.android.chrome" className="com.google.android.apps.chrome.Main"/>
<favorite packageName="com.google.android.gm" className="com.google.android.gm.ConversationListActivityGmail"/>
<favorite packageName="com.google.android.apps.maps" className="com.google.android.maps.MapsActivity"/>
<favorite packageName="com.google.android.youtube" className="com.google.android.youtube.app.honeycomb.Shell$HomeActivity"/>
<favorite packageName="com.google.android.apps.docs" className="com.google.android.apps.docs.app.NewMainProxyActivity"/>
<favorite packageName="com.google.android.apps.youtube.music" className="com.google.android.apps.youtube.music.activities.MusicActivity"/>
<favorite packageName="com.google.android.videos" className="com.google.android.videos.GoogleTvEntryPoint"/>
<favorite packageName="com.google.android.apps.tachyon" className="com.google.android.apps.tachyon.MainActivity"/>
<favorite packageName="com.google.android.apps.photos" className="com.google.android.apps.photos.home.HomeActivity"/>
</folder>
<!--添加应用-->
<favorite screen="0" x="2" y="3" packageName="com.android.vending" className="com.android.vending.AssetBrowserActivity"/>
<!--添加组件-->
<appwidget screen="0" x="2" y="0" packageName="com.google.android.deskclock" className="com.android.alarmclock.DigitalAppWidgetProvider" spanX="3" spanY="2" />
<!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
<!-- 定义桌面的hotSeat显示的应用,本项目中已经设定不显示HotSeat,所以下面的代码设置也不起作用 -->
<favorite container="-101" screen="0" x="0" y="0" packageName="com.google.android.dialer" className="com.google.android.dialer.extensions.GoogleDialtactsActivity"/>
<favorite container="-101" screen="1" x="1" y="0" packageName="com.google.android.apps.messaging" className="com.google.android.apps.messaging.ui.ConversationListActivity"/>
<favorite container="-101" screen="0" x="0" y="0" packageName="com.android.settings" className="com.android.settings.Settings"/>
<favorite container="-101" screen="1" x="1" y="0" packageName="com.android.deskclock" className="com.android.deskclock.DeskClock"/>
<favorite container="-101" screen="2" x="2" y="0" packageName="com.google.android.calendar" className="com.android.calendar.event.LaunchInfoActivity"/>
<favorite container="-101" screen="3" x="3" y="0" packageName="com.google.android.contacts" className="com.android.contacts.activities.PeopleActivity"/>
<favorite container="-101" screen="4" x="4" y="0" packageName="com.android.camera2" className="com.android.camera.CameraLauncher"/>
</favorites>
上面文件有几个属性参数解析一下:
[*]folder标签表示文件夹;
[*]favorite标签表示应用;
[*]appwidget表示组件;
[*]container表示放置的位置或区域,-101表示hotseat;-100代表是desktop;container=正数,则代表App shortcut;
[*]screen表示位于第几页屏幕,0表示第一页;
[*]x表示x轴方向的位置(从左到右,从0开始);
[*]y表示y轴方向的位置(从上往下,从0开始);
[*]packageName表示应用或组件的包名;
[*]className表示应用或组件对应的Activity或AppWidgetProvider;
[*]spanX表示x轴方向占用的格子数;
[*]spanY表示y轴方向占用的格子数
在partner_default_layout.xml中改动效果如下:
https://i-blog.csdnimg.cn/direct/5ba13807f47049018aa7f66682844fbf.png
注意:如果你修改了结构文件,但是重新编译后运行模仿器,发现桌面应用并没有改变,有可能是launcher3的database没有更新,因为这些结构文件的内容,在launcher第一次开机启动时会创建database并生存数据到其中,后续有利用直接从db读取,而不消重复读结构文件
# 恢复默认布局
adb root
adb shell rm /data/data/com.android.launcher3/databases/launcher.db
# 实测我这里使用的是6x5布局,所以db文件是:launcher_6_by_5.db
# 重启模拟器即可生效
修改谷歌搜索框
如果要修改搜索框占用格子的宽度,可以在Launcher3\res\xml\device_profiles.xml文件内里,对应的尺寸结构中修改numSearchContainerColumns的值:
<?xml version="1.0" encoding="utf-8"?>
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
<grid-option
launcher:name="6_by_5"
launcher:numRows="6"
launcher:numColumns="7"
launcher:numSearchContainerColumns="5"
.../>
/>
我在6x5的结构(现实上是后7x6)中将谷歌搜索框的宽度改成了5,即在x轴方向上,占用五个格子。
接下来是修改谷歌搜索框的位置:在workspace.java的bindAndInitFirstWorkspaceScreen()方法中:
// Launcher3/src/com/android/launcher3/Workspace.java
/**
* Initializes and binds the first page
*/
public void bindAndInitFirstWorkspaceScreen() {
if (!FeatureFlags.QSB_ON_FIRST_SCREEN) {
return;
}
// Add the first page
CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, getChildCount());
// Always add a first page pinned widget on the first screen.
if (mFirstPagePinnedItem == null) {
mFirstPagePinnedItem = LayoutInflater.from(getContext())
.inflate(R.layout.search_container_workspace, firstPage, false);
}
// 先获取device_profiles.xml中定义的numSearchContainerColumns值
int cellHSpan = mLauncher.getDeviceProfile().inv.numSearchContainerColumns;
// 在这里修改搜索框的位置,五个参数分别代表:x坐标、y坐标,占用宽度、占用高度、屏幕id
// 所以这里效果是:在第一个CellLayout的第二列第三行(xy都是从0开始算的)添加谷歌搜索框,宽度为5个格子,高度为1个格子
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(1, 2, cellHSpan, 1, FIRST_SCREEN_ID);
lp.canReorder = false;
// 将谷歌搜索框添加到布局中,最后一个参数是代表是否可移动的意思
if (!firstPage.addViewToCellLayout(
mFirstPagePinnedItem, 0, R.id.search_container_workspace, lp, true)) {
Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
mFirstPagePinnedItem = null;
}
}
另外:还要在LoaderCursor.java的checkItemPlacement()方法中修改搜索框对位置的占用效果:
// Launcher3/src/com/android/launcher3/model/LoaderCursor.java
/**
* check & update map of what's occupied; used to discard overlapping/invalid items
*/
protected boolean checkItemPlacement(ItemInfo item) {
int containerIndex = item.screenId;
// ...
if (!occupied.containsKey(item.screenId)) {
GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
if (item.screenId == Workspace.FIRST_SCREEN_ID && FeatureFlags.QSB_ON_FIRST_SCREEN) {
int spanX = mIDP.numSearchContainerColumns;
int spanY = 1;
// 这里需要跟WorkSpace.java中设置的搜索框位置保持一致
screen.markCells(1, 2, spanX, spanY, true);
}
occupied.put(item.screenId, screen);
}
final GridOccupancy occupancy = occupied.get(item.screenId);
// Check if any workspace icons overlap with each other
if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
occupancy.markCells(item, true);
return true;
} else {
Log.e(TAG, "Error loading shortcut " + item
+ " into cell (" + containerIndex + "-" + item.screenId + ":"
+ item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
+ ") already occupied");
return false;
}
}
如果只修改WorkSpace、而没有修改LoaderCursor中的代码,那么搜索框原本的结构(默认位置是x=0,y=0)占用效果会一直存在,导致背面一些应用或者组件无法放置在首页的首行位置。
注意:Workspace.java的bindAndInitFirstWorkspaceScreen方法不但是设定谷歌搜索框位置的关键代码,也是控制了谷歌搜索框是否显示,如果不需要显示谷歌搜索框,可以将上面代码注释:
// Launcher3/src/com/android/launcher3/Workspace.java
/**
* Initializes and binds the first page
*/
public void bindAndInitFirstWorkspaceScreen() {
if (!FeatureFlags.QSB_ON_FIRST_SCREEN) {
return;
}
// 注意要保留这一行,否则会报错
CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, getChildCount());
// if (mFirstPagePinnedItem == null) {
// mFirstPagePinnedItem = LayoutInflater.from(getContext())
// .inflate(R.layout.search_container_workspace, firstPage, false);
// }
//
// int cellHSpan = mLauncher.getDeviceProfile().inv.numSearchContainerColumns;
// CellLayoutLayoutParams lp = new CellLayoutLayoutParams(1, 2, cellHSpan, 1, FIRST_SCREEN_ID);
// lp.canReorder = false;
// if (!firstPage.addViewToCellLayout(
// mFirstPagePinnedItem, 0, R.id.search_container_workspace, lp, true)) {
// Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
// mFirstPagePinnedItem = null;
// }
}
除了通过Java文件配置谷歌搜索框之外,也可以通过XML文件来配置谷歌搜索框,主要是在default_workspace.xml或partner_default_layout.xml中进行配置。如果项目有配置GMS框架,那么会读取partner_default_layout.xml文件中的配置,否则会读取default_workspace.xml的配置。
先在device_profiles.xml文件内里定义桌面的行列数:
<?xml version="1.0" encoding="utf-8"?>
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
<grid-option
launcher:name="6_by_5"
launcher:numRows="6"
launcher:numColumns="7"
launcher:numSearchContainerColumns="5"
.../>
/>
这里定义的app分布结构是六行七列。然后根据系统是否会读取partner_default_layout.xml结构文件来决定修改的位置:
注意:利用xml配置时,需要先将Java代码中配置谷歌搜索框的代码(Workspace.java和LoaderCursor.java)注释掉,避免相互影响。
// Launcher3/src/com/android/launcher3/Workspace.java
public void bindAndInitFirstWorkspaceScreen() {
if (!FeatureFlags.QSB_ON_FIRST_SCREEN) {
return;
}
// 注意要保留这一行,否则会报错
CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, getChildCount());
// if (mFirstPagePinnedItem == null) {
// mFirstPagePinnedItem = LayoutInflater.from(getContext())
// .inflate(R.layout.search_container_workspace, firstPage, false);
// }
//
// int cellHSpan = mLauncher.getDeviceProfile().inv.numSearchContainerColumns;
// CellLayoutLayoutParams lp = new CellLayoutLayoutParams(1, 2, cellHSpan, 1, FIRST_SCREEN_ID);
// lp.canReorder = false;
// if (!firstPage.addViewToCellLayout(
// mFirstPagePinnedItem, 0, R.id.search_container_workspace, lp, true)) {
// Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
// mFirstPagePinnedItem = null;
// }
}
// Launcher3/src/com/android/launcher3/model/LoaderCursor.java
protected boolean checkItemPlacement(ItemInfo item) {
int containerIndex = item.screenId;
// ...
if (!occupied.containsKey(item.screenId)) {
GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
// 注释下面if中的内容
// if (item.screenId == Workspace.FIRST_SCREEN_ID && FeatureFlags.QSB_ON_FIRST_SCREEN) {
// // Mark the first X columns (X is width of the search container) in the first row as
// // occupied (if the feature is enabled) in order to account for the search
// // container.
// int spanX = mIDP.numSearchContainerColumns;
// int spanY = 1;
// screen.markCells(1, 2, spanX, spanY, true);
// }
occupied.put(item.screenId, screen);
}
final GridOccupancy occupancy = occupied.get(item.screenId);
// Check if any workspace icons overlap with each other
if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
occupancy.markCells(item, true);
return true;
} else {
Log.e(TAG, "Error loading shortcut " + item
+ " into cell (" + containerIndex + "-" + item.screenId + ":"
+ item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
+ ") already occupied");
return false;
}
}
(1)如果适配了GMS框架并读取partner_default_layout.xml结构文件,则在google_gms包下的release\vendor\partner_gms\apps\GmsSampleIntegration\res_dhs_full\xml\partner_default_layout.xml内里定义应用的包名和类型以及位置信息,比方
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 Google Inc. All Rights Reserved. -->
<favorites>
<!--其他配置-->
<appwidget screen="0" x="1" y="2" packageName="com.google.android.googlequicksearchbox" className="com.android.alarmclock.DigitalAppWidgetProvider" spanX="5" spanY="1" />
</favorites>
(2)如果不读取partner_default_layout.xml结构文件,则在对应的default_workspace_MxN.xml内里修改应用的位置(MN分别代表行列数):
<?xml version="1.0" encoding="utf-8"?>
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!--其他配置-->
<resolve>
<search
launcher:screen="0"
launcher:x="1"
launcher:y="2"
launcher:spanX="5"
launcher:spanY="1"
launcher:packageName="com.google.android.googlequicksearchbox"
launcher:className="com.google.android.googlequicksearchbox.SearchWidgetProvider"/>
</resolve>
</favorites>
通过XML配置出来的效果和在Java代码中配置的一样,这里就不贴图了。
修改主页应用、组件
和谷歌搜索框的改动一样,也是先在device_profiles.xml文件内里定义桌面的行列数:
<?xml version="1.0" encoding="utf-8"?>
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
<grid-option
launcher:name="6_by_5"
launcher:numRows="6"
launcher:numColumns="7"
launcher:numSearchContainerColumns="5"
.../>
/>
这里定义的app分布结构是六行七列。然后根据系统是否会读取partner_default_layout.xml结构文件来决定修改的位置:
(1)如果读取partner_default_layout.xml结构文件,则在partner_default_layout.xml内里定义应用的包名和类型以及位置信息,比方
<favorite screen="0" x="2" y="3" packageName="com.android.vending" className="com.android.vending.AssetBrowserActivity"/>
<!--表示在screen=“0”(第一屏)的第3列格子,第4行格子添加谷歌商店app。-->
(2)如果不读取partner_default_layout.xml结构文件,则在对应的default_workspace_MxN.xml内里修改应用的位置(MN分别代表行列数):
<!--定义一个普通应用程序快捷方式-->
<resolve>
<favorite
launcher:screen="0"
launcher:x="2"
launcher:y="3"
launcher:packageName="com.android.vending"
launcher:className="com.android.vending.AssetBrowserActivity"/>
</resolve>
由于个人能力有限,上面的分析和讲解不免有错漏之处,接待各位批评指正;接待大家相互互换学习,一起进步,共勉!!!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]