Launcher3 抽屉型桌面改造成横屏桌面

Google Launcher3默认是抽屉型的桌面,到Android 8.0依然是没有这样的功能。这样的功能是手机厂商提供给我们的,不得不说,横向排列的桌面
更适合国人的使用习惯,可能是使用iphone的习惯吧。

好,那我们如何实现这样功能呢?其实并不会太难的。

在Launcher加载流程里,我们知道桌面的数据是在LauncherModel的 LoaderTask完成加载的

我们在loadAndBindAllApps()方法调用之后添加一个verifyApplications()方法调用,为什么在这里调用呢?
因为只用当应用数据加载完全后,我们才能讲所有的应用进行横向绑定到Workspace的操作


        @Override
        public void run() {
            AppTypeHelper.configSystemAppIcon(mContext);

            synchronized (mLock) {
                if (mStopped) {
                    return;
                }
                mIsLoaderTaskRunning = true;
            }
            // Optimize for end-user experience: if the Launcher is up and // running with the
            // All Apps interface in the foreground, load All Apps first. Otherwise, load the
            // workspace first (default).
            keep_running:
            {
                if (DEBUG_LOADERS) {
                    Log.d(TAG, "step 1: loading workspace");
                }
                loadAndBindWorkspace();

                if (mStopped) {
                    break keep_running;
                }

                waitForIdle();

                // second step
                if (DEBUG_LOADERS) {
                    Log.d(TAG, "step 2: loading all apps");
                }
                loadAndBindAllApps();
            }

            if (LauncherAppState.getInstance().getInvariantDeviceProfile()
                    .isDisableAllApps) {
                verifyApplications();
            }
            // Clear out this reference, otherwise we end up holding it until all of the
            // callback runnables are done.
            mContext = null;

            synchronized (mLock) {
                // If we are still the last one to be scheduled, remove ourselves.
                if (mLoaderTask == this) {
                    mLoaderTask = null;
                }
                mIsLoaderTaskRunning = false;
                mHasLoaderCompletedOnce = true;
            }
        }

这里呢,我简单的添加了一个布尔值 LauncherAppState.getInstance().getInvariantDeviceProfile().isDisableAllApps 表示是否启用横屏桌面,小伙伴开发的时候建议做成开关的方式,以满足不同的产品需求。

verifyApplications方法里怎么实现呢?来看

 private void verifyApplications() {
        final Context context = mApp.getContext();

        // Cross reference all the applications in our apps list with items in the workspace
        ArrayList<ItemInfo> tmpInfos;
        ArrayList<ItemInfo> added = new ArrayList<ItemInfo>();
        synchronized (sBgLock) {
            for (AppInfo app : mBgAllAppsList.data) {
                tmpInfos = getItemInfoForComponentName(app.componentName, app.user);
                if (tmpInfos.isEmpty()) {
                    // ignore the apps
                    if (mIgnoreAppsList.contain(app.componentName.getPackageName())) {
                        continue;
                    }
                    // We are missing an application icon, so add this to the workspace
                    added.add(app);
                    // This is a rare event, so lets log it
                    // Log.e(TAG, "Missing Application on load: " + app);
                }
            }
        }
        if (!added.isEmpty()) {
            addAndBindAddedWorkspaceItems(context, added);
        }
    }

如果小伙伴有用心看加载流程的细节的话,在loadAndBindAllApps()方法里,会把获取到的所有应用信息保存到 AllAppsList这个类里,也就是 mBgAllAppsList.data 里面,故
我们遍历data数据,将需要绑定的数据绑定到Workspace上就可以了。这里还有一个方法 getItemInfoForComponentName ,作用是 mBgAllAppsList.data的数据跟sBgItemsIdMap里
的数据做匹配,避免因为线程的关系将不必要的数据添加到桌面

拿到数据的备份added集合后,我们使用LauncherModel里的 addAndBindAddedWorkspaceItems 方法添加item

   /**
     * Adds the provided items to the workspace.
     */
    public void addAndBindAddedWorkspaceItems(final Context context,
                                              final ArrayList<? extends ItemInfo> workspaceApps) {
        final Callbacks callbacks = getCallback();
        if (workspaceApps.isEmpty()) {
            return;
        }
        // Process the newly added applications and add them to the database first
        Runnable r = new Runnable() {
            @Override
            public void run() {
                final ArrayList<ItemInfo> addedShortcutsFinal = new ArrayList<ItemInfo>();
                final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>();

                // Get the list of workspace screens.  We need to append to this list and
                // can not use sBgWorkspaceScreens because loadWorkspace() may not have been
                // called.
                ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context);
                synchronized (sBgLock) {
                    for (ItemInfo item : workspaceApps) {

                        if (item instanceof ShortcutInfo) {
                            // Short-circuit this logic if the icon exists somewhere on the workspace
                            if (shortcutExists(context, item.getIntent(), item.user)) {
                                continue;
                            }
                        }
                        // Find appropriate space for the item.
                        Pair<Long, int[]> coords = findSpaceForItem(context,
                                workspaceScreens, addedWorkspaceScreensFinal,
                                1, 1);
                        long screenId = coords.first;
                        int[] cordinates = coords.second;

                        ItemInfo itemInfo;
                        if (item instanceof ShortcutInfo || item instanceof FolderInfo) {
                            itemInfo = item;
                        } else if (item instanceof AppInfo) {
                            itemInfo = ((AppInfo) item).makeShortcut();
                        } else {
                            throw new RuntimeException("Unexpected info type");
                        }

                        // Add the shortcut to the db
                        addItemToDatabase(context, itemInfo,
                                LauncherSettings.Favorites.CONTAINER_DESKTOP,
                                screenId, cordinates[0], cordinates[1]);
                        // Save the ShortcutInfo for binding in the workspace
                        addedShortcutsFinal.add(itemInfo);
                    }
                }

                // Update the workspace screens
                updateWorkspaceScreenOrder(context, workspaceScreens);

                if (!addedShortcutsFinal.isEmpty()) {
                    runOnMainThread(new Runnable() {
                        @Override
                        public void run() {
                            Callbacks cb = getCallback();
                            if (callbacks == cb && cb != null) {
                                final ArrayList<ItemInfo> addAnimated = new ArrayList<ItemInfo>();
                                final ArrayList<ItemInfo> addNotAnimated = new ArrayList<ItemInfo>();
                                if (!addedShortcutsFinal.isEmpty()) {
                                    ItemInfo info = addedShortcutsFinal.get(addedShortcutsFinal.size() - 1);
                                    long lastScreenId = info.screenId;
                                    for (ItemInfo i : addedShortcutsFinal) {
                                        if (i.screenId == lastScreenId) {
                                            addAnimated.add(i);
                                        } else {
                                            addNotAnimated.add(i);
                                        }
                                    }
                                }
                                callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
                                        addNotAnimated, addAnimated, null);
                            }
                        }
                    });
                }
            }
        };
        runOnWorkerThread(r);
    }

这里就跟加载流程里的绑定worksspace的Screen类似了。简单的介绍一下细节,

  1. 从数据库拿到ScreenId信息 workspaceScreens,遍历需要添加的item信息
  2. 通过findSpaceForItem 方法在workspace上找到空余的位置,如果没有位置会新创建一个Screen出来。
  3. 根据ItemInfo的类型创建ShortcutInfo,将ShortcutInfo,screen order信息更新到数据库
  4. 拿到Launcher 这个callbacks调用 bindAppsAdded,开始绑定到workspace

    @Override
    public void bindAppsAdded(final ArrayList<Long> newScreens,
                              final ArrayList<ItemInfo> addNotAnimated,
                              final ArrayList<ItemInfo> addAnimated,
                              final ArrayList<AppInfo> addedApps) {
        Log.e(TAG, "bindAppsAdded");
        Runnable r = new Runnable() {
            @Override
            public void run() {
                bindAppsAdded(newScreens, addNotAnimated, addAnimated, addedApps);
            }
        };
        if (waitUntilResume(r)) {
            return;
        }

        // Add the new screens
        if (newScreens != null) {
            bindAddScreens(newScreens);
        }

        // We add the items without animation on non-visible pages, and with
        // animations on the new page (which we will try and snap to).
        if (addNotAnimated != null && !addNotAnimated.isEmpty()) {
            bindItems(addNotAnimated, 0,
                    addNotAnimated.size(), false);
        }
        if (addAnimated != null && !addAnimated.isEmpty()) {
            bindItems(addAnimated, 0,
                    addAnimated.size(), true);
        }

        // Remove the extra empty screen
        mWorkspace.removeExtraEmptyScreen(false, false);

        if (addedApps != null && mAppsView != null) {
            mAppsView.addApps(addedApps);
        }
    }

可以发现,会先使用新生成的ScreenId创建screen,之后才开始bindItems, 如果继续往bindItems里看你就会发现,会在WorkSpace里调用addInScreenFromBind,完成图标的创建。

这里有个地方值得我们提一下,就是 waitUntilResume 方法的使用,在很多地方都会使用这个方法。 作用是在Launcher onResume的时候再执行我们的Runnable。通常,类似的操作
我们会直接在onResume调用或实现,如果操作一多,onResume里就会很臃肿,不好维护

    @Thunk
    boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) {
        if (mPaused) {
            if (LOGD) {
                Log.d(TAG, "Deferring update until onResume");
            }
            if (deletePreviousRunnables) {
                while (mBindOnResumeCallbacks.remove(run)) {
                }
            }
            mBindOnResumeCallbacks.add(run);
            return true;
        } else {
            return false;
        }
    }

这里使用的是一个状态的机制,在mPaused的状态,把需要执行的runnable添加到mBindOnResumeCallbacks,在onResume的时候在遍历出来执行即可

这样就能将抽屉型的Launcher改造成横向的Launcher了,当然改完之后可能会有一些bug,比如桌面里的应用都是 ShortcutInfo类型的,在拖拽时没有查看信息的功能等等
就需要小伙伴自己修改啦

感谢阅读~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258