MTKLauncher_布局页面分析


前言

第一次拿着开发板,想研究一下Launcher3源码,第一次接触Launcher3,一步一步看看Launcher3 定制的话有哪些内容需要掌握的。

遇到的困难点

  • 拿到源码从何看起,从何研究起。
  • 源码无论在线的谷歌源码还是各大半导体厂商提供的Launcher3源码,存在一定的差别。
    需要针对性看,特别是针对自己的开发板上面看尤其重要,不然对不上。
  • 部分代码看不明白,布局和UI如何对上的。
  • 源码到底在哪里,配置文件、布局文件到底是哪一个,对不上,琢磨实验好久。

针对性解决困难

  • 多从广义角度、全局角度来看Launcher3,不要一下子钻进了牛角尖 出不来,毫无收获
  • 多看一看网上在线的第三方博客、别人的总结、比人的分析,从中自己体会,理解
  • 多打印日志,调试;在源码里面搜索关键字、关键文件路径
  • 不管你是开发板或者公司产品,务必先针对一款源码熟悉、了解、分析,尽量不要好多平台源码一起看,源码部分架构不一样的,代码也不一样的。
  • 多啃、多吃啃源码还是有必要的

需求

remind:初识Launcher首页布局是怎么加载的

首页页面是如何加载的,如何配置,如下图界面我想更改一下每个图标的位置,我想自定义这个界面如何实现?

在这里插入图片描述

比如如下看网上别人定制的桌面,蛮好看的,如下图:
在这里插入图片描述

相关资料

Launcher3 相关资料参考
菜鸟成长之路-源码分析专栏
Android Launcher3 简介
Launcher3 高端定制
Launcher3 开发
Launcher3 Android Code Search在线源码查看
Launcher3 xref 在线源码查看
Launcher3 RK 源码查看
Launcher3 解析
Launcher3 AndroidP AS版本
谷歌Launcher3 Android13源码修改
Launcher3 和 Launcher3QuickStep 区别
Android14 不分Launcher3修改
Launcher3 LoaderTask 的数据加载
Android14 浅析Launcher
Android O Launcher3-Workspace加载

Launcher3 源码 目录简单介绍

当拿到Launcher3 源码时候,对源码还是一脸懵逼, 用了这么久的手机,源码不熟悉 也正常,先有个大概了解

allapps 目录:主要存放主菜单界面相关代码。
anim目录:存放动画相关代码,主要是动画基类代码。
compat目录:主要存放解决兼容性相关的代码。
config目录:主要配置Launcher相关功能的宏开关,目前Launcher原生新增的功能宏开关都在这个目录。
dragndrop目录:主要存放拖拽相关操作的代码
graphics目录:主要存放处理图标大小、颜色、自适应等相关的代码
model目录:存放Launcher加载流程相关模块化的代码
notification目录:存放通知相关的代码
pageindicators目录:存放桌面页面指示器相关的代码
popu目录:存放长按图标显示弹出框相关的代码
provider目录:存放Launcher数据库相关的代码
qsb目录:存放搜索功能相关的代码
shortcuts目录:存放桌面所属应用某些功能的快捷图标相关的代码。

Launcher3 简介及页面布局分析

回归到需求,我们需要了解的是布局相关,那还是从整体架构来看看 Launcher3

UI整体架构

在这里插入图片描述
在这里插入图片描述

UI 架构是我们比较熟悉的内容,用了这么多年的手机,手机桌面部分不就是这些内容的嘛,从研发的角度讲我们可以和对应的名称 关联起来。

数据加载

数据加载是Launcher3中一个比较核心的内容,后续需要深入了解,下面给一下加载流程图,后续再继续分析,在看源码过程中提供一个源码查看方向,针对本文就此打住,需要了解 非深入研究部分
在这里插入图片描述

布局加载

本身我们通过Launcher3 找到主Activity,主Activity里面再找对应的layout 布局,launcher.xml,这些和上面的UI架构对应 对应的是模块,非本文核心问题。我们的核心问题是找桌面的那些Icon 文件夹 快捷 搜索栏 UI和布局及数据如何展现的,这些其实是配置或者在源码里面硬编码更改。

布局加载核心思想

我自己开发过程中,看 device_profiles.xml 时候,一脸懵,不清楚每个字段 比如 grid-option 、display-option、numRows、numColumns、numFolderRows、numFolderColumns、numHotseatIcons、minWidthDps、minHeightDps、iconImageSize、iconTextSize… 到底啥玩意 这么多配置,后来想一想见名知意。 就是显示的属性和配置呀,都是见名知意。

launcher:defaultLayoutId="@xml/default_workspace_5x5"
launcher:defaultLayoutId="@xml/default_workspace_4x4" 
launcher:defaultLayoutId="@xml/default_workspace_3x3"

布局里面好多 grid-option 、display-option,到底用哪一个对应的 default_workspace_MxN

核心思想:

  • 动态选择,更具屏幕大小分辨率动态适配加载选择
  • 更具横竖屏动态选择

device_profiles.xml 加载

我们先给一个简单的流程图
在这里插入图片描述

device_profiles.xml 文件用于定义不同设备配置的布局,该文件是启动器根据设备的特性(如屏幕尺寸、分辨率、密度等)来适配布局和图标大小等元素的重要配置文件。

该文件的主要功能有定义网格布局、设置图标大小、配置热区、定义所有应用列表的布局、屏幕和设备类型特定配置、壁纸和背景设置、提供默认布局等。


<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" >

        <display-option
            launcher:name="Super Short Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="300"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Shorter Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="400"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

    </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:dbFile="launcher_4_by_4.db"
        launcher:defaultLayoutId="@xml/default_workspace_4x4" >

        <display-option
            launcher:name="Short Stubby"
            launcher:minWidthDps="275"
            launcher:minHeightDps="420"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="450"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Nexus S"
            launcher:minWidthDps="296"
            launcher:minHeightDps="491.33"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Nexus 4"
            launcher:minWidthDps="359"
            launcher:minHeightDps="567"
            launcher:iconImageSize="54"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Nexus 5"
            launcher:minWidthDps="335"
            launcher:minHeightDps="567"
            launcher:iconImageSize="54"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

    </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:dbFile="launcher.db"
        launcher:defaultLayoutId="@xml/default_workspace_5x5" >

        <display-option
            launcher:name="Large Phone"
            launcher:minWidthDps="406"
            launcher:minHeightDps="694"
            launcher:iconImageSize="56"
            launcher:iconTextSize="14.4"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Large Phone Split Display"
            launcher:minWidthDps="406"
            launcher:minHeightDps="694"
            launcher:iconImageSize="56"
            launcher:iconTextSize="14.4"
            launcher:canBeDefault="split_display" />

        <display-option
            launcher:name="Shorter Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="400"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

    </grid-option>

InvariantDeviceProfile

初始化地方

 Launcher.java
 
 InvariantDeviceProfile idp = app.getInvariantDeviceProfile();

LauncherAppState.java

public InvariantDeviceProfile getInvariantDeviceProfile() {
          return mInvariantDeviceProfile;
     }
     

LauncherAppState 构造方法
 mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);

InvariantDeviceProfile.java    构造方法
 @TargetApi(23)
      private InvariantDeviceProfile(Context context) {
          String gridName = getCurrentGridName(context);
          String newGridName = initGrid(context, gridName);
....
      }
initGrid(context, gridName)

大家这么想,为啥是Grid? init Grid 是做什么的。 其实桌面中WorkSpack 中的CellLayout, 不就是由网格组成的嘛, 然后给对应的坐标,告诉放到哪一个位置不就可以了嘛。 所以这个名字 Grid 是很有意义的。

   //初始化网格
   private String initGrid(Context context, String gridName) {
      ......
        // getPredefinedDeviceProfiles  获取预定义的文件配置列表
        ArrayList<DisplayOption> allOptions =
                getPredefinedDeviceProfiles(context, gridName, isSplitDisplay);
        DisplayOption displayOption =
                invDistWeightedInterpolate(displayInfo, allOptions, isSplitDisplay);
		Log.d(TAG," initGrid  gridName:"+displayOption.grid.name);		
        initGrid(context, displayInfo, displayOption, isSplitDisplay);
        return displayOption.grid.name;
    }
getPredefinedDeviceProfiles

getPredefinedDeviceProfiles 获取预定义的文件配置列表

  private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(
            Context context, String gridName, boolean isSplitDisplay) {
        ArrayList<DisplayOption> profiles = new ArrayList<>();
		Log.d(TAG,"getPredefinedDeviceProfiles   huoqu yudingyi device list gridName:"+gridName);
        try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
            final int depth = parser.getDepth();
            int type;
            while (((type = parser.next()) != XmlPullParser.END_TAG ||
                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
						Log.d(TAG,"getPredefinedDeviceProfiles   GridOption.TAG_NAME:"+GridOption.TAG_NAME+"   parser.getName:"+parser.getName());
                if ((type == XmlPullParser.START_TAG)
                        && GridOption.TAG_NAME.equals(parser.getName())) {

                    GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser));
                    final int displayDepth = parser.getDepth();
                    while (((type = parser.next()) != XmlPullParser.END_TAG ||
                            parser.getDepth() > displayDepth)
                            && type != XmlPullParser.END_DOCUMENT) {
                        if ((type == XmlPullParser.START_TAG) && "display-option".equals(
                                parser.getName())) {
									Log.d(TAG,"getPredefinedDeviceProfiles  display-option  parser.getName:"+parser.getName());
                            profiles.add(new DisplayOption(gridOption, context,
                                    Xml.asAttributeSet(parser),
                                    isSplitDisplay ? DEFAULT_SPLIT_DISPLAY : DEFAULT_TRUE));
                        }
                    }
                }
            }
        } catch (IOException|XmlPullParserException e) {
            throw new RuntimeException(e);
        }

        ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
        if (!TextUtils.isEmpty(gridName)) {
            for (DisplayOption option : profiles) {
                if (gridName.equals(option.grid.name)) {
                    filteredProfiles.add(option);
                }
            }
        }
		Log.d(TAG,"getPredefinedDeviceProfiles  000 profiles:"+profiles.size()+"   filteredProfiles:"+filteredProfiles.size() );
        if (filteredProfiles.isEmpty()) {
            // No grid found, use the default options
            for (DisplayOption option : profiles) {
                if (option.canBeDefault) {
                    filteredProfiles.add(option);
                }
            }
        }
	   Log.d(TAG,"getPredefinedDeviceProfiles 111 profiles:"+profiles.size()+"   filteredProfiles:"+filteredProfiles.size() );
        if (filteredProfiles.isEmpty()) {
            throw new RuntimeException("No display option with canBeDefault=true");
        }
        return filteredProfiles;
    }

这个方法比较核心,关注三点:

  • 加载R.xml.device_profiles 文件,并解析
  • DisplayOption类和device_profiles 里面的display-option
    属性,不就对上了嘛,得到一个displayOption 配置列表
  • 解析device_profiles,将grid-option 节点想的一级属性信息,封装在了DisplayOption,这样实现了
    通过displayOption 能够获取 配置文件中上一层的grid 信息。 比如获取gridname,
    也就是接下来要讲的MxN.xml

在这里插入图片描述

invDistWeightedInterpolate

接着上面的 getPredefinedDeviceProfiles 方法讲,这个方法返回的是List 集合

ArrayList<DisplayOption> getPredefinedDeviceProfiles

那么为什么会返回一个集合? 我们通过 上面分析 device_profiles.xml 配置文件中,grid-option 节点下对应的是多个display-option的。 比如如下日志,可以说明问题:
在这里插入图片描述
在这里插入图片描述

返回了DisplayOption 集合后,如何选择其一适合自己的呢? invDistWeightedInterpolate 就派上用场了

private static DisplayOption invDistWeightedInterpolate(
            Info displayInfo, ArrayList<DisplayOption> points, boolean isSplitDisplay) {
        int minWidthPx = Integer.MAX_VALUE;
        int minHeightPx = Integer.MAX_VALUE;
        for (WindowBounds bounds : displayInfo.supportedBounds) {
            boolean isTablet = displayInfo.isTablet(bounds);
            if (isTablet && isSplitDisplay) {
                // For split displays, take half width per page
                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
            } else if (!isTablet && bounds.isLandscape()) {
                // We will use transposed layout in this case
                minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
                minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
            } else {
                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
            }
        }
        float width = dpiFromPx(minWidthPx, displayInfo.densityDpi);
        float height = dpiFromPx(minHeightPx, displayInfo.densityDpi);
        // Sort the profiles based on the closeness to the device size
        Collections.sort(points, (a, b) ->
                Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
                        dist(width, height, b.minWidthDps, b.minHeightDps)));
        GridOption closestOption = points.get(0).grid;
        float weights = 0;
        DisplayOption p = points.get(0);
        if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
            return p;
        }
        DisplayOption out = new DisplayOption(closestOption);
        for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
            p = points.get(i);
            float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
            weights += w;
            out.add(new DisplayOption().add(p).multiply(w));
        }
        return out.multiply(1.0f / weights);
    }

如则经过一系列的计算,来得到与当前屏幕可用宽高最为合适的各参数大小。具体算法这边就不细究了。

initGrid(Context context, Info displayInfo, DisplayOption displayOption,boolean isSplitDisplay)

得到了gridName,DisplayOption 不就可以获取得到 快捷方式、文件夹、图标等一些列的参数了嘛,且看 源码。

 private void initGrid(
            Context context, Info displayInfo, DisplayOption displayOption,
            boolean isSplitDisplay) {
		Log.d(TAG,"initGrid...");		
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        GridOption closestProfile = displayOption.grid;
        numRows = closestProfile.numRows;
        numColumns = closestProfile.numColumns;
        dbFile = closestProfile.dbFile;
        defaultLayoutId = closestProfile.defaultLayoutId;
        demoModeLayoutId = closestProfile.demoModeLayoutId;
        numFolderRows = closestProfile.numFolderRows;
        numFolderColumns = closestProfile.numFolderColumns;
        isScalable = closestProfile.isScalable;
        devicePaddingId = closestProfile.devicePaddingId;

        mExtraAttrs = closestProfile.extraAttrs;

        iconSize = displayOption.iconSize;
        landscapeIconSize = displayOption.landscapeIconSize;
        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
        iconTextSize = displayOption.iconTextSize;
        landscapeIconTextSize = displayOption.landscapeIconTextSize;
        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);

        minCellHeight = displayOption.minCellHeight;
        minCellWidth = displayOption.minCellWidth;
        borderSpacing = displayOption.borderSpacing;
		  Log.d(TAG,"initGrid displayOption   iconSize:"+iconSize+"  landscapeIconSize:"+landscapeIconSize+"  iconTextSize:"+iconTextSize+"  landscapeIconTextSize:"+landscapeIconTextSize);
		Log.d(TAG,"initGrid displayOption   minCellHeight:"+minCellHeight+"  minCellWidth:"+minCellWidth+"   borderSpacing:"+borderSpacing+"   ");

        numShownHotseatIcons = closestProfile.numHotseatIcons;
        numDatabaseHotseatIcons = isSplitDisplay
                ? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons;

        numAllAppsColumns = closestProfile.numAllAppsColumns;
        numDatabaseAllAppsColumns = isSplitDisplay
                ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns;
         Log.d(TAG,"initGrid closestProfile  numRows:"+numRows+"  numColumns:"+numColumns+"   dbFile:"+dbFile+"   defaultLayoutId:"+defaultLayoutId+"   demoModeLayoutId:"+demoModeLayoutId);
         Log.d(TAG,"initGrid closestProfile  numFolderRows:"+numFolderRows+"   numFolderColumns:"+numFolderColumns+"   isScalable:"+isScalable+"  devicePaddingId:"+devicePaddingId);
         Log.d(TAG,"initGrid closestProfile  numShownHotseatIcons:"+numShownHotseatIcons+"   numDatabaseHotseatIcons:"+numDatabaseHotseatIcons);

        if (Utilities.isGridOptionsEnabled(context)) {
            allAppsIconSize = displayOption.allAppsIconSize;
            allAppsIconTextSize = displayOption.allAppsIconTextSize;
        } else {
            allAppsIconSize = iconSize;
            allAppsIconTextSize = iconTextSize;
        }

        if (devicePaddingId != 0) {
            devicePaddings = new DevicePaddings(context, devicePaddingId);
        }

        // If the partner customization apk contains any grid overrides, apply them
        // Supported overrides: numRows, numColumns, iconSize
        applyPartnerDeviceProfileOverrides(context, metrics);

        final List<DeviceProfile> localSupportedProfiles = new ArrayList<>();
        defaultWallpaperSize = new Point(displayInfo.currentSize);
        for (WindowBounds bounds : displayInfo.supportedBounds) {
            localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
                    .setUseTwoPanels(isSplitDisplay)
                    .setWindowBounds(bounds).build());

            // Wallpaper size should be the maximum of the all possible sizes Launcher expects
            int displayWidth = bounds.bounds.width();
            int displayHeight = bounds.bounds.height();
            defaultWallpaperSize.y = Math.max(defaultWallpaperSize.y, displayHeight);

            // We need to ensure that there is enough extra space in the wallpaper
            // for the intended parallax effects
            float parallaxFactor =
                    dpiFromPx(Math.min(displayWidth, displayHeight), displayInfo.densityDpi) < 720
                            ? 2
                            : wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight);
            defaultWallpaperSize.x =
                    Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth));
        }
        supportedProfiles = Collections.unmodifiableList(localSupportedProfiles);

        ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
        defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
    }

部分日志如下:

initGrid displayOption   iconSize:55.69532  landscapeIconSize:55.69532  iconTextSize:14.34668  landscapeIconTextSize:14.34668
initGrid displayOption   minCellHeight:0.0  minCellWidth:0.0   borderSpacing:0.0   
initGrid closestProfile  numRows:5  numColumns:5   dbFile:launcher.db   defaultLayoutId:2131951619   demoModeLayoutId:2131951619
initGrid closestProfile  numFolderRows:4   numFolderColumns:4   isScalable:false  devicePaddingId:0
initGrid closestProfile  numShownHotseatIcons:5   numDatabaseHotseatIcons:5
InvariantDeviceProfile  gridName:5_by_5  newGridName:5_by_5

default_workspace_MxN.xml

经过上面的分析,其实已经找到了GridName,如当前 调试是5_by_5。获取后保存一份,下次获取。每次也要获取一份新的,在上面分析中 筛选DisPlayOption 里面

String newGridName = initGrid(context, gridName);

 private String initGrid(Context context, String gridName) {
         ...
        DisplayOption displayOption =
                invDistWeightedInterpolate(displayInfo, allOptions, isSplitDisplay);
		Log.d(TAG," initGrid  gridName:"+displayOption.grid.name);		
        initGrid(context, displayInfo, displayOption, isSplitDisplay);
        return displayOption.grid.name;
    }

解析文件要解析道德其实是 launcher:defaultLayoutId="@xml/default_workspace_5x5" 
defaultLayoutId 属性, 在initGrid 中解析到defaultLayoutId ,然后找到对应的布局

<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
  <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
  <!-- Dialer Messaging [All Apps] Contacts Camera -->
  <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="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.mediatek.camera" className="com.mediatek.camera.CameraLauncher"/>
  <!-- In Launcher3, workspaces extend infinitely to the right, incrementing from zero -->
  <!-- Google folder -->
  <!-- Google, Chrome, Gmail, Maps, YouTube, (Drive), (Music), (Movies), Hangouts, Photos -->
  <folder title="@string/google_folder_title" screen="0" x="0" y="4">
    <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"/>
    <favorite packageName="com.google.android.apps.adm" className="com.google.android.apps.adm.activities.MainActivity"/>
  </folder>
  <favorite screen="0" x="2" y="4" packageName="com.google.android.apps.googleassistant" className="com.google.android.apps.googleassistant.AssistantActivity"/>
  <favorite screen="0" x="4" y="4" packageName="com.android.vending" className="com.android.vending.AssetBrowserActivity"/>
</favorites>
查找资源文件 default_workspace_MxN.xml 位置

找到 default_workspace_5x5.xml 在哪里呢? 我用的是mtk 平台,搜索文件名如下:
在这里插入图片描述
你会发现好多个呀,下面给出具体位置:

MTK 平台GMS版本
\vendor\google\overlay\gms_overlay\vendor\google\apps\SearchLauncher\res\xml\default_workspace_5x5.xml

MTK Launcher3 源码位置

我在MTK 平台上面测试验证,GMS 版本下:
Launcher3 源码存在两份,分别位于 package/app/和vendor/mediatek/proprietary/packages/apps/下,当前调试源码位置:

packages\apps\Launcher3

需求实现

上面我们已经找到了 default_workspace_5x5.xml ,首页的配置就在上面 ,我们更改配置即可,具体更改 每个 字段含有,见名知意的。 可以自己实验。
比如,我自己更改如下,实际效果如下

  <favorite screen="0" x="2" y="4" packageName="com.google.android.apps.googleassistant" className="com.google.android.apps.googleassistant.AssistantActivity"/>

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

野火少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值