本文基於Android 6.0的源碼,來分析documentsUI模塊。html
本來基於7.1源碼看了兩天,可是Android 7.1與6.0中documentsUI模塊差別很大,且更加複雜,所以從新基於6.0的源碼分析。java
documentsUI是Android系統提供的一個文件選擇器,相似於Windows系統中點擊「打開」按鈕彈出的文件選擇框,有人稱documentsUI爲文件管理器,這是不許確的。android
documentsUI是Android系統中存儲訪問框架(Storage Access Framework,SAF)的一部分。瀏覽器
Android 4.4(API 級別 19)引入了存儲訪問框架 (SAF)。SAF 讓用戶可以在其全部首選文檔存儲提供程序中方便地瀏覽並打開文檔、圖像以及其餘文件。 用戶能夠經過易用的標準UI,以統一方式在全部應用和提供程序中瀏覽文件和訪問最近使用的文件。架構
documentsUI的清單文件中只有一個Activity,且沒有帶category.LAUNCHER的屬性,所以Launcher桌面上並無圖標,可是進入documentsUI的入口不少,如桌面上的下載應用、短信中的添加附件、瀏覽器中上傳圖片等。app
documentsUI清單文件中的activity以下:框架
<activity android:name=".DocumentsActivity" android:theme="@style/DocumentsTheme" android:icon="@drawable/ic_doc_text"> <intent-==filter==> <action android:name="android.intent.action.OPEN_DOCUMENT" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="*/*" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.CREATE_DOCUMENT" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="*/*" /> </intent-filter> <intent-filter android:priority="100"> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="*/*" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.OPEN_DOCUMENT_TREE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> <intent-filter> <action android:name="android.provider.action.MANAGE_ROOT" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.document/root" /> </intent-filter> <intent-filter> <action android:name="android.provider.action.BROWSE_DOCUMENT_ROOT" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.document/root" /> </intent-filter> </activity>
在介紹documentUI以前,須要介紹存儲訪問框架,在Android 4.4(API 級別 19),Google引入了存儲訪問框架 (SAF),讓用戶可以在其全部首選文檔存儲提供程序中方便地瀏覽並打開文檔、圖像以及其餘文件。 用戶能夠經過易用的標準 UI,以統一方式在全部應用和提供程序中瀏覽文件和訪問最近使用的文件。異步
雲存儲服務或本地存儲服務能夠經過實現封裝其服務的 DocumentsProvider 參與今生態系統。只需幾行代碼,即可將須要訪問提供程序文檔的客戶端應用與 SAF 集成。ide
SAF 包括如下內容:源碼分析
文檔提供程序 — 一種內容提供程序,容許存儲服務(如 Google Drive)顯示其管理的文件。 文檔提供程序做爲 DocumentsProvider 類的子類實現。文檔提供程序的架構基於傳統文件層次結構,但其實際數據存儲方式由您決定。Android 平臺包括若干內置文檔提供程序,如 Downloads、Images 和 Videos。
客戶端應用 — 一種自定義應用,它調用 ACTION_OPEN_DOCUMENT 和/或 ACTION_CREATE_DOCUMENT Intent 並接收文檔提供程序返回的文件;
選取器 — 一種系統 UI,容許用戶訪問全部知足客戶端應用搜索條件的文檔提供程序內的文檔。
文檔提供程序數據模型基於傳統文件層次結構。 經過DocumentsProvider
API訪問數據,能夠按照本身喜愛的任何方式存儲數據。例如,可使用基於標記的雲存儲來存儲數據。
如上圖所示,在 SAF 中,提供程序和客戶端並不直接交互。
客戶端請求與文件交互(即讀取、編輯、建立或刪除文件)的權限;
交互在應用(在本示例中爲照片應用)觸發 Intent ACTION_OPEN_DOCUMENT 或ACTION_CREATE_DOCUMENT 後開始。Intent 可能包括進一步細化條件的過濾器 — 例如,「爲我提供全部 MIME 類型爲‘圖像’的可打開文件」;
Intent 觸發後,系統選取器將檢索每一個已註冊的提供程序,並向用戶顯示匹配的內容根目錄;
選取器會爲用戶提供一個標準的文檔訪問界面,但底層文檔提供程序可能與其差別很大。 例如,圖 2 顯示了一個 Google Drive 提供程序、一個 USB 提供程序和一個雲提供程序。
編寫一個客戶端應用,調用documentsUI選擇文件。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // Filter to only show results that can be "opened", such as a file (as opposed to a list of contacts or timezones) intent.addCategory(Intent.CATEGORY_OPENABLE); // Filter to show only images, using the image MIME data type. intent.setType("image/*"); startActivityForResult(intent, READ_REQUEST_CODE);
經過Intent.ACTION_OPEN_DOCUMENT
,documentsUI將響應該意圖,選擇文件後,在返回結果中提取URI,解析URI後進行相應操做。
如需使得的本身的應用程序經過documentsUI向用戶展現文件,可編寫文檔提供程序,經過 SAF 提供本身的文件。
首先要在清單文件中定義相應的provider和activity屬性,而後建立繼承DocumentsProvider
的子類,並實現如下方法:queryRoots()、queryChildDocuments()、queryDocument()、openDocument()
。
關於存儲訪問框架的詳細介紹可在Android開發者官網獲取。
documentsUI代碼結構較爲複雜,本文只分析大體流程。
佈局文件是DrawerLayout,左邊是側滑菜單,右邊是內容顯示
內容顯示區域佈局:
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.android.documentsui.DocumentsToolBar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?android:attr/actionBarSize" android:background="?android:attr/colorPrimary" android:elevation="8dp" android:theme="?android:attr/actionBarTheme"> <Spinner android:id="@+id/stack" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="4dp" android:overlapAnchor="true" /> </com.android.documentsui.DocumentsToolBar> <com.android.documentsui.DirectoryContainerView android:id="@+id/container_directory" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <FrameLayout android:id="@+id/container_save" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/material_grey_50" android:elevation="8dp" /> </LinearLayout>
內容顯示區域由一個自定義view DocumentsToolBar
和DirectoryContainerView
組成。
側滑菜單佈局:
<LinearLayout android:id="@+id/drawer_roots" android:layout_width="256dp" android:layout_height="match_parent" android:layout_gravity="start" android:orientation="vertical" android:elevation="16dp" android:background="@*android:color/white"> <Toolbar android:id="@+id/roots_toolbar" android:layout_width="match_parent" android:layout_height="?android:attr/actionBarSize" android:background="?android:attr/colorPrimary" android:elevation="8dp" android:theme="?android:attr/actionBarTheme" /> <FrameLayout android:id="@+id/container_roots" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout>
側滑菜單由一個Toolbar和FrameLayout組成。
在 onCreate 方法中
if (mState.action == ACTION_CREATE) { final String mimeType = getIntent().getType(); final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); SaveFragment.show(getFragmentManager(), mimeType, title); } else if (mState.action == ACTION_OPEN_TREE || mState.action == ACTION_OPEN_COPY_DESTINATION) { PickFragment.show(getFragmentManager()); } 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_OPEN_TREE || mState.action == ACTION_OPEN_COPY_DESTINATION) { RootsFragment.show(getFragmentManager(), null); }
mState保存狀態信息,在buildDefaultState初始化,假設啓動的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(); }
show
方法顯示出RootsFragment
本身,RootsFragment
就是側滑菜單部分,在 RootsFragment
的 onCreateView
方法中,加載出的view就是一個listview,以下圖:
listview中顯示的是能響應該打開文件Itent的文檔提供者
和第三方應用
,在 onActivityCreated方法中,使用Loard機制加載出listview要顯示的數據
mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() { @Override public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) { return new RootsLoader(context, roots, state); } @Override public void onLoadFinished( Loader<Collection<RootInfo>> loader, Collection<RootInfo> result) { if (!isAdded()) return; final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS); mAdapter = new RootsAdapter(context, result, includeApps); mList.setAdapter(mAdapter); onCurrentRootChanged(); } @Override public void onLoaderReset(Loader<Collection<RootInfo>> loader) { mAdapter = null; mList.setAdapter(null); } };
在onLoadFinished
中實例化RootsAdapter
private static class RootsAdapter extends ArrayAdapter<Item> { public RootsAdapter(Context context, Collection<RootInfo> roots, Intent includeApps) { super(context, 0); RootItem recents = null; RootItem images = null; RootItem videos = null; RootItem audio = null; RootItem downloads = null; final List<RootInfo> clouds = Lists.newArrayList(); final List<RootInfo> locals = Lists.newArrayList(); for (RootInfo root : roots) { if (root.isRecents()) { recents = new RootItem(root); } else if (root.isExternalStorage()) { locals.add(root); } else if (root.isDownloads()) { downloads = new RootItem(root); } else if (root.isImages()) { images = new RootItem(root); } else if (root.isVideos()) { videos = new RootItem(root); } else if (root.isAudio()) { audio = new RootItem(root); } else { clouds.add(root); } } final RootComparator comp = new RootComparator(); Collections.sort(clouds, comp); Collections.sort(locals, comp); if (recents != null) add(recents); for (RootInfo cloud : clouds) { add(new RootItem(cloud)); } if (images != null) add(images); if (videos != null) add(videos); if (audio != null) add(audio); if (downloads != null) add(downloads); for (RootInfo local : locals) { add(new RootItem(local)); } if (includeApps != null) { final PackageManager pm = context.getPackageManager(); final List<ResolveInfo> infos = pm.queryIntentActivities( includeApps, PackageManager.MATCH_DEFAULT_ONLY); final List<AppItem> apps = Lists.newArrayList(); // Omit ourselves from the list for (ResolveInfo info : infos) { if (!context.getPackageName().equals(info.activityInfo.packageName)) { apps.add(new AppItem(info)); } } if (apps.size() > 0) { add(new SpacerItem()); for (Item item : apps) { add(item); } } } } @Override public View getView(int position, View convertView, ViewGroup parent) { final Item item = getItem(position); return item.getView(convertView, parent); } @Override public boolean areAllItemsEnabled() { return false; } @Override public boolean isEnabled(int position) { return getItemViewType(position) != 1; } @Override public int getItemViewType(int position) { final Item item = getItem(position); if (item instanceof RootItem || item instanceof AppItem) { return 0; } else { return 1; } } @Override public int getViewTypeCount() { return 2; } }
RootsAdapter中主要包含如下幾點:
實例化RootsAdapter時,解析傳入的數據獲得recents
、images
、videos
、audio
、downloads
、locals
、clouds
,這些均可以在內容顯示區展現文檔
includeApps
表明能夠相應該Intent的第三方APP,獲取這些APP的信息(如圖標、名稱等)顯示在listview中
根據getItemViewType判斷不一樣類型item,顯示其佈局。listview中包含兩種item,分別是RootItem
和AppItem
,它們共同繼承自Item
類
SpacerItem也是繼承自Item
類,它是一個分隔線,分隔RootItem
和AppItem
側滑菜單的listview設置了兩個點擊事件,普通點擊事件和長按點擊事件
private OnItemClickListener mItemListener = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Item item = mAdapter.getItem(position); if (item instanceof RootItem) { BaseActivity activity = BaseActivity.get(RootsFragment.this); activity.onRootPicked(((RootItem) item).root); } else if (item instanceof AppItem) { DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this); activity.onAppPicked(((AppItem) item).info); } else { throw new IllegalStateException("Unknown root: " + item); } } }; private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { final Item item = mAdapter.getItem(position); if (item instanceof AppItem) { showAppDetails(((AppItem) item).info); return true; } else { return false; } } };
長按點擊事件只對AppItem
有效,長按AppItem
時跳轉到對應APP的應用信息界面,點擊AppItem
時,啓動documentsUI的intent交由相應APP處理。
當點擊的是RootItem
時,調用DocumentsActivity
的 onRootPicked( )
方法,該方法繼承自BaseActivity
。
void onRootPicked(RootInfo root) { State state = getDisplayState(); // Clear entire backstack and start in new root state.stack.root = root; state.stack.clear(); state.stackTouched = true; mSearchManager.update(root); // Recents is always in memory, so we just load it directly. // Otherwise we delegate loading data from disk to a task // to ensure a responsive ui. if (mRoots.isRecentsRoot(root)) { onCurrentDirectoryChanged(ANIM_SIDE); } else { new PickRootTask(root).executeOnExecutor(getCurrentExecutor()); } }
這裏判斷是否點擊的是「最近」菜單,若是是則直接加載,若是不是則執行new PickRootTask(root).executeOnExecutor(getCurrentExecutor())
加載相應item的內容,最後也是進入onCurrentDirectoryChanged
中
下面看一下onCurrentDirectoryChanged
方法
final void onCurrentDirectoryChanged(int anim) { onDirectoryChanged(anim); //更新文檔內容顯示 final RootsFragment roots = RootsFragment.get(getFragmentManager()); if (roots != null) { roots.onCurrentRootChanged();//更新側滑菜單點擊狀態 } updateActionBar(); invalidateOptionsMenu(); }
其中重點是onDirectoryChanged(anim)
方法,這個方法是在BaseActivity
類中定義的一個抽象方法
abstract void onDirectoryChanged(int anim);
其具體實如今DocumentsActivity
中:
@Override void onDirectoryChanged(int anim) { final FragmentManager fm = getFragmentManager(); final RootInfo root = getCurrentRoot(); final DocumentInfo cwd = getCurrentDirectory(); mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN); if (cwd == null) { // No directory means recents if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE || mState.action == ACTION_OPEN_COPY_DESTINATION) { RecentsCreateFragment.show(fm); } else { DirectoryFragment.showRecentsOpen(fm, anim); // Start recents in grid when requesting visual things final boolean visualMimes = MimePredicate.mimeMatches( MimePredicate.VISUAL_MIMES, mState.acceptMimes); mState.userMode = visualMimes ? State.MODE_GRID : State.MODE_LIST; mState.derivedMode = mState.userMode; } } else { if (mState.currentSearch != null) { // Ongoing search DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim); } else { // Normal boring directory DirectoryFragment.showNormal(fm, root, cwd, anim); } } // Forget any replacement target if (mState.action == ACTION_CREATE) { final SaveFragment save = SaveFragment.get(fm); if (save != null) { save.setReplaceTarget(null); } } if (mState.action == ACTION_OPEN_TREE || mState.action == ACTION_OPEN_COPY_DESTINATION) { final PickFragment pick = PickFragment.get(fm); if (pick != null) { pick.setPickTarget(mState.action, cwd); } } }
其中分支判斷當前文檔是「最近」、帶搜索結果的文檔內容仍是普通文檔內容,這裏只看showNormal
方法,其餘不看,showNormal
中調用的是show
方法
進入DirectoryFragment
類
private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc, String query, int anim) { final Bundle args = new Bundle(); args.putInt(EXTRA_TYPE, type); args.putParcelable(EXTRA_ROOT, root); args.putParcelable(EXTRA_DOC, doc); args.putString(EXTRA_QUERY, query); final FragmentTransaction ft = fm.beginTransaction(); ...... final DirectoryFragment fragment = new DirectoryFragment(); fragment.setArguments(args); ft.replace(R.id.container_directory, fragment); ft.commitAllowingStateLoss(); }
show
方法顯示DirectoryFragment
本身
在onCreateView
中,初始化ListView
和GridView
,在onActivityCreated
方法中:
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 (result == null || result.exception != null) { // onBackPressed does a fragment transaction, which can't be done inside // onLoadFinished mHandler.post(new Runnable() { @Override public void run() { final Activity activity = getActivity(); if (activity != null) { activity.onBackPressed(); } } }); return; } 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; ((BaseActivity) context).onStateChanged(); updateDisplayState(); // When launched into empty recents, show drawer if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched && context instanceof DocumentsActivity) { ((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); } };
使用loader機制加載文檔內容,在onCreateLoader
返回DirectoryLoader
加載文檔內容內容,加載完成回調onLoadFinished
傳入加載的結果,最後經過mAdapter.swapResult(result)
將數據與Adapter綁定,Adapter有了數據就去更新界面。
那麼從啓動documentsUI到顯示出所選菜單的內容整個過程就結束了,整個過程大體通過如下步驟:
響應Intent啓動documentsUI,轉到DocumentsActivity
保存Intent和應用顯示狀態的各類信息
經過RootsLoader加載側滑菜單數據
點擊菜單選項後,經過DirectoryLoader完成異步查詢,加載顯示文檔數據
顯示數據
還需進一步瞭解的
Loader機制
自定義View類:DirectoryContainerView
、DirectoryView
、DocumentsToolBar
縮略圖顯示