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


一、布局 抽屜的布局文件是apps_customize_pane.xml,被include在launcher.xml中, launcher.xml
apps_customize_pane.xml這個就是抽屜的樹形結構,AppsCustomizeTabHost是根視圖,id/content是內容區域,包含一個FrameLayout和頁面指示器indicator,這個FrameLayout也包含兩塊,上面一塊是用作過渡頁面,下面是AppsCustomizePagedView,就是用來顯示app列表或小部件的,是最核心的部分。<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>
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);
}
這個方法顯然是不能實現的,因為在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);

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
.................................
}

// 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,這裡我們只是了解下怎麼修改。
當然,除了背景、行列數可以改變外,我們也可以更改動畫效果,這裡就不在贅述了。
結束
Android中Intent習慣用法
Android中的Intent是一個非常重要的類,如果對Intent不是特別了解,可以參見《詳解Android中Intent的使用方法》。如果對Intent Filter
Android仿京東首頁輪播文字(又名垂直跑馬燈)
京東客戶端的輪播文字效果: 本次要實現的只是後面滾動的文字(前面的用ImageView或者TextView實現即可),看一下實現的效果 實
Android MVP
前言前段時間,公司由個同事分享的時候,提到了MVP模式,自己之前也了解過,但是真正在自己的編碼過程中使用的非常少。最近在幫助一個朋友做畢業設計,心想這是一個很好的機會練習
Toolbar的使用以及自定義Toolbar的方法
android5.0以後出現了Toolbar,今天折騰了一下,在此做個記錄方便以後查看,同時也給有需要的朋友們參考!!!!!很慚愧只做了一點微小的工作。下面將完成兩個方面