android4.4的文件管理器documentsui源碼解析
在4.4以上的版本中如果通過如下的Intent調用Activity:
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); // The MIME data type filter intent.setType("*/*"); // Only return URIs that can be opened with ContentResolver intent.addCategory(Intent.CATEGORY_OPENABLE); startActivity(intent);
則會自動調用系統自帶的documentsui文件管理器。因為documentsui的manifest中沒有帶category等于category.LAUNCHER的屬性,因此documentsui是不會顯示在LAUNCHER桌面上的。
documentsui的界面如下:
為了研究在android中資源文件是如何訪問的的,我決定研究一下其代碼。源代碼地址
https://github.com/OWLeeMod/android_packages_apps_DocumentsUI
將代碼下載下來之后,發現起代碼結構非常復雜,這篇文章只是一個初步的分析。
入口:DocumentsActivity。DocumentsActivity的布局是由DrawerLayout構成的,左邊是文件類別的菜單,右邊是相應的目錄樹。
onCreate方法中:
// Hide roots when we're managing a specific root if (mState.action == ACTION_MANAGE) { if (mShowAsDialog) { findViewById(R.id.dialog_roots).setVisibility(View.GONE); } else { mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); } } if (mState.action == ACTION_CREATE) { final String mimeType = getIntent().getType(); final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); SaveFragment.show(getFragmentManager(), mimeType, title); } if (mState.action == ACTION_GET_CONTENT) { final Intent moreApps = new Intent(getIntent()); moreApps.setComponent(null); moreApps.setPackage(null); RootsFragment.show(getFragmentManager(), moreApps); } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE || mState.action == ACTION_STANDALONE) { RootsFragment.show(getFragmentManager(), null); } if (!mState.restored) { if (mState.action == ACTION_MANAGE) { final Uri rootUri = getIntent().getData(); if (rootUri != null) { new RestoreRootTask(rootUri).executeOnExecutor(getCurrentExecutor()); } else { new RestoreStackTask().execute(); } } else { new RestoreStackTask().execute(); } } else { onCurrentDirectoryChanged(ANIM_NONE); }
其中有個判斷條件是mState.action == ACTION_GET_CONTENT
表示調用documentsui的人想獲得的是文件系統的內容,也就是說本文開始那樣的調用方式將會轉到這個條件中來,執行
if (mState.action == ACTION_GET_CONTENT) { final Intent moreApps = new Intent(getIntent()); moreApps.setComponent(null); moreApps.setPackage(null); RootsFragment.show(getFragmentManager(), moreApps); }
中的代碼。本文也主要是圍繞documentsui的這一功能來分析的。除此之外從上面的代碼還可以看出documentsui還有些其他的功能對應的是ACTION_CREATE,
ACTION_MANAGE等,目前不清楚這些功能具體做什么。
回到
mState.action == ACTION_GET_CONTENT
這個條件中,他調用了RootsFragment
中的show方法:
public static void show(FragmentManager fm, Intent includeApps) { final Bundle args = new Bundle(); args.putParcelable(EXTRA_INCLUDE_APPS, includeApps); final RootsFragment fragment = new RootsFragment(); fragment.setArguments(args); final FragmentTransaction ft = fm.beginTransaction(); ft.replace(R.id.container_roots, fragment); ft.commitAllowingStateLoss(); }
可以看出這是為了顯示RootsFragment
自己,其實RootsFragment
就是DrawerLayout的菜單部分。
點擊某個菜單之后(如圖片)右邊的目錄樹做相應的切換,那么這個過程是如何進行的呢?
在
中,菜單列表是一個ListView當然點擊菜單就會進入到listvIew的RootsFragment
OnItemClickListener
中:
private OnItemClickListener mItemListener = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this); final Item item = mAdapter.getItem(position); if (item instanceof RootItem) { activity.onRootPicked(((RootItem) item).root, true); } else if (item instanceof AppItem) { activity.onAppPicked(((AppItem) item).info); } else { throw new IllegalStateException("Unknown root: " + item); } } };
我們注意上面的界面圖,可以看到左邊菜單不僅僅有關于文件類型的,還有可以選擇應用程序的。因此點擊菜單之后上面的代碼首先判斷了你點擊的RootItem
類型,這里我們不去研究用戶點擊應用程序的情況。
當用戶點擊的是文件類型(如圖片)菜單,則會調用DocumentsActivity
的onRootPicked:
public void onRootPicked(RootInfo root, boolean closeDrawer) { // Clear entire backstack and start in new root mState.stack.root = root; mState.stack.clear(); mState.stackTouched = true; if (!mRoots.isRecentsRoot(root)) { new PickRootTask(root).executeOnExecutor(getCurrentExecutor()); } else { onCurrentDirectoryChanged(ANIM_SIDE); } if (closeDrawer) { setRootsDrawerOpen(false); } }
這里又有一個條件分支,判斷了是否是“最近”菜單(
isRecentsRoot
,注意比照上面的界面圖),如果是則執行new
PickRootTask(root).executeOnExecutor(getCurrentExecutor());
不是則調用onCurrentDirectoryChanged
方法。因為分支過多我只研究最基本的,也就是列出目錄文件的功能所以忽略“最近”菜單被點擊的情況,進入到
中(其實反之最終也還是要進入到onCurrentDirectoryChanged中),很煩的是onCurrentDirectoryChanged
中又有很多分支情況,但可以肯定的是onCurrentDirectoryChanged
列出目錄文件的功能
是調用
DirectoryFragment.showNormal(fm, root, cwd, anim);
完成的。所以我的重點就在
DirectoryFragment
這個類中了。
我想了解在
DirectoryFragment
中是如何列出文件和目錄 同時是如何顯示縮略圖的。
是使用loader機制來加載內容的。DirectoryFragment
getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
其中
mCallbacks
如下:
mCallbacks = new LoaderCallbacks<DirectoryResult>() { @Override public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { final String query = getArguments().getString(EXTRA_QUERY); Uri contentsUri; switch (mType) { case TYPE_NORMAL: contentsUri = DocumentsContract.buildChildDocumentsUri( doc.authority, doc.documentId); if (state.action == ACTION_MANAGE) { contentsUri = DocumentsContract.setManageMode(contentsUri); } return new DirectoryLoader( context, mType, root, doc, contentsUri, state.userSortOrder); case TYPE_SEARCH: contentsUri = DocumentsContract.buildSearchDocumentsUri( root.authority, root.rootId, query); if (state.action == ACTION_MANAGE) { contentsUri = DocumentsContract.setManageMode(contentsUri); } return new DirectoryLoader( context, mType, root, doc, contentsUri, state.userSortOrder); case TYPE_RECENT_OPEN: final RootsCache roots = DocumentsApplication.getRootsCache(context); return new RecentLoader(context, roots, state); default: throw new IllegalStateException("Unknown type " + mType); } } @Override public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { if (!isAdded()) return; mAdapter.swapResult(result); // Push latest state up to UI // TODO: if mode change was racing with us, don't overwrite it if (result.mode != MODE_UNKNOWN) { state.derivedMode = result.mode; } state.derivedSortOrder = result.sortOrder; ((DocumentsActivity) context).onStateChanged(); updateDisplayState(); // When launched into empty recents, show drawer if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) { ((DocumentsActivity) context).setRootsDrawerOpen(true); } // Restore any previous instance state final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { getView().restoreHierarchyState(container); } else if (mLastSortOrder != state.derivedSortOrder) { mListView.smoothScrollToPosition(0); mGridView.smoothScrollToPosition(0); } mLastSortOrder = state.derivedSortOrder; } @Override public void onLoaderReset(Loader<DirectoryResult> loader) { mAdapter.swapResult(null); } };
其中很重要的是啟用了DirectoryLoader類。DirectoryLoader完成了目錄的加載。本文不對其深入講解。當loader加載完成(
onLoadFinished
)會將結果result
傳遞給mAdapter
:mAdapter.swapResult(result),這個result的類型是
DirectoryResult
。上面說了我想知道文件或者目錄的縮略圖是如何加載的,所以需要了解這個mAdapter
的內部情況。
mAdapter的聲明如下:
private DocumentsAdapter mAdapter;
DocumentsAdapter 是
的一個內部類,我直接跳到他的getview方法:DirectoryFragment
@Override public View getView(int position, View convertView, ViewGroup parent) { if (position < mCursorCount) { return getDocumentView(position, convertView, parent); } else { position -= mCursorCount; convertView = mFooters.get(position).getView(convertView, parent); // Only the view itself is disabled; contents inside shouldn't // be dimmed. convertView.setEnabled(false); return convertView; } }
貌似沒什么東西,繼續跟著跳到getDocumentView
,其中有這樣的一段代碼:
if (showThumbnail) { final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); final Bitmap cachedResult = thumbs.get(uri); if (cachedResult != null) { iconThumb.setImageBitmap(cachedResult); cacheHit = true; } else { iconThumb.setImageDrawable(null); final ThumbnailAsyncTask task = new ThumbnailAsyncTask( uri, iconMime, iconThumb, mThumbSize); iconThumb.setTag(task); ProviderExecutor.forAuthority(docAuthority).execute(task); } }
縮略圖獲得的過程是:首先從thumbs
這個ThumbnailCache
中獲取緩存圖片,定義如下
final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( context, mThumbSize);
如果緩存中沒找到,則開啟ThumbnailAsyncTask
。
然后再繼續分析ThumbnailAsyncTask
吧 總之這是一個很繁瑣的過程。
本文只是理出一條分析的線索,希望對大家還是有所幫助。