摘要:源碼分析本文基于的源碼,來分析模塊。原本基于源碼看了兩天,但是與中模塊差異很大,且更加復雜,因此重新基于的源碼分析。通過,將響應該意圖,選擇文件后,在返回結果中提取,解析后進行相應操作。源碼分析代碼結構較為復雜,本文只分析大致流程。
documentsUI源碼分析
本文基于Android 6.0的源碼,來分析documentsUI模塊。
原本基于7.1源碼看了兩天,但是Android 7.1與6.0中documentsUI模塊差異很大,且更加復雜,因此重新基于6.0的源碼分析。
documentsUI是什么?documentsUI是Android系統提供的一個文件選擇器,類似于Windows系統中點擊“打開”按鈕彈出的文件選擇框,有人稱documentsUI為文件管理器,這是不準確的。
documentsUI是Android系統中存儲訪問框架(Storage Access Framework,SAF)的一部分。
Android 4.4(API 級別 19)引入了存儲訪問框架 (SAF)。SAF 讓用戶能夠在其所有首選文檔存儲提供程序中方便地瀏覽并打開文檔、圖像以及其他文件。 用戶可以通過易用的標準UI,以統一方式在所有應用和提供程序中瀏覽文件和訪問最近使用的文件。
documentsUI的清單文件中只有一個Activity,且沒有帶category.LAUNCHER的屬性,因此Launcher桌面上并沒有圖標,但是進入documentsUI的入口很多,如桌面上的下載應用、短信中的添加附件、瀏覽器中上傳圖片等。
documentsUI清單文件中的activity如下:
存儲訪問框架SAF
在介紹documentUI之前,需要介紹存儲訪問框架,在Android 4.4(API 級別 19),Google引入了存儲訪問框架 (SAF),讓用戶能夠在其所有首選文檔存儲提供程序中方便地瀏覽并打開文檔、圖像以及其他文件。 用戶可以通過易用的標準 UI,以統一方式在所有應用和提供程序中瀏覽文件和訪問最近使用的文件。
云存儲服務或本地存儲服務可以通過實現封裝其服務的 DocumentsProvider 參與此生態系統。只需幾行代碼,便可將需要訪問提供程序文檔的客戶端應用與 SAF 集成。
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代碼結構較為復雜,本文只分析大致流程。
1. 入口: DocumentsActivity布局文件是DrawerLayout,左邊是側滑菜單,右邊是內容顯示
內容顯示區域布局:
內容顯示區域由一個自定義view DocumentsToolBar和DirectoryContainerView組成。
側滑菜單布局:
側滑菜單由一個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方法。
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就是側滑菜單部分,在 RootsFragment 的 onCreateView 方法中,加載出的view就是一個listview,如下圖:
listview中顯示的是能響應該打開文件Itent的文檔提供者和第三方應用,在 onActivityCreated方法中,使用Loard機制加載出listview要顯示的數據
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中實例化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中主要包含以下幾點:
實例化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() { @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機制加載文檔內容,在onCreateLoader返回DirectoryLoader加載文檔內容內容,加載完成回調onLoadFinished傳入加載的結果,最后通過mAdapter.swapResult(result)將數據與Adapter綁定,Adapter有了數據就去更新界面。
那么從啟動documentsUI到顯示出所選菜單的內容整個過程就結束了,整個過程大致經過以下步驟:
響應Intent啟動documentsUI,轉到DocumentsActivity
保存Intent和應用顯示狀態的各種信息
通過RootsLoader加載側滑菜單數據
點擊菜單選項后,通過DirectoryLoader完成異步查詢,加載顯示文檔數據
顯示數據
其他還需進一步了解的
Loader機制
自定義View類:DirectoryContainerView、DirectoryView、DocumentsToolBar
縮略圖顯示
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/67446.html
摘要:背景在工作中雖然我經常使用到庫但是很多時候對的一些概念還是處于知其然不知其所以然的狀態因此就萌生了學習源碼的想法剛開始看源碼的時候自然是比較痛苦的主要原因有兩個第一網上沒有找到讓我滿意的詳盡的源碼分析的教程第二我也是第一次系統地學習這么大代 背景 在工作中, 雖然我經常使用到 Netty 庫, 但是很多時候對 Netty 的一些概念還是處于知其然, 不知其所以然的狀態, 因此就萌生了學...
摘要:簡介本篇文章是容器源碼分析系列文章的最后一篇文章,本篇文章所分析的對象是方法,該方法用于對已完成屬性填充的做最后的初始化工作。后置處理器是拓展點之一,通過實現后置處理器接口,我們就可以插手的初始化過程。 1. 簡介 本篇文章是Spring IOC 容器源碼分析系列文章的最后一篇文章,本篇文章所分析的對象是 initializeBean 方法,該方法用于對已完成屬性填充的 bean 做最...
閱讀 2790·2023-04-26 01:47
閱讀 3599·2023-04-25 23:45
閱讀 2476·2021-10-13 09:39
閱讀 614·2021-10-09 09:44
閱讀 1802·2021-09-22 15:59
閱讀 2780·2021-09-13 10:33
閱讀 1729·2021-09-03 10:30
閱讀 665·2019-08-30 15:53