摘要:源碼分析本文基于的源碼,來分析模塊。原本基于源碼看了兩天,但是與中模塊差異很大,且更加復(fù)雜,因此重新基于的源碼分析。通過,將響應(yīng)該意圖,選擇文件后,在返回結(jié)果中提取,解析后進(jìn)行相應(yīng)操作。源碼分析代碼結(jié)構(gòu)較為復(fù)雜,本文只分析大致流程。
documentsUI源碼分析
本文基于Android 6.0的源碼,來分析documentsUI模塊。
原本基于7.1源碼看了兩天,但是Android 7.1與6.0中documentsUI模塊差異很大,且更加復(fù)雜,因此重新基于6.0的源碼分析。
documentsUI是什么?documentsUI是Android系統(tǒng)提供的一個文件選擇器,類似于Windows系統(tǒng)中點(diǎn)擊“打開”按鈕彈出的文件選擇框,有人稱documentsUI為文件管理器,這是不準(zhǔn)確的。
documentsUI是Android系統(tǒng)中存儲訪問框架(Storage Access Framework,SAF)的一部分。
Android 4.4(API 級別 19)引入了存儲訪問框架 (SAF)。SAF 讓用戶能夠在其所有首選文檔存儲提供程序中方便地瀏覽并打開文檔、圖像以及其他文件。 用戶可以通過易用的標(biāo)準(zhǔn)UI,以統(tǒng)一方式在所有應(yīng)用和提供程序中瀏覽文件和訪問最近使用的文件。
documentsUI的清單文件中只有一個Activity,且沒有帶category.LAUNCHER的屬性,因此Launcher桌面上并沒有圖標(biāo),但是進(jìn)入documentsUI的入口很多,如桌面上的下載應(yīng)用、短信中的添加附件、瀏覽器中上傳圖片等。
documentsUI清單文件中的activity如下:
存儲訪問框架SAF
在介紹documentUI之前,需要介紹存儲訪問框架,在Android 4.4(API 級別 19),Google引入了存儲訪問框架 (SAF),讓用戶能夠在其所有首選文檔存儲提供程序中方便地瀏覽并打開文檔、圖像以及其他文件。 用戶可以通過易用的標(biāo)準(zhǔn) UI,以統(tǒng)一方式在所有應(yīng)用和提供程序中瀏覽文件和訪問最近使用的文件。
云存儲服務(wù)或本地存儲服務(wù)可以通過實(shí)現(xiàn)封裝其服務(wù)的 DocumentsProvider 參與此生態(tài)系統(tǒng)。只需幾行代碼,便可將需要訪問提供程序文檔的客戶端應(yīng)用與 SAF 集成。
SAF 包括以下內(nèi)容:
文檔提供程序 — 一種內(nèi)容提供程序,允許存儲服務(wù)(如 Google Drive)顯示其管理的文件。 文檔提供程序作為 DocumentsProvider 類的子類實(shí)現(xiàn)。文檔提供程序的架構(gòu)基于傳統(tǒng)文件層次結(jié)構(gòu),但其實(shí)際數(shù)據(jù)存儲方式由您決定。Android 平臺包括若干內(nèi)置文檔提供程序,如 Downloads、Images 和 Videos。
客戶端應(yīng)用 — 一種自定義應(yīng)用,它調(diào)用 ACTION_OPEN_DOCUMENT 和/或 ACTION_CREATE_DOCUMENT Intent 并接收文檔提供程序返回的文件;
選取器 — 一種系統(tǒng) UI,允許用戶訪問所有滿足客戶端應(yīng)用搜索條件的文檔提供程序內(nèi)的文檔。
控制流文檔提供程序數(shù)據(jù)模型基于傳統(tǒng)文件層次結(jié)構(gòu)。 通過DocumentsProvider API訪問數(shù)據(jù),可以按照自己喜好的任何方式存儲數(shù)據(jù)。例如,可以使用基于標(biāo)記的云存儲來存儲數(shù)據(jù)。
如上圖所示,在 SAF 中,提供程序和客戶端并不直接交互。
客戶端請求與文件交互(即讀取、編輯、創(chuàng)建或刪除文件)的權(quán)限;
交互在應(yīng)用(在本示例中為照片應(yīng)用)觸發(fā) Intent ACTION_OPEN_DOCUMENT 或ACTION_CREATE_DOCUMENT 后開始。Intent 可能包括進(jìn)一步細(xì)化條件的過濾器 — 例如,“為我提供所有 MIME 類型為‘圖像’的可打開文件”;
Intent 觸發(fā)后,系統(tǒng)選取器將檢索每個已注冊的提供程序,并向用戶顯示匹配的內(nèi)容根目錄;
選取器會為用戶提供一個標(biāo)準(zhǔn)的文檔訪問界面,但底層文檔提供程序可能與其差異很大。 例如,圖 2 顯示了一個 Google Drive 提供程序、一個 USB 提供程序和一個云提供程序。
客戶端應(yīng)用編寫一個客戶端應(yīng)用,調(diào)用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將響應(yīng)該意圖,選擇文件后,在返回結(jié)果中提取URI,解析URI后進(jìn)行相應(yīng)操作。
內(nèi)容提供程序如需使得的自己的應(yīng)用程序通過documentsUI向用戶展示文件,可編寫文檔提供程序,通過 SAF 提供自己的文件。
首先要在清單文件中定義相應(yīng)的provider和activity屬性,然后創(chuàng)建繼承DocumentsProvider的子類,并實(shí)現(xiàn)以下方法:queryRoots()、queryChildDocuments()、queryDocument()、openDocument()。
關(guān)于存儲訪問框架的詳細(xì)介紹可在Android開發(fā)者官網(wǎng)獲取。
源碼分析documentsUI代碼結(jié)構(gòu)較為復(fù)雜,本文只分析大致流程。
1. 入口: DocumentsActivity布局文件是DrawerLayout,左邊是側(cè)滑菜單,右邊是內(nèi)容顯示
內(nèi)容顯示區(qū)域布局:
內(nèi)容顯示區(qū)域由一個自定義view DocumentsToolBar和DirectoryContainerView組成。
側(cè)滑菜單布局:
側(cè)滑菜單由一個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保存狀態(tài)信息,在buildDefaultState初始化,假設(shè)啟動的action為ACTION_GET_CONTENT,那么將調(diào)用RootsFragment的show方法。
2. RootsFragmentpublic 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就是側(cè)滑菜單部分,在 RootsFragment 的 onCreateView 方法中,加載出的view就是一個listview,如下圖:
listview中顯示的是能響應(yīng)該打開文件Itent的文檔提供者和第三方應(yīng)用,在 onActivityCreated方法中,使用Loard機(jī)制加載出listview要顯示的數(shù)據(jù)
mCallbacks = new LoaderCallbacks>() { @Override public Loader > onCreateLoader(int id, Bundle args) { return new RootsLoader(context, roots, state); } @Override public void onLoadFinished( Loader > loader, Collection 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 > loader) { mAdapter = null; mList.setAdapter(null); } };
在onLoadFinished中實(shí)例化RootsAdapter
RootsAdapterprivate static class RootsAdapter extends ArrayAdapter- { public RootsAdapter(Context context, Collection
roots, Intent includeApps) { super(context, 0); RootItem recents = null; RootItem images = null; RootItem videos = null; RootItem audio = null; RootItem downloads = null; final List clouds = Lists.newArrayList(); final List 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 infos = pm.queryIntentActivities( includeApps, PackageManager.MATCH_DEFAULT_ONLY); final List 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中主要包含以下幾點(diǎn):
實(shí)例化RootsAdapter時,解析傳入的數(shù)據(jù)得到recents、images、videos、audio、downloads、locals、clouds,這些都可以在內(nèi)容顯示區(qū)展示文檔
includeApps代表可以相應(yīng)該Intent的第三方APP,獲取這些APP的信息(如圖標(biāo)、名稱等)顯示在listview中
根據(jù)getItemViewType判斷不同類型item,顯示其布局。listview中包含兩種item,分別是RootItem和AppItem,它們共同繼承自Item類
SpacerItem也是繼承自Item類,它是一個分隔線,分隔RootItem和AppItem
點(diǎn)擊事件側(cè)滑菜單的listview設(shè)置了兩個點(diǎn)擊事件,普通點(diǎn)擊事件和長按點(diǎn)擊事件
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; } } };
長按點(diǎn)擊事件只對AppItem有效,長按AppItem時跳轉(zhuǎn)到對應(yīng)APP的應(yīng)用信息界面,點(diǎn)擊AppItem時,啟動documentsUI的intent交由相應(yīng)APP處理。
當(dāng)點(diǎn)擊的是RootItem時,調(diào)用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()); } }
這里判斷是否點(diǎn)擊的是“最近”菜單,如果是則直接加載,如果不是則執(zhí)行new PickRootTask(root).executeOnExecutor(getCurrentExecutor())加載相應(yīng)item的內(nèi)容,最后也是進(jìn)入onCurrentDirectoryChanged中
下面看一下onCurrentDirectoryChanged方法
final void onCurrentDirectoryChanged(int anim) { onDirectoryChanged(anim); //更新文檔內(nèi)容顯示 final RootsFragment roots = RootsFragment.get(getFragmentManager()); if (roots != null) { roots.onCurrentRootChanged();//更新側(cè)滑菜單點(diǎn)擊狀態(tài) } updateActionBar(); invalidateOptionsMenu(); }
其中重點(diǎn)是onDirectoryChanged(anim)方法,這個方法是在BaseActivity類中定義的一個抽象方法
abstract void onDirectoryChanged(int anim);
其具體實(shí)現(xiàn)在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); } } }
其中分支判斷當(dāng)前文檔是“最近”、帶搜索結(jié)果的文檔內(nèi)容還是普通文檔內(nèi)容,這里只看showNormal方法,其他不看,showNormal中調(diào)用的是show方法
進(jìn)入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() { @Override public Loader 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 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 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 loader) { mAdapter.swapResult(null); } };
使用loader機(jī)制加載文檔內(nèi)容,在onCreateLoader返回DirectoryLoader加載文檔內(nèi)容內(nèi)容,加載完成回調(diào)onLoadFinished傳入加載的結(jié)果,最后通過mAdapter.swapResult(result)將數(shù)據(jù)與Adapter綁定,Adapter有了數(shù)據(jù)就去更新界面。
那么從啟動documentsUI到顯示出所選菜單的內(nèi)容整個過程就結(jié)束了,整個過程大致經(jīng)過以下步驟:
響應(yīng)Intent啟動documentsUI,轉(zhuǎn)到DocumentsActivity
保存Intent和應(yīng)用顯示狀態(tài)的各種信息
通過RootsLoader加載側(cè)滑菜單數(shù)據(jù)
點(diǎn)擊菜單選項后,通過DirectoryLoader完成異步查詢,加載顯示文檔數(shù)據(jù)
顯示數(shù)據(jù)
其他還需進(jìn)一步了解的
Loader機(jī)制
自定義View類:DirectoryContainerView、DirectoryView、DocumentsToolBar
縮略圖顯示
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/67446.html
摘要:背景在工作中雖然我經(jīng)常使用到庫但是很多時候?qū)Φ囊恍└拍钸€是處于知其然不知其所以然的狀態(tài)因此就萌生了學(xué)習(xí)源碼的想法剛開始看源碼的時候自然是比較痛苦的主要原因有兩個第一網(wǎng)上沒有找到讓我滿意的詳盡的源碼分析的教程第二我也是第一次系統(tǒng)地學(xué)習(xí)這么大代 背景 在工作中, 雖然我經(jīng)常使用到 Netty 庫, 但是很多時候?qū)?Netty 的一些概念還是處于知其然, 不知其所以然的狀態(tài), 因此就萌生了學(xué)...
摘要:簡介本篇文章是容器源碼分析系列文章的最后一篇文章,本篇文章所分析的對象是方法,該方法用于對已完成屬性填充的做最后的初始化工作。后置處理器是拓展點(diǎn)之一,通過實(shí)現(xiàn)后置處理器接口,我們就可以插手的初始化過程。 1. 簡介 本篇文章是Spring IOC 容器源碼分析系列文章的最后一篇文章,本篇文章所分析的對象是 initializeBean 方法,該方法用于對已完成屬性填充的 bean 做最...
閱讀 2779·2023-04-26 01:47
閱讀 3591·2023-04-25 23:45
閱讀 2461·2021-10-13 09:39
閱讀 606·2021-10-09 09:44
閱讀 1789·2021-09-22 15:59
閱讀 2761·2021-09-13 10:33
閱讀 1706·2021-09-03 10:30
閱讀 656·2019-08-30 15:53