安卓开发- 安卓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主页的构造图,以便我们了解各个子布局的相关内容:

在这里插入图片描述

  • 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的布局和各个组件的位置。DeviceProfileLauncher启动时被创建,并根据设备的实际配置(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:inlineNavButtonsEndSpacinglauncher:devicePaddingId: 分别定义了内联导航按钮的结束间距和设备的填充空间ID。
  • launcher:dbFilelauncher:defaultLayoutId: 数据库文件名和默认布局ID,用于存储和恢复布局状态。
  • launcher:deviceCategory="tablet": 设备类别,这里是平板电脑。

接下来的 <display-option> 标签提供了更多细节:

  • launcher:minWidthDps="900"launcher:minHeightDps="820": 最小宽度和高度(以密度无关像素dp为单位),用于确定此布局适用于哪种屏幕尺寸。
  • launcher:minCellHeightlauncher:minCellWidth: 单个单元格的最小高度和宽度。
  • 后面的属性,如 launcher:iconImageSizelauncher:iconTextSize 等,分别定义了图标、文本、边框空间等元素的尺寸和间距。
  • launcher:horizontalMarginlauncher:horizontalMarginLandscape: 水平方向上的外边距,在竖屏和横屏模式下可能不同。
  • launcher:hotseatBarBottomSpacelauncher: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中改动效果如下:

在这里插入图片描述

注意:如果你修改了布局文件,但是重新编译后运行模拟器,发现桌面应用并没有改变,有可能是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.javabindAndInitFirstWorkspaceScreen()方法中:

// 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.xmlpartner_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>

由于个人能力有限,上面的分析和讲解难免有错漏之处,欢迎各位批评指正;欢迎大家相互交流学习,一起进步,共勉!!!

  • 34
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值