飞不高 发表于 2024-10-25 11:44:49

安卓开发- 安卓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]
查看完整版本: 安卓开发- 安卓13 Launcher3 主页结构修改