Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Launcher3--抽屜

Launcher3--抽屜

編輯:關於Android編程

抽屜是用來放置安卓手機中所有需要顯示到Launcher上的(當然也可以進行過濾,將不想顯示的隱藏起來)應用和小部件,啟動應用、添加快捷方式到桌面、卸載等。之前也提到過,有些Launcher是沒有抽屜的,如MIUI的Launcher。在Launcher3中,默認是有的,當然,也提供了不顯示抽屜的方法,這個後面會說到,在此先了解下抽屜。

\\

 

一、布局   抽屜的布局文件是apps_customize_pane.xml,被include在launcher.xml中, launcher.xml
        
apps_customize_pane.xml


    

        <framelayout android:clipchildren="false" android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent">
            <framelayout android:clipchildren="false" android:cliptopadding="false" android:id="@+id/fake_page_container" android:layout_height="match_parent" android:layout_width="match_parent">
                <framelayout android:cliptopadding="false" android:id="@+id/fake_page" android:layout_height="match_parent" android:layout_width="match_parent" android:visibility="invisible">
            </framelayout>
            
        </framelayout>
        
    </framelayout>
這個就是抽屜的樹形結構,AppsCustomizeTabHost是根視圖,id/content是內容區域,包含一個FrameLayout和頁面指示器indicator,這個FrameLayout也包含兩塊,上面一塊是用作過渡頁面,下面是AppsCustomizePagedView,就是用來顯示app列表或小部件的,是最核心的部分。
  二、數據加載和顯示   首先需要弄清楚的是,並不是我們點擊了抽屜按鈕進入抽屜頁才開始加載數據的,我們之前分析了<<Launcher3的加載流程>>,知道這些數據在Launcher啟動過程中就加載了。這個也很好理解,Android系統中安裝了很多應用,如果每次打開抽屜都要加載數據,那可想而知是多麼糟糕的體驗。 這部分的數據加載就是在<<Launcher3的加載流程>>中分析的loadAndBindAllApps過程,此過程已將應用數據保存到數據庫中,並且設置到AppsCustomizePagedView中,詳細過程就不在介紹了。很明顯,這個時候要做的就是將其顯示,並將Workspace隱藏。 進入抽屜的途徑一個是點擊桌面抽屜按鈕圖標,另一個是長按桌面選擇小部件按鈕,這兩個操作其實進入的是同一個界面,只不過是根據操作的不同選擇加載應用還是小部件,那我們就以顯示應用列表來分析。
    public void onClick(View v) {
        .............
        } else if (v == mAllAppsButton) {// 抽屜按鈕
            onClickAllAppsButton(v);
        } else if (tag instanceof AppInfo) {// 應用列表中的應用
         ............
    }
    protected void onClickAllAppsButton(View v) {
        if (LOGD) Log.d(TAG, "onClickAllAppsButton");
        // copy db
        CommonUtil.copyDBToSDcard();
        // end
        if (isAllAppsVisible()) {// 抽屜頁面是否可見,實際情況在抽屜頁時,不會顯示按鈕
            showWorkspace(true);
        } else {
            showAllApps(true, AppsCustomizePagedView.ContentType.Applications, false);
        }
        if (mLauncherCallbacks != null) {
            mLauncherCallbacks.onClickAllAppsButton(v);
        }
    }
這裡根據抽屜頁是否可見來確定是顯示Workspace還是抽屜,但在實際情況中抽屜中不會顯示抽屜按鈕,所以也就不可能執行到showWorkspace這個方法中。直接看showAllApps方法,
    void showAllApps(boolean animated, AppsCustomizePagedView.ContentType contentType,
                     boolean resetPageToZero) {
        if (mState != State.WORKSPACE) return;

        if (resetPageToZero) {// 是否需要恢復到首頁
            mAppsCustomizeTabHost.reset();
        }
        showAppsCustomizeHelper(animated, false, contentType);
        mAppsCustomizeTabHost.post(new Runnable() {
            @Override
            public void run() {
                // We post this in-case the all apps view isn't yet constructed.
                mAppsCustomizeTabHost.requestFocus();// 給抽屜界面焦點
            }
        });

        // Change the state *after* we've called all the transition code
        mState = State.APPS_CUSTOMIZE;// 更新頁面狀態未APPS_CUSTOMIZE

        // Pause the auto-advance of widgets until we are out of AllApps
        mUserPresent = false;
        updateRunning();
        closeFolder();// 關閉文件夾

        // Send an accessibility event to announce the context change
        getWindow().getDecorView().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
    }
這裡面調用了showAppsCustomizeHelper方法,這是顯示抽屜的的一個幫助方法,與此方法對應的是hideAppsCustomizeHelper方法,很顯然使用隱藏抽屜時調用的,這兩個方法實現很相似,我們這裡只分析showAppsCustomizeHelper。
        if (mStateAnimation != null) {// 重置mStateAnimation
            mStateAnimation.setDuration(0);
            mStateAnimation.cancel();
            mStateAnimation = null;
        }
重置AnimatorSet,其實這個方法裡面最主要就是實現各種動畫效果,Workspace上的動畫、抽屜上的動畫。
        boolean material = Utilities.isLmpOrAbove();// sdk版本是否大於等於21

        final Resources res = getResources();

        // 定義了一些動畫時長
        final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime);
        final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime);
        final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime);
        final int itemsAlphaStagger = res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger);

        final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor);//縮放大小
        // 從Workspace切換到AppsCustomizeTabHost
        final View fromView = mWorkspace;
        final AppsCustomizeTabHost toView = mAppsCustomizeTabHost;

        final ArrayList layerViews = new ArrayList();// DragLayer上的View列表
定義了一些變量,material來判斷sdk版本,後面會根據這個布爾變量來進行不同的動畫設置,在Android L及以上采用了material design,所有在較高的版本上可以有一些更好的動畫效果。然後還定義動畫時長,縮放比例等。
        Workspace.State workspaceState = contentType == AppsCustomizePagedView.ContentType.Widgets ?
                Workspace.State.OVERVIEW_HIDDEN : Workspace.State.NORMAL_HIDDEN;
        Animator workspaceAnim = mWorkspace.getChangeStateAnimation(workspaceState, animated, layerViews);// 定義切換時Workspace上的動畫
        // 設置加載的數據類型
        if (!LauncherAppState.isDisableAllApps() || contentType == AppsCustomizePagedView.ContentType.Widgets) {
            // Set the content type for the all apps/widgets space
            mAppsCustomizeTabHost.setContentTypeImmediate(contentType);
        }
設置加載內容的類型,有兩種類型:application和widget,這裡是application類型。
        // If for some reason our views aren't initialized, don't animate
        boolean initialized = getAllAppsButton() != null;// 是否初始化完成
        animated && initialized
來判斷是否實現動畫效果,我們直接看動畫是怎麼實現的。
            mStateAnimation = LauncherAnimUtils.createAnimatorSet();// 創建AnimatorSet
            final AppsCustomizePagedView content = (AppsCustomizePagedView)
                    toView.findViewById(R.id.apps_customize_pane_content);// 抽屜內容組件

            final View page = content.getPageAt(content.getCurrentPage());// 抽屜當前頁
            final View revealView = toView.findViewById(R.id.fake_page);// 一個過渡頁面,用來實現動畫

            final boolean isWidgetTray = contentType == AppsCustomizePagedView.ContentType.Widgets;
            // 設置過渡頁面的背景,根據類型分別設置
            if (isWidgetTray) {
                revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark));
            } else {
                revealView.setBackground(res.getDrawable(R.drawable.quantum_panel));
            }
初始化抽屜頁面的組件,其中revealView是一個過渡頁,用來實現動畫效果的,動畫結束後將其隱藏。
            // 先隱藏真實頁面,顯示過渡頁面
            // Hide the real page background, and swap in the fake one
            content.setPageBackgroundsVisible(false);
            revealView.setVisibility(View.VISIBLE);
            // We need to hide this view as the animation start will be posted.
            // alpha置為0
            revealView.setAlpha(0);

            int width = revealView.getMeasuredWidth();
            int height = revealView.getMeasuredHeight();
            float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4);

            // 偏移量置為0
            revealView.setTranslationY(0);
            revealView.setTranslationX(0);

            // Get the y delta between the center of the page and the center of the all apps button
            int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView,
                    getAllAppsButton(), null);

            float alpha = 0;
            float xDrift = 0;
            float yDrift = 0;
            if (material) {// sdk > 21 ?
                alpha = isWidgetTray ? 0.3f : 1f;
                yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1];
                xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0];
            } else {
                yDrift = 2 * height / 3;
                xDrift = 0;
            }
            final float initAlpha = alpha;
動畫設置之前的一些初始化工作,將過渡頁面的透明度、偏移量都先置0,然後設置動畫時的透明度初始值和偏移量的初始值。
            revealView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            layerViews.add(revealView);
            PropertyValuesHolder panelAlpha = PropertyValuesHolder.ofFloat("alpha", initAlpha, 1f);
            PropertyValuesHolder panelDriftY = PropertyValuesHolder.ofFloat("translationY", yDrift, 0);
            PropertyValuesHolder panelDriftX = PropertyValuesHolder.ofFloat("translationX", xDrift, 0);

            ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView,
                    panelAlpha, panelDriftY, panelDriftX);

            panelAlphaAndDrift.setDuration(revealDuration);
            panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));

            mStateAnimation.play(panelAlphaAndDrift);
定義了動畫的類型、時長和變化速率等。這是一個組合動畫,很明顯動畫效果是透明度的變化和偏移量的變化。
            // 抽屜當前頁的動畫
            if (page != null) {
                page.setVisibility(View.VISIBLE);
                page.setLayerType(View.LAYER_TYPE_HARDWARE, null);
                layerViews.add(page);

                ObjectAnimator pageDrift = ObjectAnimator.ofFloat(page, "translationY", yDrift, 0);
                page.setTranslationY(yDrift);
                pageDrift.setDuration(revealDuration);
                pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
                pageDrift.setStartDelay(itemsAlphaStagger);
                mStateAnimation.play(pageDrift);

                page.setAlpha(0f);
                ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(page, "alpha", 0f, 1f);
                itemsAlpha.setDuration(revealDuration);
                itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
                itemsAlpha.setStartDelay(itemsAlphaStagger);
                mStateAnimation.play(itemsAlpha);
            }
這一段是抽屜當前頁的動畫效果,也是用屬性動畫來實現的,關於屬性動畫的使用可參考<<屬性動畫之ObjectAnimator>>。 然後是頁面指示器和sdk>21的動畫,這個就不再細說了,到動畫監聽,
            mStateAnimation.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    dispatchOnLauncherTransitionEnd(fromView, animated, false);
                    dispatchOnLauncherTransitionEnd(toView, animated, false);

                    // 隱藏過渡頁面
                    revealView.setVisibility(View.INVISIBLE);
                    revealView.setLayerType(View.LAYER_TYPE_NONE, null);
                    if (page != null) {
                        page.setLayerType(View.LAYER_TYPE_NONE, null);
                    }
                    // 顯示抽屜
                    content.setPageBackgroundsVisible(true);

                    // Hide the search bar
                    // 隱藏搜索欄
                    if (mSearchDropTargetBar != null) {
                        mSearchDropTargetBar.hideSearchBar(false);
                    }

                    // This can hold unnecessary references to views.
                    mStateAnimation = null;
                }

            });
動畫結束後:隱藏過渡頁面;顯示抽屜內容;隱藏搜索欄。
            // Workspace動畫效果
            if (workspaceAnim != null) {
                mStateAnimation.play(workspaceAnim);
            }
這個是Workspace的動畫,該動畫定義在Workspace.java的getChangeStateAnimation方法中,該方法定義了多種情況下的動畫效果,如Workspace到桌面縮略圖、桌面縮略圖到Workspace、Workspace到抽屜等等,進行alpha、scale等設置。 最後定義一個runnable執行塊,用於動畫播放,
            final Runnable startAnimRunnable = new Runnable() {
                public void run() {
                    // Check that mStateAnimation hasn't changed while
                    // we waited for a layout/draw pass
                    if (mStateAnimation != stateAnimation)
                        return;
                    dispatchOnLauncherTransitionStart(fromView, animated, false);
                    dispatchOnLauncherTransitionStart(toView, animated, false);

                    revealView.setAlpha(initAlpha);
                    if (Utilities.isLmpOrAbove()) {// sdk > 21 ?
                        for (int i = 0; i < layerViews.size(); i++) {
                            View v = layerViews.get(i);
                            if (v != null) {
                                if (Utilities.isViewAttachedToWindow(v)) v.buildLayer();
                            }
                        }
                    }
                    mStateAnimation.start();// 執行動畫
                }
            };
這樣動畫結束後,抽屜就顯示出來,該隱藏的也隱藏了。如果是沒有動畫的情況,直接設為可見就行了,但會顯得比較突兀,體驗差了點。 另外,在該方法中,多次調用了dispatchOnLauncherTransitionXXX方法,最終調用View中實現了LauncherTransitionable頁面過渡接口的方法,在切換的不同階段做相應的處理。
interface LauncherTransitionable {
    View getContent();
    void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace);
    void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace);
    void onLauncherTransitionStep(Launcher l, float t);
    void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace);
}

三、自定義修改   1、如何更換抽屜背景? Launcher3中,抽屜內容的背景默認是白色的,如果想改成透明的,該怎麼修改? 一般情況下,我們首先想到的是在布局文件中找到AppsCustomizePagedView的布,然後將背景設為透明的,
            
這個方法顯然是不能實現的,因為在AppsCustomizePagedView中還有一層AppsCustomizeCellLayout,一個列表頁就是一個AppsCustomizeCellLayout,在<<Launcher3的加載流程>>中,有提到過對每一頁的設置,直接找出這部分代碼, launcher3\src\main\java\com\android\launcher3\AppsCustomizePagedView.java
    // 設置page的表格、背景色
    private void setupPage(AppsCustomizeCellLayout layout) {
        layout.setGridSize(mCellCountX, mCellCountY);// 設置頁面表格數

        // Note: We force a measure here to get around the fact that when we do layout calculations
        // immediately after syncing, we don't have a proper width.  That said, we already know the
        // expected page width, so we can actually optimize by hiding all the TextView-based
        // children that are expensive to measure, and let that happen naturally later.
        setVisibilityOnChildren(layout, View.GONE);
        int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST);
        int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST);
        layout.measure(widthSpec, heightSpec);

        // 設置page背景色
        Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel);
        if (bg != null) {
            bg.setAlpha(mPageBackgroundsVisible ? 255: 0);
            layout.setBackground(bg);
        }

        setVisibilityOnChildren(layout, View.VISIBLE);
    }
這裡設置了AppsCustomizeCellLayout的背景色,我們將其設置透明背景,看能否達到效果。
        // 設置page背景色
//        Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel);
//        if (bg != null) {
//            bg.setAlpha(mPageBackgroundsVisible ? 255: 0);
//            layout.setBackground(bg);
//        }
        layout.setBackgroundColor(Color.TRANSPARENT);
\
這樣就滿足了效果,但是文字是灰色的有些不協調,我們改成白色的,這個在syncAppsPageItems方法中,做如下修改,
        for (int i = startIndex; i < endIndex; ++i) {// 循環添加items
            AppInfo info = mApps.get(i);
            BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(R.layout.apps_customize_application, layout, false);
            icon.applyFromApplicationInfo(info);
            icon.setOnClickListener(mLauncher);
            icon.setOnLongClickListener(this);
            icon.setOnTouchListener(this);
            icon.setOnKeyListener(this);
            icon.setOnFocusChangeListener(layout.mFocusHandlerView);
            icon.setTextColor(Color.WHITE); // modify text color

            .................................
        }
\

2、如何改變行和列數? 可能已經注意到了,在布局文件中通過launcher:widgetCountX,launcher:widgetCountY來設置小部件沒有顯示數量,之所以可以這麼設置,是因為在AppsCustomizePagedView中定義了這兩個屬性。
        // Save the default widget preview background
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0);
        mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2);
        mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2);

但對於application而言,並沒有定義類似的屬性,那如何來改變行列數呢?首先得知道行和列是怎麼得到的。mCellCountX和mCellCountY這兩個變量分別代表行數和列數,它們的值是怎麼得到的呢?

    protected void onDataReady(int width, int height) {
        // Now that the data is ready, we can calculate the content width, the number of cells to
        // use for each page
        LauncherAppState app = LauncherAppState.getInstance();
        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
        mCellCountX = (int) grid.allAppsNumCols;
        mCellCountY = (int) grid.allAppsNumRows;
        .....................................
    }

跟allAppsNumCols和allAppsNumRows相關,這兩個值在DeviceProfile.java中定義的,

    private void updateIconSize(float scale, int drawablePadding, Resources resources,
                                DisplayMetrics dm) {
        ...................
        // All Apps
        allAppsCellWidthPx = allAppsIconSizePx;
        allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx;
        int maxLongEdgeCellCount =
                resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
        int maxShortEdgeCellCount =
                resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
        int minEdgeCellCount =
                resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
        int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount);
        int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount);

        if (allAppsShortEdgeCount > 0 && allAppsLongEdgeCount > 0) {
            allAppsNumRows = isLandscape ? allAppsShortEdgeCount : allAppsLongEdgeCount;
            allAppsNumCols = isLandscape ? allAppsLongEdgeCount : allAppsShortEdgeCount;
        } else {
            allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) /
                    (allAppsCellHeightPx + allAppsCellPaddingPx);
            allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows));
            allAppsNumCols = (availableWidthPx) /
                    (allAppsCellWidthPx + allAppsCellPaddingPx);
            allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols));
        }
    }

我們可以看到行列數並不是固定的,是根據配置的行列數、圖標大小、表格間距等計算出來的。如果我們想增加行列數,可以把圖標縮小、間距加大,反之可以減小行列數。

Launcher3根據不同的型號的手機加載不同的配置項,launcher3\src\main\java\com\android\launcher3\DynamicGrid.java,

        deviceProfiles.add(new DeviceProfile("Nexus 4",
                335, 567,  4, 4,  DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4));
        deviceProfiles.add(new DeviceProfile("Nexus 5",
                359, 567,  4, 4,  DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4));

我用的測試機是Nexus 5,但實際使用的配置卻是上面那個,這個我們就不管了。一共有十個參數,分別表示:設備名、最小寬度Dps、最小高度Dps、行數、列數、圖標大小、圖標字體大小、固定熱鍵數目(Hotseat)、固定熱鍵圖標大小、默認Workspace布局。

我們先將四列改成五列,

        deviceProfiles.add(new DeviceProfile("Nexus 4",
                335, 567,  4, 5,  DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4));

測試後好像沒什麼變化,我們把圖標再改小點,默認60,改成48,

    static float DEFAULT_ICON_SIZE_DP = 48;
\

 

我們可以看到變成5列了,但是也變成6行了,我們在把最大行數設為5,原來是6,launcher3\src\main\res\values\config.xml,

6

 

\

 

這樣就變成5行5列了,但是看上去不大協調,目前我的測試機還是適合5*4,這裡我們只是了解下怎麼修改。

當然,除了背景、行列數可以改變外,我們也可以更改動畫效果,這裡就不在贅述了。

結束


  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved