android M Launcher之数据库实现

本文详细解析了Launcher数据库的实现过程,包括数据库实体SQLiteOpenHelper的具体实现,以及ContentProvider如何封装数据库操作,提供跨进程访问。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面一系列文章我们分析了LauncherModel的工作过程,它会把数据绑定到桌面上。从今天开始我们来分析下Launcher的数据来源即Launcher数据库的实现。

一个完整的数据库实现都应该包括两方面的内容,第一是数据库实体SQLiteOpenHelper的实现,第二是数据库ContentProvider的实现。数据库的实体包含了数据库实体以及相关的操作,ContentProvider负责数据库内容的访问接口实现。

1、Launcher数据库的实现

  private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
        private final Context mContext;
        @Thunk final AppWidgetHost mAppWidgetHost;
        private long mMaxItemId = -1;
        private long mMaxScreenId = -1;

        private boolean mNewDbCreated = false;

        @Thunk LauncherProviderChangeListener mListener;

        DatabaseHelper(Context context) {
            super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
            mContext = context;
            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);

            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
            // the DB here
            if (mMaxItemId == -1) {
                mMaxItemId = initializeMaxItemId(getWritableDatabase());
            }
            if (mMaxScreenId == -1) {
                mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
            }
        }
        ...
     }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

相信大家对SQLiteOpenHelper 的构造方法都比较了解我们主要看下Launcher相关的

    if (mMaxItemId == -1) {
        mMaxItemId = initializeMaxItemId(getWritableDatabase());
      }
    if (mMaxScreenId == -1) {
         mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
    }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

通常每一个数据库表都包含一个可以自增长的id字段,但Launcher比较特殊,id字段只作为数据库表的主键存在,因此,我们每一次在桌面上增加一个组件,都需要在当前最大的id号上加1,以做新的id号这就是mMaxItemId 。

在Launcher3之前。Launcher的桌面页数是固定的,随着Launcher3的到来,桌面的页数已经修改为可以动态增加了。如果当前桌面上已经没有额外的空间来加载新增的桌面组件,Launcher3将会根据当前最大的桌面页ID再增加一个桌面页。initializeMaxScreenId方法就是用来获取最大桌面页数的。

上面我们在构造函数中创建了Launcher的数据库,下面我们将在oncreate中创建表。代码如下:

@Override
        public void onCreate(SQLiteDatabase db) {
            if (LOGD) Log.d(TAG, "creating new launcher database");

            mMaxItemId = 1;
            mMaxScreenId = 0;
            mNewDbCreated = true;

            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
            long userSerialNumber = userManager.getSerialNumberForUser(
                    UserHandleCompat.myUserHandle());

            db.execSQL("CREATE TABLE favorites (" +
                    "_id INTEGER PRIMARY KEY," +
                    "title TEXT," +
                    "intent TEXT," +
                    "container INTEGER," +
                    "screen INTEGER," +
                    "cellX INTEGER," +
                    "cellY INTEGER," +
                    "spanX INTEGER," +
                    "spanY INTEGER," +
                    "itemType INTEGER," +
                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
                    "isShortcut INTEGER," +
                    "iconType INTEGER," +
                    "iconPackage TEXT," +
                    "iconResource TEXT," +
                    "icon BLOB," +
                    "uri TEXT," +
                    "displayMode INTEGER," +
                    "appWidgetProvider TEXT," +
                    "modified INTEGER NOT NULL DEFAULT 0," +
                    "restored INTEGER NOT NULL DEFAULT 0," +
                    "profileId INTEGER DEFAULT " + userSerialNumber + "," +
                    "rank INTEGER NOT NULL DEFAULT 0," +
                    "options INTEGER NOT NULL DEFAULT 0" +
                    ");");
            addWorkspacesTable(db);

            // Database was just created, so wipe any previous widgets
            if (mAppWidgetHost != null) {
                mAppWidgetHost.deleteHost();

                /**
                 * Send notification that we've deleted the {@link AppWidgetHost},
                 * probably as part of the initial database creation. The receiver may
                 * want to re-call {@link AppWidgetHost#startListening()} to ensure
                 * callbacks are correctly set.
                 */
                new MainThreadExecutor().execute(new Runnable() {

                    @Override
                    public void run() {
                        if (mListener != null) {
                            mListener.onAppWidgetHostReset();
                        }
                    }
                });
            }

            // Fresh and clean launcher DB.
            mMaxItemId = initializeMaxItemId(db);
            setFlagEmptyDbCreated();

            // When a new DB is created, remove all previously stored managed profile information.
            ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), mContext);
        }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

在这里 首先创建了一张名为favorites的表,还是看上面的代码吧 ,就不在复制一份了,这个表主要记录桌面组件自身的信息以及在桌面上的属性信息。我们看下这张表的主要字段含义。

  • _id 这是每个桌面组件在favorites表中的唯一标示,也是favorites表的主键,这个特殊的地方就是它不是自增长类型,所有只能手动确保这个字段的唯一性。

  • title 表示应用程序快捷方式的标题,

  • intent 只有在桌面上摆放的是应用程序快捷方式的时候,该字段才会有值,别的情况下它是没有值的,因为应用程序的快捷方式涉及到应用程序的启动,而对于启动应用程序而言,Intent是至关重要的。

  • Container 用于标示一个快捷方式处于什么样的容器中,目前Launcher提供了两种不同的容器,分别是热键区CONTAINER_HOTSEAT,取值为-101 和桌面容器CONTAINER_DESKTOP 取值为-100.

  • screen 用于标示快捷方式所在的屏幕ID。

  • cellX cellY 这两个整型字段用来表示桌面组件在桌面容器中的位置信息。举个栗子 如果Launcher将桌面区域分为5X6,那么cellX的取值范围就是0-4 cellY取值范围就是0-5.

  • spanX spanY 这两个字段用来表示桌面组件所占据的桌面范围信息,以cellX和cellY中的栗子说明 spanX取值范围是1-5 spanY的取值范围是1-6 如果X轴和Y轴取值大于或者小于这个范围,那么组件是无法加载到桌面上的。

  • itemType 用来表示快捷方式的类型。

    • ITEM_TYPE_APPLICTION值为0 意味着这个快捷方式来自应用程序
    • ITEM_TYPE_SHORTCUT 值为1 意味着这个快捷方式来自用用程序创建的快捷方式 比如联系人的快捷方式
    • ITEM_TYPE_FOLDER 值为2 意味着这个组件是一个文件夹
    • ITEM_TYPE_LIVE_FOLDER 值为3 意味着这个组件为一个实时文件夹
    • ITEM_TYPE_APPWIDGET 值为4 意味着这个组件是一个桌面小部件
    • ITEM_TYPE_WIDGET_CLOCK 值为1000 意味着这个组件是一个时钟桌面小部件
    • ITEM_TYPE_WIDGET_SEARCH 值为1001 意味着这个组件是搜索桌面小部件
    • ITEM_TYPE_WIDGET_PHOTO_FRAME 值为1002 意味着这个组件是相册桌面小部件
  • appWidgetId 该字段在favorites中被定义为不能为空并且默认值是-1的整型字段 桌面上除了可以加载不同的应用程序快捷方式以及文件夹等常见的桌面组件外,还可以加载应用程序提供的桌面小部件 这也是Android的特色之一,而桌面小部件主要依赖AppWidgetHost才能运行,每个应用程序都可以创建自己的AppWidgetHost来加载其他或者本应用提供的桌面小部件,每一个桌面小部件在其加载的AppWidgetHost中都被赋予了一个ID,来标示这个桌面小部件,appWidgetId 就是为了保存Launcher创建的AppWidgetHost中某一个桌面小部件的ID 如果桌面加载的并非小部件这个id将是-1 否则为大于或者等于0的值。

  • iconType 表示当此快捷方式需要图标的时候,可能需要保持的图标信息。

  • iconPackage iconResource : iconPackage 描述了图标来用的应用程序包名,iconResource 记录了该资源的ID号

  • icon 用于保持图片的实体

  • uri 当桌面的快捷方式为一个网页链接的时候,这个字段将会保持这个链接的地址否则为null

  • profileId 当前桌面组件所属的用户ID

创建好数据表后,Launcher需要在第一次启动或者数据库被清理的情况下创建页面配置信息表,这张表的目的是保存当前桌面中包含的桌面页的信息

private void addWorkspacesTable(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
                    LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
                    LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
                    LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
                    ");");
        }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

以上就是Launcher数据库的创建过程。

2、接下来我们看下Launcher的ContentProvider

Launcher的数据库操作都封装在LauncherProvider中,我们在做应用程序开发的时候要对外提供数据,都是使用ContentProvider对数据进行一次包装,然后通过它对外提供数据,这是因为数据库文件往往被创建在应用程序的私有空间,通过ContentProvider可实现跨进程间的访问。Launcher也采用了这种方式。

温馨提示: 每一个ContentProvider的创建都需要比应用程序创建的更早,当在
应用程序清单文件中配置了ContentProvider节点的时候,当应用程序第一次启动
时,框架层就回先将此ContentProvider创建并发布出去。主要就是调用了
ContentProvider的OnCreate方法

Launcher也是遵循这个原则的

 @Override
    public boolean onCreate() {
        final Context context = getContext();
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        mOpenHelper = new DatabaseHelper(context);
        StrictMode.setThreadPolicy(oldPolicy);
        LauncherAppState.setLauncherProvider(this);
        return true;
    }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里应该都好理解。
通过ContentProvider进行数据库操作都需要通过适当的URI,并配以不同的条件,Launcher的数据库提供者提供了一个专门用于保存并确保这些信息合法的类SqlArguments。
它有三个成员变量

  //需要查询的表名
  public final String table;
  //SQL的查询条件
  public final String where;
  //保存了where条件中所需的参数
  public final String[] args;
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

知道了成员变量的含义这个函数就很好理解了 这里就不在分析了。

准备知识都完成后 我们继续看下LauncherProvider的增删查改

  • 查询
 @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {

        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(args.table);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
        result.setNotificationUri(getContext().getContentResolver(), uri);

        return result;
    }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里需要注意的是,在Launcher的Provider创建的时候创建了mOpenHelper实例,当需要对数据库进行操作的时候,需要从中获取数据库的实例,只有通过这个实例才能进行查询操作。

  • 修改
  @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    //更新参数:表名 where条件以及条件参数的设置
        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);

        addModifiedTime(values);
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count = db.update(args.table, values, args.where, args.args);
        //更新通知
        if (count > 0) notifyListeners();

        reloadLauncherIfExternal();
        return count;
    }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 增加
 @Override
        public Uri insert(Uri uri, ContentValues initialValues) {
            SqlArguments args = new SqlArguments(uri);

           ...

            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
            addModifiedTime(initialValues);
            final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
            //插入成功rowId 是大于0的
            if (rowId < 0) return null;
            //如果插入成功则依据输入的URI为基础拼接上返回的id形成新的URI
            uri = ContentUris.withAppendedId(uri, rowId);
            notifyListeners();

            if (Utilities.ATLEAST_MARSHMALLOW) {
                reloadLauncherIfExternal();
            } else {
                // Deprecated behavior to support legacy devices which rely on provider callbacks.
                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
                if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
                    app.reloadWorkspace();
                }

                String notify = uri.getQueryParameter("notify");
                if (notify == null || "true".equals(notify)) {
                    getContext().getContentResolver().notifyChange(uri, null);
                }
            }
            return uri;
    }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 删除
 @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count = db.delete(args.table, args.where, args.args);
        if (count > 0) notifyListeners();

        reloadLauncherIfExternal();
        return count;
    }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

好了以上就是Launcher数据库的实现了,当然Launcher除了提供这些常用的访问方式外,还在内部提供了一些接口工具,以便Launcher的其他组件可以方便的使用Launcher数据库功能。这些比较零散的知识就需要大家在实际开发中去分析了。

<think>我们正在处理一个关于AndroidLauncher3的配置问题。用户希望预置应用图标默认从第二页开始显示,而不是从第一页开始。根据引用[1]和引用[2]的信息,预置应用图标是通过在xml文件中配置<favorite>或<resolve>节点来实现的。这些节点中的属性launcher:screen指定了图标所在的屏幕(页面)索引。通常,屏幕索引是从0开始的。因此,第一页的索引是0,第二页是1,以此类推。所以,要使应用图标默认显示在第二页,我们需要在配置这些图标时将launcher:screen属性设置为1(表示第二页)。但是,用户提到“默认从第二页开始显示”,这意味着可能整个预置的应用图标布局都从第二页开始,而第一页留空。根据引用[1]中的示例:<favorite launcher:screen="0"... />这表示该图标放在第一页(索引0)。因此,我们可以通过修改配置文件中每个图标的launcher:screen属性,将其设置为1(第二页)或更大的值,从而让它们出现在第二页或之后的页面。但是,注意引用[1]中提到了多个配置文件,例如default_workspace_XXX.xml(XXX可能是设备类型,如phone或tablet)以及dw_tablet_hotseat.xml(用于hotseat栏)。我们需要修改的是主屏幕的workspace布局文件。另外,引用[3]提到了一些关于拖拽时重新排序的代码,但这个问题主要是关于初始布局配置,所以代码修改可能不是必须的,除非默认行为有冲突。步骤:1.找到Launcher3中配置主屏幕图标的xml文件,通常位于:packages/apps/Launcher3/res/xml/目录下。常见的文件有:default_workspace.xml,default_workspace_phone.xml,default_workspace_tablet.xml等,具体取决于设备类型。2.在找到的配置文件中,修改每个<favorite>或<resolve>节点的launcher:screen属性。如果希望所有预置图标都从第二页开始,那么将原来的screen="0"(第一页)改为screen="1"(第二页),而原本就在第二页或之后的图标可以保持不变。3.注意:如果第一页没有任何图标,那么用户可能会看到第一页是空白页。这可能是期望的效果,但也要注意Launcher3可能默认会创建一定数量的页面(比如至少一页),所以即使第一页没有图标,它也会存在。4.另外,如果希望第一页完全不存在,那么可能需要修改Launcher3的代码,使其初始时不创建第一页。但根据用户需求,只是希望图标从第二页开始,所以第一页可以是空白页,这样也是可以的。5.如果配置文件中已经有一些图标分布在多页,那么我们需要调整所有图标的screen值,使得原本在第一页(screen0)的图标移动到第二页(screen1),原本在第二页的移动到第三页,以此类推。或者,如果只想移动预置的图标到第二页及以后,而保留其他页面的图标,则只修改那些screen属性为0的图标。6.另一种做法:我们可以将整个配置的屏幕索引都加1(即原来screen0的改为screen1,screen1的改为screen2,...),然后新增一个空白的第一页(screen0)。但是,Launcher3在初始化时会根据配置的screen值来创建页面,所以如果最小screen是1,那么它就会创建索引为1的页面,而索引0的页面可能不会自动创建。因此,我们需要确保在配置中至少有一个图标在screen0,才能创建第一页?但是,用户要求从第二页开始,所以第一页应该为空,那么我们就不要在第一页放置任何图标,即配置文件中不要有screen="0"的图标。7.因此,我们只需将配置文件中所有图标的screen属性值设置为1或更大,而不设置任何screen="0"的图标。这样,Launcher3在初始化时会创建screen1及之后的页面,而screen0(第一页)不会被创建?但是,根据Launcher3的代码逻辑,它可能会根据配置的屏幕索引范围来创建连续的页面,从0到最大索引。所以,如果我们配置的最小索引是1,那么它会创建索引0和1的页面(因为索引0缺失,所以索引0的页面是空的,索引1的页面有图标)。这样,用户就会看到第一页(索引0)是空的,第二页(索引1)开始有图标。8.因此,我们不需要修改代码,只需要修改配置文件,将所有图标的screen属性设置为1(第二页)或更大,而不要设置screen="0"的图标。这样,第一页就会是空的。9.但是,注意引用[1]中提到的配置示例,它可能包含多个图标分布在多个页面。我们需要确保移除所有screen="0"的配置,并将它们移动到screen1或更高。10.另外,引用[2]中的hotseat配置(hotseat是底部的快捷栏)通常固定在屏幕底部,与屏幕页面无关。所以hotseat的配置不需要修改,除非用户希望hotseat的图标也从第二页开始?但hotseat是独立于屏幕页面的,所以不需要改变。11.因此,我们只需要修改主屏幕(workspace)的配置。具体操作:-打开主屏幕的配置文件(例如default_workspace.xml)。-找到所有<favorite>或<resolve>节点(这些节点配置了主屏幕图标)。-将每个节点的launcher:screen属性值增加1(这样原来的0变为1,1变为2,等等),或者如果希望所有图标都放在第二页,那么可以全部设置为1(但这样所有图标都会挤在第二页,可能不是用户想要的分布)。用户可能希望保持原有的分布,只是整体后移一页。-或者,如果用户希望保持原有分布但整体后移一页,那么我们就将每个图标的screen属性值加1。-另外,如果配置文件中有定义文件夹(folder)或者小部件(widget),同样需要修改它们的screen属性。12.修改后,第一页(screen0)将没有图标,而第二页(screen1)开始显示原来的第一页的图标,第三页显示原来的第二页,以此类推。13.但是,这样修改后,第一页是空白页,用户可能会手动删除它。如果希望一开始就不显示第一页,那么可能需要修改Launcher3的代码,使其初始时跳转到第二页。但用户没有要求这个,所以暂时不考虑。14.另外,注意引用[3]中提到的代码,在ReorderAlarmListener中,当拖拽到小部件位置时,有一个判断:if(screen==0 &&mTargetCell[1]==0){return;}这可能是为了防止拖拽到第一页的特定位置(比如第一行第一列)?但我们修改的是初始布局,所以不会影响。15.最后,重新编译Launcher3并刷入设备。总结:通过修改workspace的配置文件,将所有图标的屏幕索引增加1,使得初始布局从第二页(索引1)开始,第一页(索引0)留空。注意:由于设备不同,配置文件可能有多个,需要根据具体设备使用的配置文件进行修改。示例修改:原配置:<favorite launcher:screen="0"launcher:x="0"launcher:y="0" .../><favorite launcher:screen="0"launcher:x="1"launcher:y="0"... /><favoritelauncher:screen="1" launcher:x="0"launcher:y="0" .../>修改后:<favoritelauncher:screen="1" launcher:x="0"launcher:y="0" .../><favoritelauncher:screen="1" launcher:x="1" launcher:y="0"... /><favoritelauncher:screen="2" launcher:x="0"launcher:y="0" .../>这样,原第一页的两个图标被移动到第二页(索引1),原第二页的一个图标被移动到第三页(索引2)。第一页(索引0)没有图标,为空。因此,当用户打开主屏幕时,会看到第一页是空白页,第二页开始有应用图标。注意:Launcher3可能会根据配置自动创建足够的页面,所以不用担心页面不够。另外,如果希望第一页完全不存在(即用户滑动时不会出现空白的第一页),那么可能需要修改Launcher3的初始页面设置,这通常需要代码修改。但是,用户要求的是“默认从第二页开始显示”,所以显示第二页作为起始页面也是可以的,但初始打开时可能还是显示第一页(空白页)。所以我们需要设置默认显示第二页。关于默认显示页面:在Launcher3的代码中,默认显示的是第一页(索引0)。如果我们希望默认显示第二页(索引1),那么还需要修改默认的当前页面索引。根据Launcher3的代码,默认当前页面通常是在加载布局时设置的。我们可以搜索代码中设置当前页面的地方。例如,在LauncherAppState.java中,可能有一个方法用于初始化当前页面。或者,在Launcher.java中,可能有类似setCurrentPage的调用。因此,我们可能需要额外的代码修改:将初始的当前页面设置为1(即第二页)。但是,用户的问题只要求配置预置应用图标默认从第二页开始布局,并没有要求启动时显示第二页。所以,如果用户希望启动时显示第二页,那么需要额外修改。考虑到问题描述,我们只修改布局,启动时显示第一页(空白页)可能不是用户想要的。所以,我们可能需要同时修改默认显示页面。由于问题描述中未明确,我们分两种情况:A.只修改布局,让第一页空白,第二页开始有图标,但启动时显示第一页(空白页)。B.同时修改布局和默认显示页面,让启动时显示第二页。用户可能更希望B。因此,我们需要做两件事:1.修改配置文件,将图标所在的screen索引都加1(即整体后移一页)。2.修改Launcher3代码,将默认显示的页面索引设置为1(第二页)。关于修改默认显示页面:在Launcher3的代码中,可以通过以下方式修改:在加载布局后,设置当前页面为1。例如,在Launcher.java的onResume方法中,或者初始化视图时,设置:mWorkspace.setCurrentPage(1);//假设mWorkspace是Workspace类型的对象但是,初始页面可能是在从数据库加载布局后设置的,而且可能受用户上一次退出时保存的页面影响。所以,如果我们希望第一次启动时(没有用户历史时)显示第二页,那么需要修改初始值。通常,Launcher3在第一次启动时会加载默认布局(即我们修改的xml配置),然后设置当前页面。我们可以搜索代码中设置初始页面的地方。在Launcher.java中,有一个方法bindWorkspace(),它可能会在加载布局后设置当前页面。例如:if(restoreSuccessful) {//从用户上次保存的状态恢复mWorkspace.setCurrentPage(mWorkspace.getChildCount() -1);} else{//第一次启动,加载默认布局mModel.loadDefaultFavoritesIfNecessary(0);mWorkspace.setCurrentPage(0);//这里设置初始页面为0}所以,我们需要修改第一次启动时的默认页面。例如,将mWorkspace.setCurrentPage(0)改为mWorkspace.setCurrentPage(1)。但是,注意:我们修改了布局,第一页(索引0)是空白页,第二页(索引1)开始有内容。所以,在第一次启动时,我们希望显示第二页(索引1)。因此,我们可以修改为:mWorkspace.setCurrentPage(1);但是,这样修改后,每次第一次启动都会显示第二页。而如果用户以后添加了图标到第一页,那么启动时仍然显示第二页,这可能不是用户期望的。所以,更合理的做法是:在加载默认布局后,如果当前没有历史状态(第一次启动),并且第一页是空的,那么自动显示第一个非空的页面。但是,这需要更复杂的逻辑。为了简单起见,我们按照用户要求,只针对第一次启动的默认布局设置显示第二页。考虑到用户可能只是希望预置的图标从第二页开始,而第一页留给用户自己放置图标,那么启动时显示第一页(空白)也是合理的。所以,是否修改启动页面需要用户明确。由于用户问题描述中没有明确,我们可以在回答中给出两种选择。综上所述,我们提供两种方案:方案一:仅修改配置文件,将所有图标的screen索引加1。这样第一页为空,第二页开始有图标。启动时显示第一页(空白页)。方案二:在方案一的基础上,修改Launcher3代码,将第一次启动时的默认页面设置为1(第二页)。用户可以根据需要选择。因此,回答结构如下:1.修改配置文件:找到workspace配置文件,将所有图标的screen属性值加1。2. (可选)修改Launcher3代码,设置第一次启动时默认显示第二页。最后,提供相关问题的建议。</think>### 配置 Launcher3 预置应用图标从第二页开始布局 在 Android Launcher3 中,预置应用图标的布局位置由 XML 配置文件中的 `launcher:screen` 属性控制。该属性表示屏幕页面的索引(**从 0 开始计数**),要实现默认从第二页(索引为 1)开始显示图标,需按以下步骤操作: --- #### **步骤 1:修改主屏幕布局文件** 1. 定位配置文件 打开 Launcher3 的默认布局文件,路径通常为: `packages/apps/Launcher3/res/xml/default_workspace_XXX.xml` (`XXX` 表示设备类型,如 `phone` 或 `tablet`)。 2. 调整 `screen` 属性值 将所有预置图标的 `launcher:screen` 属性值设置为 `1`(第二页),例如: ```xml <!-- 修改前:图标在第一页(screen="0") --> <favorite launcher:screen="0" launcher:x="0" launcher:y="0" launcher:packageName="com.example.app" launcher:className="com.example.app.MainActivity" /> <!-- 修改后:图标在第二页(screen="1") --> <favorite launcher:screen="1" <!-- 关键修改 --> launcher:x="0" launcher:y="0" launcher:packageName="com.example.app" launcher:className="com.example.app.MainActivity" /> ``` 3. **注意事项** - 若需分散图标到多页(如第二页、第三页),可设置 `screen="1"`、`screen="2"` 等。 - 确保第一页(`screen="0"`)**无任何预置图标**,否则第一页会被自动创建[^1]。 --- #### **步骤 2:处理 Hotseat 栏(可选)** 若需调整底部快捷栏(Hotseat)的图标位置: 1. 修改 Hotseat 配置文件(如 `dw_tablet_hotseat.xml`): ```xml <resolve launcher:container="-101" launcher:screen="1" <!-- 从第二页开始 --> launcher:x="0" launcher:y="0"> <favorite launcher:uri="..."/> </resolve> ``` 2. Hotseat 的 `container` 固定为 `-101`,需同步调整 `screen` 属性[^2]。 --- #### **步骤 3:防止布局重置(进阶)** 若遇到拖拽操作干扰布局,需在代码层拦截重置逻辑: ```java // 在 ReorderAlarmListener 中拦截第一页的拖拽操作 public void onAlarm(Alarm alarm) { int screen = getIdForScreen(mDragTargetLayout); if (screen == 0) { // 禁止在第一页添加图标 return; } // ...正常处理逻辑 } ``` 参考位置:`packages/apps/Launcher3/src/com/android/launcher3/ReorderAlarmListener.java`[^3]。 --- #### **最终效果** - **第一页**:空白页(无预置图标)。 - **第二页**:预置应用图标从该页开始布局。 - **启动行为**:首次启动时默认显示第一页(空白),用户滑动后可见第二页的图标。 > ⚠️ **重要提示**:修改后需重新编译 Launcher3 并刷入设备。实际文件名可能因设备型号或 Android 版本略有差异,请根据具体代码库调整。 --- ### 相关问题 1. **如何添加自定义小部件到 Launcher3 的默认布局?** > 需在 XML 中配置 `<appwidget>` 节点,指定 `screen`、`x`、`y` 及小部件尺寸,并确保 Launcher3 有绑定小部件的权限。 2. **Launcher3 如何动态增减主屏幕页数?** > 页面数量由 `screen` 的最大索引值决定。例如,若配置的最高 `screen="3"`,则初始化时会创建 4 个页面(索引 0~3)。删除页面需动态调整数据库中的布局记录。 3. **预置图标修改后为何不生效?** > 可能原因: > - XML 文件未覆盖到当前设备类型(检查 `default_workspace_phone/tablet.xml`)。 > - 系统已存在用户布局数据(需清除 Launcher3 数据或重置设备)。 > - `screen` 属性值超出 Launcher3 的最大页面限制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值