抽屉是用来放置安卓手机中所有需要显示到Launcher上的(当然也可以进行过滤,将不想显示的隐藏起来)应用和小部件,启动应用、添加快捷方式到桌面、卸载等。之前也提到过,有些Launcher是没有抽屉的,如MIUI的Launcher。在Launcher3中,默认是有的,当然,也提供了不显示抽屉的方法,这个后面会说到,在此先了解下抽屉。
一、布局
抽屉的布局文件是apps_customize_pane.xml,被include在launcher.xml中,
launcher.xml
<include layout="@layout/apps_customize_pane" android:id="@+id/apps_customize_pane" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" />
apps_customize_pane.xml
<com.android.launcher3.AppsCustomizeTabHost xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res-auto" android:clipChildren="false"> <LinearLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" android:orientation="vertical"> <FrameLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:clipChildren="false"> <FrameLayout android:id="@+id/fake_page_container" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" android:clipToPadding="false"> <FrameLayout android:id="@+id/fake_page" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" android:clipToPadding="false" /> </FrameLayout> <com.android.launcher3.AppsCustomizePagedView android:id="@+id/apps_customize_pane_content" android:layout_width="match_parent" android:layout_height="match_parent" launcher:widgetCountX="@integer/apps_customize_widget_cell_count_x" launcher:widgetCountY="@integer/apps_customize_widget_cell_count_y" launcher:maxGap="@dimen/workspace_max_gap" launcher:pageIndicator="@+id/apps_customize_page_indicator" /> </FrameLayout> <include android:id="@+id/apps_customize_page_indicator" layout="@layout/page_indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </LinearLayout> </com.android.launcher3.AppsCustomizeTabHost>
这个就是抽屉的树形结构,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<View> layerViews = new ArrayList<View>();// 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的布,然后将背景设为透明的,
<com.android.launcher3.AppsCustomizePagedView android:id="@+id/apps_customize_pane_content" android:layout_width="match_parent" android:layout_height="match_parent" launcher:widgetCountX="@integer/apps_customize_widget_cell_count_x" launcher:widgetCountY="@integer/apps_customize_widget_cell_count_y" launcher:maxGap="@dimen/workspace_max_gap" launcher:pageIndicator="@+id/apps_customize_page_indicator" />
这个方法显然是不能实现的,因为在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,
<integer name="config_dynamic_grid_max_long_edge_cell_count">6</integer>
这样就变成5行5列了,但是看上去不大协调,目前我的测试机还是适合5*4,这里我们只是了解下怎么修改。
当然,除了背景、行列数可以改变外,我们也可以更改动画效果,这里就不在赘述了。
结束