android13 launcher[06hotseat]

835 阅读21分钟

1.简介

  • 整理下hotseat的默认数据加载流程,方便修改查看默认数据。
  • 我们集成了谷歌套件,所以launcher里默认的数据被修改了
  • 简单学习下hotseat控件的加载

1.1.LoaderTask.java

这个类是launcher里数据的具体加载逻辑,当然也包括hotseat,这里就简单看下hotseat相关的,也就是数据库里的favorite表

>run

    public void run() {
//..
        try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
            List<ShortcutInfo> allShortcuts = new ArrayList<>();
            try {
            //
                loadWorkspace(allShortcuts, memoryLogger);
//..

>loadWorkspace

    private void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, LoaderMemoryLogger logger) {
        loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI,
                null /* selection */, logger);
    }
    protected void loadWorkspace(
            List<ShortcutInfo> allDeepShortcuts,
            Uri contentUri,
            String selection,
            @Nullable LoaderMemoryLogger logger) {
        final Context context = mApp.getContext();
        final ContentResolver contentResolver = context.getContentResolver();
        final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
        final boolean isSafeMode = pmHelper.isSafeMode();
        final boolean isSdCardReady = Utilities.isBootCompleted();
        final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);

        boolean clearDb = false;
        if (!GridSizeMigrationTaskV2.migrateGridIfNeeded(context)) {
            // Migration failed. Clear workspace.
            clearDb = true;
        }

        if (clearDb) {
            Log.d(TAG, "loadWorkspace: resetting launcher database");
            LauncherSettings.Settings.call(contentResolver,
                    LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
        }

        //加载默认的数据,搜一下method哪里用到,很容易搜到下边的LauncherProvider,见2.1
        LauncherSettings.Settings.call(contentResolver,
                LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);
    //下边就是查询表里的数据了,代码太多不贴了。                

2.LauncherProvider

2.1.call

    public Bundle call(String method, final String arg, final Bundle extras) {
        if (Binder.getCallingUid() != Process.myUid()) {
            return null;
        }
        createDbIfNotExists();

>METHOD_LOAD_DEFAULT_FAVORITES

加载默认的favorite数据

            case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
                loadDefaultFavoritesIfNecessary();
                return null;
            }

>METHOD_SWITCH_DATABASE

切换数据库,这个好像是啥小型设备才用到

            case LauncherSettings.Settings.METHOD_SWITCH_DATABASE: {
            //已打开的和传入的数据库名字一样,啥也不干
                if (TextUtils.equals(arg, mOpenHelper.getDatabaseName())) return null;
                final DatabaseHelper helper = mOpenHelper;
                //是否有对应的authority数据
                if (extras == null || !extras.containsKey(KEY_LAYOUT_PROVIDER_AUTHORITY)) {
                    mProviderAuthority = null;
                } else {
                    mProviderAuthority = extras.getString(KEY_LAYOUT_PROVIDER_AUTHORITY);
                }
                //用新的数据库名字打开新的dbhelper
                mOpenHelper = DatabaseHelper.createDatabaseHelper(
                        getContext(), arg, false /* forMigration */);
                helper.close();
                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
                if (app == null) return null;
                app.getModel().forceReload();
                return null;
            }

2.2.loadDefaultFavoritesIfNecessary

  • 获取默认的favorites数据,加载优先级如下边注解,依次查找
  • 集成谷歌包以后,最终用的是GmsSampleIntegration里的partner_default_layout.xml文件
  • 如果没有谷歌包,默认是launcher里的res/xml/default_workspace_(6x5根据实际使用的网格找对应文件)
    /**
     * Loads the default workspace based on the following priority scheme:
     *   1) From the app restrictions
     *   2) From a package provided by play store
     *   3) From a partner configuration APK, already in the system image
     *   4) The default configuration for the particular device
     */
    synchronized private void loadDefaultFavoritesIfNecessary() {
        SharedPreferences sp = Utilities.getPrefs(getContext());
        //先判断是否创建了新的数据库,是的话需要加载默认数据
        if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
            AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
            //见2.3
            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
            if (loader == null) {
            //见4.1,gms的包里没有,返回空
                loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
            }
            if (loader == null) {
                final Partner partner = Partner.get(getContext().getPackageManager());
                if (partner != null && partner.hasDefaultLayout()) {
                    final Resources partnerRes = partner.getResources();
                    //这里找的是partner_default_layout.xml文件
                    //在GmsSampleIntegration的res目录里能找到,见3.4
                    int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                            "xml", partner.getPackageName());
                    if (workspaceResId != 0) {
                        loader = new DefaultLayoutParser(getContext(), widgetHost,
                                mOpenHelper, partnerRes, workspaceResId);
                    }
                }
            }
        //是否使用的是外部的layout
            final boolean usingExternallyProvidedLayout = loader != null;
            if (loader == null) {
            //没有外部layout,则加载launcher默认的,见2.4
                loader = getDefaultLayoutParser(widgetHost);
            }

            //见小节 7.1,删除并创建新的favorites表
            mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
            //见7.4
            if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
                    && usingExternallyProvidedLayout) {
                //外部layout没有数据,那么加载默认的layout数据
                mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
                mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
                        getDefaultLayoutParser(widgetHost));
            }
            //清除if条件里的EMPTY_DATABASE_CREATED的值
            clearFlagEmptyDbCreated();
        }
    }

>DatabaseHelper

上边的EMPTY_DATABASE_CREATED的值在这里被修改为true

        protected void onEmptyDbCreated() {
            // Set the flag for empty DB
            LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
                    .commit();
        }

上述方法在onCreate里调用,

        public void onCreate(SQLiteDatabase db) {

            mMaxItemId = 1;

            addFavoritesTable(db, false);

            // Fresh and clean launcher DB.
            mMaxItemId = initializeMaxItemId(db);
            if (!mForMigration) {
            //mForMigration是构造方法里赋值的
                onEmptyDbCreated();
            }
        }
        public DatabaseHelper(Context context, String dbName, boolean forMigration) {
            super(context, dbName, SCHEMA_VERSION);
            mContext = context;
            mForMigration = forMigration;
        }

查看下构造方法的创建,

  • 迁移数据到时候,这个是true
  • 正常是false

>createDbIfNotExists

    protected synchronized void createDbIfNotExists() {
        if (mOpenHelper == null) {
            mOpenHelper = DatabaseHelper.createDatabaseHelper(
                    getContext(), false /* forMigration */);

            RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper);
        }
    }

2.3.createWorkspaceLoaderFromAppRestriction

根据提供的authority查找,

    private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
            LauncherWidgetHolder widgetHolder) {
        Context ctx = getContext();
        final String authority;
        if (!TextUtils.isEmpty(mProviderAuthority)) {
            authority = mProviderAuthority;
        } else {
            authority = Settings.Secure.getString(ctx.getContentResolver(),
                    "launcher3.layout.provider");
        }
        //公司设备上边2返回都是空
        if (TextUtils.isEmpty(authority)) {
            return null;
        }

        ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0);
        if (pi == null) {
            return null;
        }
        Uri uri = getLayoutUri(authority, ctx);
        try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
            // Read the full xml so that we fail early in case of any IO error.
            String layout = new String(IOUtils.toByteArray(in));
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(new StringReader(layout));

            return new AutoInstallsLayout(ctx, widgetHolder, mOpenHelper,
                    ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
                    () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
        } catch (Exception e) {
            return null;
        }
    }

2.4.getDefaultLayoutParser

这个就是launcher里默认的配置

    private DefaultLayoutParser getDefaultLayoutParser(LauncherWidgetHolder widgetHolder) {
        InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
        //mDefaultWorkspaceLayoutOverride是测试用的,正常用的是idp里的
        int defaultLayout = mDefaultWorkspaceLayoutOverride > 0
                ? mDefaultWorkspaceLayoutOverride : idp.defaultLayoutId;
        return new DefaultLayoutParser(getContext(), widgetHolder,
                mOpenHelper, getContext().getResources(), defaultLayout);
    }

idp里的数据来源是xml文件,位置:res/xml/device_profiles.xml,不同的网格数据不同,如下

    <grid-option
        launcher:name="6_by_5"
        launcher:devicePaddingId="@xml/paddings_6x5"
        launcher:dbFile="launcher_6_by_5.db"
        launcher:defaultLayoutId="@xml/default_workspace_6x5"

如果没有集成GMS包,那么AOSP里launcher的默认数据来源就是default_workspace_6x5.xml了(后边的6x5根据实际尺寸来),打开后可以看到favorites标签

>default_workspace_6x5.xml

<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) -->
    <!-- Mail Calendar Gallery Store Internet Camera -->
    <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>

    <resolve
        launcher:container="-101"
        launcher:screen="1"
        launcher:x="1"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_CALENDAR;end" />
    </resolve>
//...    

3.Partner

    public static final String RES_PARTNER_DEFAULT_LAYOUT = "partner_default_layout";

3.1.get

    private static final String
            ACTION_PARTNER_CUSTOMIZATION = "com.android.launcher3.action.PARTNER_CUSTOMIZATION";
            
    public static Partner get(PackageManager pm) {
        return get(pm, ACTION_PARTNER_CUSTOMIZATION);
    }
    
    public static Partner get(PackageManager pm, String action) {
        Pair<String, Resources> apkInfo = findSystemApk(action, pm);
        return apkInfo != null ? new Partner(apkInfo.first, apkInfo.second) : null;
    }    
    

>findSystemApk

找到符合action的广播接收者

    private static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
        final Intent intent = new Intent(action);
        for (ResolveInfo info : pm.queryBroadcastReceivers(intent, MATCH_SYSTEM_ONLY)) {
            final String packageName = info.activityInfo.packageName;
            try {
                final Resources res = pm.getResourcesForApplication(packageName);
                return Pair.create(packageName, res);

        }
        return null;
    }

3.2.ACTION_PARTNER_CUSTOMIZATION

查找符合要求的receiver

>空实现的

vendor/partner_gms/apps/GmsSampleIntegration/AndroidManifest.xml

        <!-- This isn't a real receiver, it's only used as a marker interface. -->
        <receiver android:name=".LauncherCustomizationReceiver"
                  android:exported="true">
            <intent-filter>
                <action android:name="com.android.launcher3.action.PARTNER_CUSTOMIZATION" />
            </intent-filter>
        </receiver>

3.3.getXmlResId

    public int getXmlResId(String layoutName) {
        return getResources().getIdentifier(layoutName, "xml", getPackageName());
    }

3.4.partner_default_layout.xml

可以看到,gms包里,默认的hotseat数据有5个(container是-101的),后边还有个文件夹里边包了一堆谷歌应用

<favorites>
  <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
  <!-- Dialer Messaging Calendar 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.AllInOneActivity"/>
  <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.CameraActivity"/>

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

4.AutoInstallsLayout

4.1.get

参看3.2对应的谷歌包,在里边没找到default_layout开头的xml文件,所以这个返回的是空

    private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s";
    private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d";
    private static final String LAYOUT_RES = "default_layout";    
    
    static AutoInstallsLayout get(Context context, LauncherWidgetHolder appWidgetHolder,
            LayoutParserCallback callback) {
            //这个参考小节 3.1
        Partner partner = Partner.get(context.getPackageManager(), ACTION_LAUNCHER_CUSTOMIZATION);
        if (partner == null) {
            return null;
        }
        InvariantDeviceProfile grid = LauncherAppState.getIDP(context);

        // 根据网格的行列以及hotseat的个数,获取布局名字,比如default_layout_6x5_h4
        String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
                grid.numColumns, grid.numRows, grid.numDatabaseHotseatIcons);
        //看下有没有对应的文件
        int layoutId = partner.getXmlResId(layoutName);

        // Try with only grid size
        if (layoutId == 0) {
           //上边的 没找到,这次找default_layout_6x5
            layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
                    grid.numColumns, grid.numRows);
            layoutId = partner.getXmlResId(layoutName);
        }

        // Try the default layout
        if (layoutId == 0) {
    //还是没找到,这次找default_layout.xml
            layoutId = partner.getXmlResId(LAYOUT_RES);
        }

        if (layoutId == 0) {
    //还没找到,那就是没有
            return null;
        }
        return new AutoInstallsLayout(context, appWidgetHolder, callback, partner.getResources(),
                layoutId, TAG_WORKSPACE);
    }

5.Launcher

上篇里简单看了下launcher数据的获取流程,获取数据以后会有各种回调的,这里看下launcher里的回调

5.1.bindItems

    public void bindItems(
            final List<ItemInfo> items,//这个就是获取到的数据集合
            final boolean forceAnimateIcons,
            final boolean focusFirstItemForAccessibility) {
        // Get the list of added items and intersect them with the set of items here
        final Collection<Animator> bounceAnims = new ArrayList<>();
        boolean canAnimatePageChange = canAnimatePageChange();
        Workspace<?> workspace = mWorkspace;
        int newItemsScreenId = -1;
        int end = items.size();
        View newView = null;
        for (int i = 0; i < end; i++) {
            final ItemInfo item = items.get(i);
//..
            final View view;//根据itemType创建不同类型的view
            switch (item.itemType) {
//..

            workspace.addInScreenFromBind(view, item);//添加到容器里,见6.1


            if (newView == null) {
                newView = view;
            }
        }

6.WorkspaceLayoutManager.java

Workspace实现了这个接口,所以上边的workspace调用的就是这个接口里的方法

6.1.addInScreenFromBind

    default void addInScreenFromBind(View child, ItemInfo info) {
        int x = info.cellX;
        int y = info.cellY;
        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
                || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
            int screenId = info.screenId;
            x = getHotseat().getCellXFromOrder(screenId);
            y = getHotseat().getCellYFromOrder(screenId);
        }
        addInScreen(child, info.container, info.screenId, x, y, info.spanX, info.spanY);
    }

>addInScreen

    default void addInScreen(View child, int container, int screenId, int x, int y,
            int spanX, int spanY) {
        final CellLayout layout;
        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
                || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
            //容器类型是hotseat,获取对应容器
            layout = getHotseat();

            //hotseat只显示图标,不显示文字
            if (child instanceof FolderIcon) {
                ((FolderIcon) child).setTextVisible(false);
            }
        } else {
            // Show folder title if not in the hotseat
            if (child instanceof FolderIcon) {
                ((FolderIcon) child).setTextVisible(true);
            }
            //这个是workspace了,可以有多个屏,根据当前id获取对应的容器
            layout = getScreenWithId(screenId);
        }

        ViewGroup.LayoutParams genericLp = child.getLayoutParams();
        CellLayoutLayoutParams lp;
        //设置layoutParams
        if (genericLp == null || !(genericLp instanceof CellLayoutLayoutParams)) {
            lp = new CellLayoutLayoutParams(x, y, spanX, spanY, screenId);
        } else {
            lp = (CellLayoutLayoutParams) genericLp;
            lp.cellX = x;
            lp.cellY = y;
            lp.cellHSpan = spanX;
            lp.cellVSpan = spanY;
        }

        if (spanX < 0 && spanY < 0) {
            lp.isLockedToGrid = false;
        }

        // Get the canonical child id to uniquely represent this view in this screen
        ItemInfo info = (ItemInfo) child.getTag();
        int childId = info.getViewId();

        boolean markCellsAsOccupied = !(child instanceof Folder);
        //添加到对应的容器里
        if (!layout.addViewToCellLayout(child, -1, childId, lp, markCellsAsOccupied)) {
        }

        child.setHapticFeedbackEnabled(false);
        child.setOnLongClickListener(getWorkspaceChildOnLongClickListener());
        if (child instanceof DropTarget) {
            onAddDropTarget((DropTarget) child);
        }
    }

7.DatabaseHelper

7.1.createEmptyDB

清除favorites表和workspaceScreens表,并创建新的表

        /**
         * Clears all the data for a fresh start.
         */
        public void createEmptyDB(SQLiteDatabase db) {
            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
                dropTable(db, Favorites.TABLE_NAME);
                dropTable(db, "workspaceScreens");
                onCreate(db);
                t.commit();
            }
        }

7.2.onCreate

        public void onCreate(SQLiteDatabase db) {
            mMaxItemId = 1;
            addFavoritesTable(db, false);//添加favorite表
            // Fresh and clean launcher DB.
            mMaxItemId = initializeMaxItemId(db);
            if (!mForMigration) {
                onEmptyDbCreated();
            }
        }

>onEmptyDbCreated

修改sp的值,表明默认的db已创建

        protected void onEmptyDbCreated() {
            // Set the flag for empty DB
            LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
                    .commit();
        }

7.3.createDatabaseHelper

        static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
            return createDatabaseHelper(context, null, forMigration);
        }

        static DatabaseHelper createDatabaseHelper(Context context, String dbName,
                boolean forMigration) {
            if (dbName == null) {
            //默认的db名字,可以参考2.4里读取的配置文件
                dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
            }
            DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);

            //如果favorites表没有创建,默认创建
            if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
                databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
            }
            //hotseat_restore_backup是否存在
            databaseHelper.mHotseatRestoreTableExists = tableExists(
                    databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);

            databaseHelper.initIds();//获取favorites表的最大id
            return databaseHelper;
        }

7.4.loadFavorites

        @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
            //见8.1
            int count = loader.loadLayout(db, new IntArray());

            //获取表的最大id值
            mMaxItemId = initializeMaxItemId(db);
            return count;
        }

>initializeMaxItemId

就是查找数据库,获取id的最大值

        private int initializeMaxItemId(SQLiteDatabase db) {
            return getMaxId(db, "SELECT MAX(%1$s) FROM %2$s", Favorites._ID, Favorites.TABLE_NAME);
        }

7.5.newLauncherWidgetHost

        public LauncherWidgetHolder newLauncherWidgetHolder() {
            return LauncherWidgetHolder.newInstance(mContext);
        }

通过factory实例化的,在配置里配置的

public final class QuickstepWidgetHolder extends LauncherWidgetHolder {

7.6.insertAndCheck

        public int insertAndCheck(SQLiteDatabase db, ContentValues values) {
            return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
        }

>dbInsertAndCheck

    @Thunk static int dbInsertAndCheck(DatabaseHelper helper,
            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
        if (values == null) {
            throw new RuntimeException("Error: attempting to insert null values");
        }
        if (!values.containsKey(LauncherSettings.Favorites._ID)) {
            throw new RuntimeException("Error: attempting to add item without specifying an id");
        }
        helper.checkId(values);
        return (int) db.insert(table, nullColumnHack, values);
    }

8.DefaultLayoutParser

public class DefaultLayoutParser extends AutoInstallsLayout {

8.1.loadLayout

父类实现

    public int loadLayout(SQLiteDatabase db, IntArray screenIds) {
        mDb = db;
        try {
            return parseLayout(mInitialLayoutSupplier.get(), screenIds);
        } catch (Exception e) {
            return -1;
        }
    }

8.2.parseLayout

父类实现

    /**
     * Parses the layout and returns the number of elements added on the homescreen.
     */
    protected int parseLayout(XmlPullParser parser, IntArray screenIds)
            throws XmlPullParserException, IOException {
        beginDocument(parser, mRootTag);
        final int depth = parser.getDepth();
        int type;
        ArrayMap<String, TagParser> tagParserMap = getLayoutElementsMap();
        int count = 0;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            count += parseAndAddNode(parser, tagParserMap, screenIds);
        }
        return count;
    }

>getLayoutElementsMap

map里存储了支持的tag,以及对应tag用到的解析器

    protected ArrayMap<String, TagParser> getLayoutElementsMap() {
        ArrayMap<String, TagParser> parsers = new ArrayMap<>();
        parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
        parsers.put(TAG_APPWIDGET, new AppWidgetParser());
        parsers.put(TAG_SEARCH_WIDGET, new SearchWidgetParser());
        parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes));
        parsers.put(TAG_RESOLVE, new ResolveParser());
        parsers.put(TAG_FOLDER, new MyFolderParser());
        parsers.put(TAG_PARTNER_FOLDER, new PartnerFolderParser());
        return parsers;
    }

8.3.parseAndAddNode

父类实现

    protected int parseAndAddNode(
            XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)
            throws XmlPullParserException, IOException {

        if (TAG_INCLUDE.equals(parser.getName())) {
        //include标签,说明可以引用其他文件里的数据
            final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
            if (resId != 0) {
                // recursively load some more favorites, why not?
                return parseLayout(mSourceRes.getXml(resId), screenIds);
            } else {
                return 0;
            }
        }

        mValues.clear();
        //见8.4,获取容器id以及屏幕id
        parseContainerAndScreen(parser, mTemp);
        final int container = mTemp[0];
        final int screenId = mTemp[1];

        mValues.put(Favorites.CONTAINER, container);
        mValues.put(Favorites.SCREEN, screenId);

        mValues.put(Favorites.CELLX,
                convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount));
        mValues.put(Favorites.CELLY,
                convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount));
    //根据tag获取对应的解析器
        TagParser tagParser = tagParserMap.get(parser.getName());
        if (tagParser == null) {
           //解析器为null,返回
            return 0;
        }
        //调用对应解析器解析
        int newElementId = tagParser.parseAndAdd(parser);
        if (newElementId >= 0) {
            // Keep track of the set of screens which need to be added to the db.
            if (!screenIds.contains(screenId) &&
                    container == Favorites.CONTAINER_DESKTOP) {
                screenIds.add(screenId);
            }
            return 1;
        }
        return 0;
    }

>convertToDistanceFromEnd

负的表示从右往左数,正的从左往右,为空表示就在末尾

    private static String convertToDistanceFromEnd(String value, int endValue) {
        if (!TextUtils.isEmpty(value)) {
            int x = Integer.parseInt(value);
            if (x < 0) {
                return Integer.toString(endValue + x);
            }
        }
        return value;
    }

8.4.parseContainerAndScreen

    protected void parseContainerAndScreen(XmlPullParser parser, int[] out) {
    //默认container是-100
        out[0] = LauncherSettings.Favorites.CONTAINER_DESKTOP;
        String strContainer = getAttributeValue(parser, ATTR_CONTAINER);
        if (strContainer != null) {
        //有设置container,就用设置的
            out[0] = Integer.parseInt(strContainer);
        }
        out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_SCREEN));
    }

9.AppShortcutParser

AutoInstallsLayout.java的内部类,有各种各样的解析器,我们研究一种就行了。

    protected class AppShortcutParser implements TagParser {

9.1.parseAndAdd

        public int parseAndAdd(XmlPullParser parser) {
            final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
            final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
    //参考3.4 有包名和类名的,走if
    //参考2.4,没有包名和类名的,走else,另外一种解析方法,具体实现在子类。
            if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
                ActivityInfo info;
                try {
                    ComponentName cn;
                    try {
                        cn = new ComponentName(packageName, className);
                        info = mPackageManager.getActivityInfo(cn, 0);
                    } catch (PackageManager.NameNotFoundException nnfe) {
                        String[] packages = mPackageManager.currentToCanonicalPackageNames(
                                new String[]{packageName});
                        cn = new ComponentName(packages[0], className);
                        info = mPackageManager.getActivityInfo(cn, 0);
                    }
                    final Intent intent = new Intent(Intent.ACTION_MAIN, null)
                            .addCategory(Intent.CATEGORY_LAUNCHER)
                            .setComponent(cn)
                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);

                    return addShortcut(info.loadLabel(mPackageManager).toString(),
                            intent, Favorites.ITEM_TYPE_APPLICATION);
                } catch (PackageManager.NameNotFoundException e) {
                    Log.e(TAG, "Favorite not found: " + packageName + "/" + className);
                }
                return -1;
            } else {
                return invalidPackageOrClass(parser);
            }
        }

>addShortcut

这里的callback是构造方法里传过来的,就是小节7的databaseHelper

    protected int addShortcut(String title, Intent intent, int type) {
        int id = mCallback.generateNewItemId();
        mValues.put(Favorites.INTENT, intent.toUri(0));
        mValues.put(Favorites.TITLE, title);
        mValues.put(Favorites.ITEM_TYPE, type);
        mValues.put(Favorites.SPANX, 1);
        mValues.put(Favorites.SPANY, 1);
        mValues.put(Favorites._ID, id);
        //见7.6
        if (mCallback.insertAndCheck(mDb, mValues) < 0) {
            return -1;
        } else {
            return id;
        }
    }

10.HotseatEduDialog

image.png

//父类见小节11,可以看到是个线性布局
public class HotseatEduDialog extends AbstractSlideInView<Launcher> implements Insettable {

10.1.predicted_hotseat_edu.xml

10.7里用这个布局

<com.android.launcher3.hybridhotseat.HotseatEduDialog xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:launcher="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_gravity="bottom"
    android:layout_height="wrap_content"
    android:gravity="bottom"
    android:orientation="vertical">
    <!--顶部固定的padding,不带背景的-->
    <View
        android:layout_width="match_parent"
        android:layout_height="32dp"
        android:backgroundTint="?attr/eduHalfSheetBGColor"
        android:background="@drawable/bottom_sheet_top_border" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/eduHalfSheetBGColor" //背景颜色
        android:orientation="vertical">
<!--图上第一行文字-->
        <TextView
            style="@style/TextHeadline"
            android:id="@+id/hotseat_edu_heading"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="18dp"
            android:paddingLeft="@dimen/bottom_sheet_edu_padding"
            android:paddingRight="@dimen/bottom_sheet_edu_padding"
            android:text="@string/hotseat_edu_title_migrate"
            android:fontFamily="google-sans"
            android:textAlignment="center"
            android:textColor="@android:color/white"
            android:textSize="20sp" />
<!--图上第二行文字-->
        <TextView
            android:layout_width="match_parent"
            android:id="@+id/hotseat_edu_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="18dp"
            android:layout_marginBottom="18dp"
            android:fontFamily="roboto-medium"
            android:paddingLeft="@dimen/bottom_sheet_edu_padding"
            android:paddingRight="@dimen/bottom_sheet_edu_padding"
            android:text="@string/hotseat_edu_message_migrate"
            android:textAlignment="center"
            android:textColor="@android:color/white"
            android:textSize="16sp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/hotseat_wrapper"
            android:orientation="vertical">
        <!--这个就是显示图标的容器-->
            <com.android.launcher3.CellLayout
                android:id="@+id/sample_prediction"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                launcher:containerType="hotseat" />

            <LinearLayout
                android:id="@+id/button_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingLeft="@dimen/bottom_sheet_edu_padding"
                android:paddingTop="8dp"
                android:paddingRight="@dimen/bottom_sheet_edu_padding">

                <FrameLayout
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical"
                    android:layout_weight=".4">
                    <Button
                        android:id="@+id/no_thanks"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:background="?android:attr/selectableItemBackground"
                        android:text="@string/hotseat_edu_dismiss"
                        android:layout_gravity="start|center_vertical"
                        android:textColor="@android:color/white"/>
                </FrameLayout>
                <!--右侧按钮,右侧间距在10.3里动态调整过-->
                <FrameLayout
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical"
                    android:layout_weight=".6">
                    <Button
                        android:id="@+id/turn_predictions_on"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:background="?android:attr/selectableItemBackground"
                        android:layout_gravity="end|center_vertical"
                        android:text="@string/hotseat_edu_accept"
                        android:textColor="@android:color/white"/>
                </FrameLayout>

            </LinearLayout>
        </LinearLayout>
    </LinearLayout>

</com.android.launcher3.hybridhotseat.HotseatEduDialog>

10.2.onLayout

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //可以看到布局完整以后默认是close状态,见11.3
        setTranslationShift(TRANSLATION_SHIFT_CLOSED);
    }

10.3.onFinishInflate

    protected void onFinishInflate() {
        super.onFinishInflate();
        //文本下边的容器
        mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
        //显示图标的cellLayout
        mSampleHotseat = findViewById(R.id.sample_prediction);

        Context context = getContext();
        DeviceProfile grid = mActivityContext.getDeviceProfile();
        Rect padding = grid.getHotseatLayoutPadding(context);
        //设置高度,大小
        mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
        mSampleHotseat.setGridSize(grid.numShownHotseatIcons, 1);
        mSampleHotseat.setPadding(padding.left, 0, padding.right, 0);
        //右边按钮
        Button turnOnBtn = findViewById(R.id.turn_predictions_on);
        turnOnBtn.setOnClickListener(this::onAccept);//补充1
        //左边按钮
        mDismissBtn = findViewById(R.id.no_thanks);
        mDismissBtn.setOnClickListener(this::onDismiss);//补充2
        //2个按钮所在的容器
        LinearLayout buttonContainer = findViewById(R.id.button_container);
        //hotseatBarEndOffset主要就是偏移了3个导航按钮的距离,包括间距
        int adjustedMarginEnd = grid.hotseatBarEndOffset - buttonContainer.getPaddingEnd();
        if (InvariantDeviceProfile.INSTANCE.get(context)
                .getDeviceProfile(context).isTaskbarPresent && adjustedMarginEnd > 0) {
            //调整margin
            ((LinearLayout.LayoutParams) buttonContainer.getLayoutParams()).setMarginEnd(
                    adjustedMarginEnd);
        }
    }

>1.onAccept

接受建议按钮的点击事件

    private void onAccept(View v) {
        mHotseatEduController.migrate();//13.1数据迁移
        handleClose(true);

        mHotseatEduController.moveHotseatItems();//13.2重新绑定数据
        mHotseatEduController.finishOnboarding();
    }

>2.onDismiss

不需要建议的按钮点击事件

    private void onDismiss(View v) {
        mHotseatEduController.showDimissTip();//13.3提示
        mHotseatEduController.finishOnboarding();
        
        handleClose(true);
    }

10.4.isOfType

判断是否是给定的类型

    protected boolean isOfType(int type) {
        return (type & TYPE_ON_BOARD_POPUP) != 0;
    }

10.5.setInsets

    public void setInsets(Rect insets) {
        int leftInset = insets.left - mInsets.left;
        int rightInset = insets.right - mInsets.right;
        int bottomInset = insets.bottom - mInsets.bottom;
        mInsets.set(insets);
        if (mActivityContext.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
            setPadding(leftInset, getPaddingTop(), rightInset, 0);
            mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
                    mHotseatWrapper.getPaddingRight(), bottomInset);
            //垂直方向,底部容器高度是固定的了
            mHotseatWrapper.getLayoutParams().height =
                    mActivityContext.getDeviceProfile().hotseatBarSizePx + insets.bottom;

        } else {
            setPadding(0, getPaddingTop(), 0, 0);
            mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
                    mHotseatWrapper.getPaddingRight(),
                    (int) getResources().getDimension(R.dimen.bottom_sheet_edu_padding));
            //横屏下文字提示有点变化
            ((TextView) findViewById(R.id.hotseat_edu_heading)).setText(
                    R.string.hotseat_edu_title_migrate_landscape);
            ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
                    R.string.hotseat_edu_message_migrate_landscape);
        }
    }

10.6.show

    public void show(List<WorkspaceItemInfo> predictions) {
        if (getParent() != null //parent不为空说明已经显示了
                || predictions.size() < mActivityContext.getDeviceProfile().numShownHotseatIcons//提供的数据不足
                || mHotseatEduController == null) {//controller还没设置
            return;
        }
        //见12.5,关闭所有打开的悬浮view
        AbstractFloatingView.closeAllOpenViews(mActivityContext);
        //见11.2,把自己添加到容器里
        attachToContainer();
        animateOpen();//补充1,带动画打开
        populatePreview(predictions);//补充2,添加图标
    }

>1.animateOpen

    private void animateOpen() {
        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
        //已经打开或者正在打开
            return;
        }
        mIsOpen = true;
        //动画,和close逻辑一样,这里反过来,参数为TRANSLATION_SHIFT_OPENED
        mOpenCloseAnimator.setValues(
                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
        mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
        mOpenCloseAnimator.start();
    }

>2.populatePreview

给容器添加数据

    private void populatePreview(List<WorkspaceItemInfo> predictions) {
        for (int i = 0; i < mActivityContext.getDeviceProfile().numShownHotseatIcons; i++) {
            WorkspaceItemInfo info = predictions.get(i);
            PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info);
            icon.setEnabled(false);
            icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
            icon.verifyHighRes();
            CellLayoutLayoutParams lp = new CellLayoutLayoutParams(i, 0, 1, 1, -1);
            mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true);
        }
    }

10.7.getDialog

加载布局

    public static HotseatEduDialog getDialog(Launcher launcher) {
        LayoutInflater layoutInflater = LayoutInflater.from(launcher);
        return (HotseatEduDialog) layoutInflater.inflate(
                R.layout.predicted_hotseat_edu, launcher.getDragLayer(),
                false);

    }

11.AbstractSlideInView

父类见小节12

public abstract class AbstractSlideInView<T extends Context & ActivityContext>
        extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {

11.1.构造方法

    public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mActivityContext = ActivityContext.lookupContext(context);

        mScrollInterpolator = Interpolators.SCROLL_CUBIC;
        mSwipeDetector = new SingleAxisSwipeDetector(context, this,
                SingleAxisSwipeDetector.VERTICAL);

        mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
        mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mSwipeDetector.finishedScrolling();
                announceAccessibilityChanges();
            }
        });
        int scrimColor = getScrimColor(context);
        mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
    }

>1.createColorScrim

背景蒙版view

    protected View createColorScrim(Context context, int bgColor) {
        View view = new View(context);
        view.forceHasOverlappingRendering(false);
        view.setBackgroundColor(bgColor);

        BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT);
        lp.ignoreInsets = true;
        view.setLayoutParams(lp);

        return view;
    }

11.2.attachToContainer

    protected void attachToContainer() {
        if (mColorScrim != null) {
            getPopupContainer().addView(mColorScrim);
        }
        getPopupContainer().addView(this);
    }

>1.getPopupContainer

获取容器

    protected BaseDragLayer getPopupContainer() {
        return mActivityContext.getDragLayer();
    }

11.3.setTranslationShift

  • translationShift就是移动的比例
    protected void setTranslationShift(float translationShift) {
        mTranslationShift = translationShift;
        mContent.setTranslationY(mTranslationShift * getShiftRange());
        if (mColorScrim != null) {
            mColorScrim.setAlpha(1 - mTranslationShift);
        }
    }

>1.getShiftRange

移动的范围也就是容器的高度

    protected float getShiftRange() {
        return mContent.getHeight();
    }

>2.TRANSLATION_SHIFT_

打开关闭状态的移动比例

    protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
    protected static final float TRANSLATION_SHIFT_OPENED = 0f;

11.4.handleClose

    protected void handleClose(boolean animate, long defaultDuration) {
        if (!mIsOpen) {
            return;
        }
        Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed);

        if (!animate) {//不需要动画
            mOpenCloseAnimator.cancel();
            //见11.3,移动y到屏幕外
            setTranslationShift(TRANSLATION_SHIFT_CLOSED);
            onCloseComplete();
            return;
        }
        //需要动画,通过TRANSLATION_SHIFT处理,见补充2
        mOpenCloseAnimator.setValues(
                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
        mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
            //动画结束处理
                mOpenCloseAnimator.removeListener(this);
                onCloseComplete();
            }
        });
        if (mSwipeDetector.isIdleState()) {
            mOpenCloseAnimator
                    .setDuration(defaultDuration)
                    .setInterpolator(getIdleInterpolator());
        } else {
            mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
        }
        //开启动画
        mOpenCloseAnimator.start();
    }

>1.onCloseComplete

视图关闭完成以后的处理

    protected void onCloseComplete() {
        mIsOpen = false;//修改标记
        getPopupContainer().removeView(this);//移除view
        if (mColorScrim != null) {
            getPopupContainer().removeView(mColorScrim);
        }
        //有监听的话回调监听
        mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed);
    }

>2.TRANSLATION_SHIFT

自定义的移动属性,

    protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
            new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {

                @Override
                public Float get(AbstractSlideInView view) {
                    return view.mTranslationShift;
                }

                @Override
                public void set(AbstractSlideInView view, Float value) {
                    view.setTranslationShift(value);//见11.3
                }
            };

12.AbstractFloatingView

public abstract class AbstractFloatingView extends LinearLayout implements TouchController {

12.1.close

    public final void close(boolean animate) {
        animate &= areAnimatorsEnabled();
        if (mIsOpen) {
            // Add to WW logging
        }
        //子类实现,最终见11.4
        handleClose(animate);
        mIsOpen = false;
    }

12.2.onBackPressed

后退的时候关闭

    public boolean onBackPressed() {
        close(true);
        return true;
    }

12.3.getView

根据提供的type查找对应的控件

    private static <T extends AbstractFloatingView> T getView(
            ActivityContext activity, @FloatingViewType int type, boolean mustBeOpen) {
        //获取容器
        BaseDragLayer dragLayer = activity.getDragLayer();
        if (dragLayer == null) return null;
        //循环所有的child
        for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) {
            View child = dragLayer.getChildAt(i);
            //类型判断
            if (child instanceof AbstractFloatingView) {
                AbstractFloatingView view = (AbstractFloatingView) child;
                //是否是指定的类型,必须打开为false或者是打开状态
                if (view.isOfType(type) && (!mustBeOpen || view.isOpen())) {
                    return (T) view;
                }
            }
        }
        return null;
    }

12.4.closeOpenViews

关闭type对应的views

    public static void closeOpenViews(ActivityContext activity, boolean animate,
            @FloatingViewType int type) {
        BaseDragLayer dragLayer = activity.getDragLayer();
        // Iterate in reverse order. AbstractFloatingView is added later to the dragLayer,
        // and will be one of the last views.
        for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) {
            View child = dragLayer.getChildAt(i);
            //循环所有child
            if (child instanceof AbstractFloatingView) {
                AbstractFloatingView abs = (AbstractFloatingView) child;
                //看是否是对应的type,是的话关闭child
                if (abs.isOfType(type)) {
                    abs.close(animate);
                }
            }
        }
    }

12.5.closeAllOpenViews

    public static void closeAllOpenViews(ActivityContext activity) {
        closeAllOpenViews(activity, true);
    }

    public static void closeAllOpenViews(ActivityContext activity, boolean animate) {
        closeOpenViews(activity, animate, TYPE_ALL);//见12.4
        activity.finishAutoCancelActionMode();
    }

13.HotseatEduController.java

13.1.migrate

    void migrate() {
        HotseatRestoreHelper.createBackup(mLauncher);
        migrateHotseatWhole();//补充1
        //已启动应用建议的提示
        Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled,
                R.string.hotseat_prediction_settings, null,
                //设置按钮的点击事件,intent见补充2
                () -> mLauncher.startActivity(getSettingsIntent()));
    }

>1.migrateHotseatWhole

  • 首先需要知道,hotseat里有两种图标,一种是固定的pinned,一种是推荐的predicted
    private int migrateHotseatWhole() {
        Workspace<?> workspace = mLauncher.getWorkspace();

        int pageId = -1;
        int toRow = 0;
        for (int i = 0; i < workspace.getPageCount(); i++) {
            CellLayout target = workspace.getScreenWithId(workspace.getScreenIdForPageIndex(i));
            if (target.makeSpaceForHotseatMigration(true)) {
                toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
                pageId = i;
                break;
            }
        }
        if (pageId == -1) {
            pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
                    .getInt(LauncherSettings.Settings.EXTRA_VALUE);
            mNewScreens = IntArray.wrap(pageId);
        }
        //横屏模式hotseat在右侧显示,平板是false
        boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout();
        int hotseatItemsNum = mLauncher.getDeviceProfile().numShownHotseatIcons;
        for (int i = 0; i < hotseatItemsNum; i++) {
            int x = isPortrait ? i : 0;
            int y = isPortrait ? 0 : hotseatItemsNum - i - 1;
            //循环获取当前hotseat里的child
            View child = mHotseat.getChildAt(x, y);
            if (child == null || child.getTag() == null) continue;
            ItemInfo tag = (ItemInfo) child.getTag();
            //如果child本身就是推荐类型的,不做处理
            if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) continue;
            //修改数据里的存储位置
            mLauncher.getModelWriter().moveItemInDatabase(tag,
                    LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow);
            //加入集合
            mNewItems.add(tag);
        }
        return pageId;
    }

>2.getSettingsIntent

    public static final String SETTINGS_ACTION =
            "android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS";

    static Intent getSettingsIntent() {
        return new Intent(SETTINGS_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    }

13.2.moveHotseatItems

    void moveHotseatItems() {
    //清空hotaset容器
        mHotseat.removeAllViewsInLayout();
        //数据来源13.1
        if (!mNewItems.isEmpty()) {
            int lastPage = mNewItems.get(mNewItems.size() - 1).screenId;
            ArrayList<ItemInfo> animated = new ArrayList<>();
            ArrayList<ItemInfo> nonAnimated = new ArrayList<>();

            for (ItemInfo info : mNewItems) {
                if (info.screenId == lastPage) {
                    animated.add(info);
                } else {
                    nonAnimated.add(info);
                }
            }
            //重新绑定数据
            mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated);
        }
    }

13.3.showDimissTip

    void showDimissTip() {
    //hotseat当前显示的child个数不满
        if (mHotseat.getShortcutsAndWidgets().getChildCount()
                < mLauncher.getDeviceProfile().numShownHotseatIcons) {
           //提示文字: 应用建议已添加到空白区域    
            Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
                    R.string.hotseat_prediction_settings, null,
                    //设置按钮同13.1.2
                    () -> mLauncher.startActivity(getSettingsIntent()));
        } else {
            showHotseatArrowTip(true, mLauncher.getString(R.string.hotseat_tip_no_empty_slots));//将应用拖离底部,以获取应用建议
        }
    }

>1.showHotseatArrowTip

  • usePinned为true表示查找第一个pinned数据,为false则使用第一个找到的predicted数据
  • 作用就是找到合适的child,上边显示个带箭头的文字提示
    private boolean showHotseatArrowTip(boolean usePinned, String message) {
        int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
        boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout();

        BubbleTextView tipTargetView = null;
        for (int i = childCount - 1; i > -1; i--) {
            int x = isPortrait ? i : 0;
            int y = isPortrait ? 0 : i;
            View v = mHotseat.getShortcutsAndWidgets().getChildAt(x, y);
            if (v instanceof BubbleTextView && v.getTag() instanceof WorkspaceItemInfo) {
                ItemInfo info = (ItemInfo) v.getTag();
                boolean isPinned = info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
                if (isPinned == usePinned) {
                    tipTargetView = (BubbleTextView) v;
                    break;
                }
            }
        }
        if (tipTargetView == null) {
            Log.e(TAG, "Unable to find suitable view for ArrowTip");
            return false;
        }
        Rect bounds = Utilities.getViewBounds(tipTargetView);
        new ArrowTipView(mLauncher).show(message, Gravity.END, bounds.centerX(), bounds.top);
        return true;
    }

13.4.showEdu

  • requiresMigration为true说明当前hotseat里有pinned的数据
  • canMigrateToFirstPage
    void showEdu() {
        //hotseat下的child个数
        int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
        CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID);
        // hotseat is already empty and does not require migration. show edu tip
        //循环所有的child,如果非predicted的数据,返回true
        boolean requiresMigration = IntStream.range(0, childCount).anyMatch(i -> {
            View v = mHotseat.getShortcutsAndWidgets().getChildAt(i);
            return v != null && v.getTag() != null && ((ItemInfo) v.getTag()).container
                    != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
        });
        //有点复杂,就是判断是否有足够的控件展示数据?
        boolean canMigrateToFirstPage = cellLayout.makeSpaceForHotseatMigration(false);
        if (requiresMigration && canMigrateToFirstPage) {
            showDialog();//补充1,推荐提示框
        } else {
            //不满足显示弹框的条件,那么这里在合适的图标上边显示个带箭头的提示文字
            if (showHotseatArrowTip(requiresMigration, mLauncher.getString(
//requiresMigration为true,文字为:将应用拖离底部,以获取应用建议
//requiresMigration为false,文字为:最常用的应用会显示在此处,显示的项目会根据日常安排而发生变化
                    requiresMigration ? R.string.hotseat_tip_no_empty_slots
                            : R.string.hotseat_auto_enrolled))) {
            }
            finishOnboarding();
        }
    }

>1.showDialog

    void showDialog() {
        if (mPredictedApps == null || mPredictedApps.isEmpty()) {
        //没有推荐数据
            return;
        }
        if (mActiveDialog != null) {
        //关闭旧的
            mActiveDialog.handleClose(false);
        }
        //创建新的
        mActiveDialog = HotseatEduDialog.getDialog(mLauncher);
        mActiveDialog.setHotseatEduController(this);
        //显示
        mActiveDialog.show(mPredictedApps);
    }

14.HotseatPredictionController.java

14.1.showEdu

显示推荐应用提示框

    public void showEdu() {
    //首先回到normal状态,也就是默认的桌面
        mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
        //新建一个对象
            HotseatEduController eduController = new HotseatEduController(mLauncher);
            //设置推荐数据
            eduController.setPredictedApps(mPredictedItems.stream()
                    .map(i -> (WorkspaceItemInfo) i)
                    .collect(Collectors.toList()));
            //显示,13.4
            eduController.showEdu();
        }));
    }

14.2.mPredictedItems数据

来源这里简单贴下,其他帖子里学习过了。

  • QuickstepModelDelegate类里有注册监听predicted数据的变化
  • 数据变化后设置给QuickstepLauncher.java里的bindExtraContainerItems,这里边会调用HotseatPredictionController.java对象里的setPredictedItems方法更新数据

15.QuickstepOnboardingPrefs

新用户引导控制台

public class QuickstepOnboardingPrefs extends OnboardingPrefs<QuickstepLauncher> {

15.1.构造方法

  • HOME_BOUNCE_SEEN的作用??
  • HOTSEAT_DISCOVERY_TIP_COUNT的作用见小节14,效果见小节10,弹个提示框
  • ALL_APPS_VISITED_COUNT的作用见小节7 AppsDividerView,决定是显示一条线还是一个文字提示
  • TYPE_ALL_APPS_EDU提示效果见17,与HINT_STATE状态有关,暂时不清楚这是啥状态??
    public QuickstepOnboardingPrefs(QuickstepLauncher launcher, SharedPreferences sharedPrefs) {
        super(launcher, sharedPrefs);

        StateManager<LauncherState> stateManager = launcher.getStateManager();
        //待研究?
        if (!getBoolean(HOME_BOUNCE_SEEN)) {
            stateManager.addStateListener(new StateListener<LauncherState>() {
                @Override
                public void onStateTransitionComplete(LauncherState finalState) {
                //手势导航模式
                    boolean swipeUpEnabled =
                            DisplayController.getNavigationMode(mLauncher).hasGestures;
                    //旧状态
                    LauncherState prevState = stateManager.getLastState();
                    //条件1:手势导航,最终状态是recent
                    //条件2:非手势导航,最终状态是allApps
                    //条件3:对应的事件触发次数达到最大值
                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
                            && finalState == ALL_APPS && prevState == NORMAL) ||
                            hasReachedMaxCount(HOME_BOUNCE_COUNT))) {
                        //修改为true
                        mSharedPrefs.edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
                        //移除监听
                        stateManager.removeStateListener(this);
                    }
                }
            });
        }
//HOTSEAT_DISCOVERY_TIP_COUNT对应的事件没有达到最大值,添加状态监听
        if (!Utilities.IS_RUNNING_IN_TEST_HARNESS
                && !hasReachedMaxCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
            
            stateManager.addStateListener(new StateListener<LauncherState>() {
                boolean mFromAllApps = false;

                @Override
                public void onStateTransitionStart(LauncherState toState) {
                //起始状态是allApps
                    mFromAllApps = mLauncher.getStateManager().getCurrentStableState() == ALL_APPS;
                }

                @Override
                public void onStateTransitionComplete(LauncherState finalState) {
                    HotseatPredictionController client = mLauncher.getHotseatPredictionController();
                    //从allApps页面回到默认桌面,并且有推荐数据
                    if (mFromAllApps && finalState == NORMAL && client.hasPredictions()) {
                        //增加事件次数,见16.3
                        if (incrementEventCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
                        //到达最大值,显示提示
                            client.showEdu();
                            //移除监听
                            stateManager.removeStateListener(this);
                        }
                    }
                }
            });
        }
        //手势导航前提下
        if (DisplayController.getNavigationMode(launcher) == NO_BUTTON) {
            stateManager.addStateListener(new StateListener<LauncherState>() {
            //滑动触发edu的最大次数
                private static final int MAX_NUM_SWIPES_TO_TRIGGER_EDU = 3;

                // Counts the number of consecutive swipes on nav bar without moving screens.
                private int mCount = 0;
                private boolean mShouldIncreaseCount;

                @Override
                public void onStateTransitionStart(LauncherState toState) {
                    if (toState == NORMAL) {
                    //目标状态是normal,不做处理
                        return;
                    }
                    //是否应该增加触发次数,2个条件
                    mShouldIncreaseCount = toState == HINT_STATE
                            && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE;
                }

                @Override
                public void onStateTransitionComplete(LauncherState finalState) {
                    if (finalState == NORMAL) {
                        //到达最大值
                        if (mCount >= MAX_NUM_SWIPES_TO_TRIGGER_EDU) {
                                //allapp的相关提示不存在
                            if (getOpenView(mLauncher, TYPE_ALL_APPS_EDU) == null) {
                                //显示对应的ui
                                AllAppsEduView.show(launcher);
                            }
                            //重置count
                            mCount = 0;
                        }
                        return;
                    }

                //应该增加count并且最终状态是hint的话,count加一,否则重置为0
                    if (mShouldIncreaseCount && finalState == HINT_STATE) {
                        mCount++;
                    } else {
                        mCount = 0;
                    }
                    //最终状态是allApps
                    if (finalState == ALL_APPS) {
                    //如果有在现实的allAppsEdu提示ui,那么隐藏
                        AllAppsEduView view = getOpenView(mLauncher, TYPE_ALL_APPS_EDU);
                        if (view != null) {
                            view.close(false);
                        }
                    }
                }
            });
        }
        //切换到allApps页面的次数没有到达最大值
        if (!hasReachedMaxCount(ALL_APPS_VISITED_COUNT)) {
            mLauncher.getStateManager().addStateListener(new StateListener<LauncherState>() {
                @Override
                public void onStateTransitionComplete(LauncherState finalState) {
                    if (finalState == ALL_APPS) {
                        //allapps页面,到达一次,次数加一
                        incrementEventCount(ALL_APPS_VISITED_COUNT);
                        return;//返回
                    }
                    //非allApps页面,判断是否到达最大值
                    boolean hasReachedMaxCount = hasReachedMaxCount(ALL_APPS_VISITED_COUNT);
                    //默认显示all apps标签,到达最大值以后不再显示
                    mLauncher.getAppsView().getFloatingHeaderView().findFixedRowByType(
                            AppsDividerView.class).setShowAllAppsLabel(!hasReachedMaxCount);
                    if (hasReachedMaxCount) {
                    //到达最大值,移除监听
                        mLauncher.getStateManager().removeStateListener(this);
                    }
                }
            });
        }
    }

16.父类OnboardingPrefs.java

16.1.MAX_COUNTS

不同事件定义的最大次数,到达最大值会做一些事情

    private static final Map<String, Integer> MAX_COUNTS;

    static {
        Map<String, Integer> maxCounts = new ArrayMap<>(5);
        maxCounts.put(HOME_BOUNCE_COUNT, 3);
        maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
        maxCounts.put(SEARCH_SNACKBAR_COUNT, 3);
        // This is the sum of all onboarding cards. Currently there is only 1 card shown 3 times.
        maxCounts.put(SEARCH_ONBOARDING_COUNT, 3);
        maxCounts.put(ALL_APPS_VISITED_COUNT, 20);
        MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
    }

16.2.hasReachedMaxCount

判断给定的事件是否达到设置的最大数


    public boolean hasReachedMaxCount(@EventCountKey String eventKey) {
        return hasReachedMaxCount(getCount(eventKey), eventKey);
    }

    private boolean hasReachedMaxCount(int count, @EventCountKey String eventKey) {
    //当前值和最大值比较
        return count >= MAX_COUNTS.get(eventKey);
    }

>1.getCount

读取本地存储的值

    public int getCount(@EventCountKey String key) {
        return mSharedPrefs.getInt(key, 0);
    }

16.3.incrementEventCount

增加前后判断是否到达最大值

    public boolean incrementEventCount(@EventCountKey String eventKey) {
        int count = getCount(eventKey);
        //见16.2
        if (hasReachedMaxCount(count, eventKey)) {
        //到达最大值
            return true;
        }
        count++;//次数加一并保存
        mSharedPrefs.edit().putInt(eventKey, count).apply();
        //再次判断
        return hasReachedMaxCount(count, eventKey);
    }

16.4.ALL_PREF_KEYS

如果有新加的key,记得也放到这个map里,方便到时候reset

    public static final Map<String, String[]> ALL_PREF_KEYS = Map.of(
            "All Apps Bounce", new String[] { HOME_BOUNCE_SEEN, HOME_BOUNCE_COUNT },
            "Hybrid Hotseat Education", new String[] { HOTSEAT_DISCOVERY_TIP_COUNT,
                    HOTSEAT_LONGPRESS_TIP_SEEN },
            "Search Education", new String[] { SEARCH_KEYBOARD_EDU_SEEN, SEARCH_SNACKBAR_COUNT,
                    SEARCH_ONBOARDING_COUNT, QSB_SEARCH_ONBOARDING_CARD_DISMISSED},
            "Taskbar Education", new String[] { TASKBAR_EDU_SEEN },
            "All Apps Visited Count", new String[] {ALL_APPS_VISITED_COUNT}
    );

16.4.launcher settings

上边的ALL_PREF_KEYS是在launcher的settings页面里用到了。

>1.launcher_preferences.xml

选项如下:

    <androidx.preference.PreferenceScreen
        android:key="pref_developer_options"
        android:persistent="false"
        android:title="@string/developer_options_title"
        android:fragment="com.android.launcher3.settings.DeveloperOptionsFragment"/>

ps:这个是aosp里默认的布局,如果带有谷歌的GMS套件的话,可能被重写,不一定用的这个,仅供参考。

>2.updateDeveloperOption

                case DEVELOPER_OPTIONS_KEY:
                    mDeveloperOptionPref = preference;
                    return updateDeveloperOption();
            }
            
        private boolean updateDeveloperOption() {
        //选项是否可见
            boolean showPreference = FeatureFlags.showFlagTogglerUi(getContext())
                    || PluginManagerWrapper.hasPlugins(getContext());//这个值是测试用的
            if (mDeveloperOptionPref != null) {
                mDeveloperOptionPref.setEnabled(showPreference);
                if (showPreference) {
                    getPreferenceScreen().addPreference(mDeveloperOptionPref);
                } else {
                    getPreferenceScreen().removePreference(mDeveloperOptionPref);
                }
            }
            return showPreference;
        }            

可以看到,显示的条件是: debug系统并且开发者模式打开

    public static boolean showFlagTogglerUi(Context context) {
        return Utilities.IS_DEBUG_DEVICE && Utilities.isDevelopersOptionsEnabled(context);
    }
    public static final boolean IS_DEBUG_DEVICE =
            Build.TYPE.toLowerCase(Locale.ROOT).contains("debug") ||
            Build.TYPE.toLowerCase(Locale.ROOT).equals("eng");
    public static boolean isDevelopersOptionsEnabled(Context context) {
        return Settings.Global.getInt(context.getApplicationContext().getContentResolver(),
                        Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
    }            

>3.DeveloperOptionsFragment.java

这个就是添加的reset选项

    private void addOnboardingPrefsCatergory() {
        PreferenceCategory onboardingCategory = newCategory("Onboarding Flows");
        onboardingCategory.setSummary("Reset these if you want to see the education again.");
        //循环map,每个entry添加一个选项
        for (Map.Entry<String, String[]> titleAndKeys : OnboardingPrefs.ALL_PREF_KEYS.entrySet()) {
            String title = titleAndKeys.getKey();
            //每个entry包含的key集合
            String[] keys = titleAndKeys.getValue();
            Preference onboardingPref = new Preference(getContext());
            onboardingPref.setTitle(title);
            onboardingPref.setSummary("Tap to reset");
            //点击事件
            onboardingPref.setOnPreferenceClickListener(preference -> {
                SharedPreferences.Editor sharedPrefsEdit = LauncherPrefs.getPrefs(getContext())
                        .edit();
                //点击以后就是循环选项所包含的key,删除对应的数据
                for (String key : keys) {
                    sharedPrefsEdit.remove(key);
                }
                sharedPrefsEdit.apply();
                return true;
            });
            onboardingCategory.addPreference(onboardingPref);
        }
    }

17.AllAppsEduView.java

这个视图是用来展示在手势模式下如何进入allApps页面

public class AllAppsEduView extends AbstractFloatingView {

17.1.构造方法

    public AllAppsEduView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //64dp的实心圆
        mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle);
        mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size);
        mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding);
        mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width);
        mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height);
        setWillNotDraw(false);
    }

17.2.show

    public static void show(Launcher launcher) {
        final DragLayer dragLayer = launcher.getDragLayer();
        //加载布局,见补充1
        AllAppsEduView view = (AllAppsEduView) launcher.getLayoutInflater().inflate(
                R.layout.all_apps_edu_view, dragLayer, false);
        //见17.3
        view.init(launcher);
        //加入容器里
        launcher.getDragLayer().addView(view);
        //刷新布局并播放动画
        view.requestLayout();
        view.playAnimation();
    }

>1.all_apps_edu_view.xml

<com.android.quickstep.views.AllAppsEduView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="@dimen/swipe_edu_width"
    android:layout_height="@dimen/swipe_edu_max_height"/>

17.3.init

    private void init(Launcher launcher) {
        mLauncher = launcher;
        mTouchController = new AllAppsEduTouchController(mLauncher);

        int accentColor = Themes.getColorAccent(launcher);
        //渐变色图
        mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
                Themes.getAttrBoolean(launcher, R.attr.isMainColorDark)
                        ? new int[]{0xB3FFFFFF, 0x00FFFFFF}
                        : new int[]{ColorUtils.setAlphaComponent(accentColor, 127),
                                ColorUtils.setAlphaComponent(accentColor, 0)});
        float r = mWidthPx / 2f;
        mGradient.setCornerRadii(new float[]{r, r, r, r, 0, 0, 0, 0});

        int top = mMaxHeightPx - mCircleSizePx + mPaddingPx;
        //设置drawable尺寸
        mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx);
        mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx);

        DeviceProfile grid = launcher.getDeviceProfile();
        DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx);
        lp.ignoreInsets = true;
        lp.leftMargin = (grid.widthPx - mWidthPx) / 2;
        lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx;
        //更新布局参数
        setLayoutParams(lp);
    }

18.TaskbarEduController

18.1.showEdu

    void showEdu() {
        TaskbarOverlayController overlayController = mControllers.taskbarOverlayController;
        TaskbarOverlayContext overlayContext = overlayController.requestWindow();
        LayoutInflater layoutInflater = overlayContext.getLayoutInflater();
        //加载布局,见补充1
        mTaskbarEduView = (TaskbarEduView) layoutInflater.inflate(
                R.layout.taskbar_edu, overlayContext.getDragLayer(), false);
        //获取里边的翻页容器
        mPagedView = mTaskbarEduView.findViewById(R.id.content);
        //给翻页容器里根据手势模式或者三键导航模式添加不同的page
        layoutInflater.inflate(
                DisplayController.isTransientTaskbar(overlayContext)
                        ? R.layout.taskbar_edu_pages_transient
                        : R.layout.taskbar_edu_pages_persistent,
                mPagedView,
                true);

        // Provide enough room for taskbar.
        View startButton = mTaskbarEduView.findViewById(R.id.edu_start_button);
        ViewGroup.MarginLayoutParams layoutParams =
                (ViewGroup.MarginLayoutParams) startButton.getLayoutParams();
        DeviceProfile dp = overlayContext.getDeviceProfile();
        layoutParams.bottomMargin += DisplayController.isTransientTaskbar(overlayContext)
                ? dp.taskbarSize + dp.transientTaskbarMargin
                : dp.taskbarSize;
        //见19.1,以及18.2
        mTaskbarEduView.init(new TaskbarEduCallbacks());

        mControllers.navbarButtonsViewController.setSlideInViewVisible(true);
        mTaskbarEduView.setOnCloseBeginListener(
                () -> mControllers.navbarButtonsViewController.setSlideInViewVisible(false));
        mTaskbarEduView.addOnCloseListener(() -> mTaskbarEduView = null);
        //见19.2加入容器
        mTaskbarEduView.show();
    }

>1.taskbar_edu.xml

  • TaskbarEduView里套个ConstraintLayout
  • ConstraintLayout里上边显示TaskbarEduPagedView(里边包含多个page)
  • 底部依次是start按钮,indicator,以及end按钮
<com.android.launcher3.taskbar.TaskbarEduView 
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:gravity="bottom"
    android:orientation="vertical"
    android:layout_marginHorizontal="108dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/edu_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_rounded_corner_bottom_sheet"
        android:gravity="center_horizontal"
        android:paddingHorizontal="36dp"
        android:paddingTop="64dp">

        <com.android.launcher3.taskbar.TaskbarEduPagedView
            android:id="@+id/content"
            android:clipToPadding="false"
            android:layout_width="match_parent"
            android:layout_height="378dp"
            app:layout_constraintTop_toTopOf="parent"
            launcher:pageIndicator="@+id/content_page_indicator"/>

        <Button
            android:id="@+id/edu_start_button"
            android:layout_width="wrap_content"
            android:layout_height="36dp"
            android:layout_marginBottom="92dp"
            android:layout_marginTop="32dp"
            app:layout_constraintTop_toBottomOf="@id/content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:text="@string/taskbar_edu_close"
            style="@style/TaskbarEdu.Button.Close"
            android:textColor="?android:attr/textColorPrimary"/>

        <com.android.launcher3.pageindicators.PageIndicatorDots
            android:id="@+id/content_page_indicator"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="@id/edu_start_button"
            app:layout_constraintBottom_toBottomOf="@id/edu_start_button"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:elevation="1dp" />

        <Button
            android:id="@+id/edu_end_button"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="@id/edu_start_button"
            app:layout_constraintBottom_toBottomOf="@id/edu_start_button"
            app:layout_constraintEnd_toEndOf="parent"
            android:text="@string/taskbar_edu_next"
            style="@style/TaskbarEdu.Button.Next"
            android:textColor="?androidprv:attr/textColorOnAccent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.android.launcher3.taskbar.TaskbarEduView>

18.2.TaskbarEduCallbacks

    class TaskbarEduCallbacks {
        void onPageChanged(int prevPage, int currentPage, int pageCount) {
            // Reset previous pages' animation.
            LottieAnimationView prevAnimation = mPagedView.getChildAt(prevPage)
                    .findViewById(R.id.animation);
            prevAnimation.cancelAnimation();
            prevAnimation.setFrame(0);

            mPagedView.getChildAt(currentPage)
                    .<LottieAnimationView>findViewById(R.id.animation)
                    .playAnimation();
            //根据当前页,决定第一个按钮显示内容以及点击事假
            if (currentPage == 0) {
                mTaskbarEduView.updateStartButton(R.string.taskbar_edu_close,
                        v -> mTaskbarEduView.close(true /* animate */));
            } else {
                mTaskbarEduView.updateStartButton(R.string.taskbar_edu_previous,
                        v -> mTaskbarEduView.snapToPage(currentPage - 1));
            }
            //同样的,最后一页,
            if (currentPage == pageCount - 1) {
                mTaskbarEduView.updateEndButton(R.string.taskbar_edu_done,
                        v -> mTaskbarEduView.close(true /* animate */));
            } else {
                mTaskbarEduView.updateEndButton(R.string.taskbar_edu_next,
                        v -> mTaskbarEduView.snapToPage(currentPage + 1));
            }
        }

        int getIconLayoutBoundsWidth() {
            return mControllers.taskbarViewController.getIconLayoutWidth();
        }

        int getOpenDuration() {
            return mControllers.taskbarOverlayController.getOpenDuration();
        }

        int getCloseDuration() {
            return mControllers.taskbarOverlayController.getCloseDuration();
        }
    }

18.3.showEdu调用逻辑

>1.LauncherTaskbarUIController.java

从逻辑来看,这个东西应该只会显示一次。

    public void showEdu() {
        if (!shouldShowEdu()) {
            return;
        }
        // TASKBAR_EDU_SEEN的值标记为true
        mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.TASKBAR_EDU_SEEN);
        //走到小节18.1
        mControllers.taskbarEduController.showEdu();
    }
  • TASKBAR_EDU_SEEN的值为false
    public boolean shouldShowEdu() {
        return !Utilities.IS_RUNNING_IN_TEST_HARNESS
                && !mLauncher.getOnboardingPrefs().getBoolean(OnboardingPrefs.TASKBAR_EDU_SEEN);
    }

>2.QuickstepTransitionManager.java

  • 获取控制从应用程序图标打开目标窗口的Animator
  • 点击应用图标,应用打开的动画结束以后,会显示edu,而从补充1的逻辑,只会显示一次。
    private Animator getOpeningWindowAnimators(View v,
    //...
        appAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
                if (taskbarController != null && taskbarController.shouldShowEdu()) {
                    // LAUNCHER_TASKBAR_EDUCATION_SHOWING is set to true here, when the education
                    // flow is about to start, to avoid a race condition with other components
                    // that would show something else to the user as soon as the app is opened.
                    Settings.Secure.putInt(mLauncher.getContentResolver(),
                            LAUNCHER_TASKBAR_EDUCATION_SHOWING, 1);
                }
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (v instanceof BubbleTextView) {
                    ((BubbleTextView) v).setStayPressed(false);
                }
                LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
                if (taskbarController != null) {
                    //这里显示提示
                    taskbarController.showEdu();
                }
                openingTargets.release();
            }
        });

19.TaskbarEduView

public class TaskbarEduView extends AbstractSlideInView<TaskbarOverlayContext>
        implements Insettable {

19.1.init

主要就是给里边的pageView添加回调

    protected void init(TaskbarEduController.TaskbarEduCallbacks callbacks) {
        if (mPagedView != null) {
            mPagedView.setControllerCallbacks(callbacks);
        }
        mTaskbarEduCallbacks = callbacks;
    }

19.2.show

    public void show() {
        attachToContainer();//补充1
        animateOpen();//开启动画
    }

>1.attachToContainer

就是把自己加入容器里

    protected void attachToContainer() {
        if (mColorScrim != null) {
        //如果有蒙版的话先加入
            getPopupContainer().addView(mColorScrim, 0);
        }
        getPopupContainer().addView(this, 1);
    }

19.3.isOfType

类型如下

    protected boolean isOfType(int type) {
        return (type & TYPE_TASKBAR_EDUCATION_DIALOG) != 0;
    }