documentsUI源码分析
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如下:
<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>
存储访问框架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,左边是侧滑菜单,右边是内容显示
内容显示区域布局:
<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
方法。
2. RootsFragment
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
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
- 缩略图显示
documentsUI源码分析的更多相关文章
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
- java使用websocket,并且获取HttpSession,源码分析
转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
- ABP源码分析三:ABP Module
Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...
随机推荐
- 如何在vuejs中抽出公共代码
当我们在使用vue构建中大型项目时,通常会遇到某些经常用的方法以及属性,比如说搭建一个员工管理系统,请求的url需要一个共同的前缀,或者在某几个view中需要用到时间,这个时间是通过某方法格式化之后的 ...
- python中xrange用法分析
本文实例讲述了python中xrange用法.分享给大家供大家参考.具体如下: 先来看如下示例: >>> x=xrange(0,8) >>> print x xra ...
- Akka(7): FSM:通过状态变化来转换运算行为
在上篇讨论里我们提到了become/unbecome.由于它们本质上是堆栈操作,所以只能在较少的状态切换下才能保证堆栈操作的协调及维持程序的清晰逻辑.对于比较复杂的程序流程,Akka提供了FSM:一种 ...
- 数据结构与算法1-2 C语言运行时间检测算法
#include <stdio.h> #include <math.h> #include <time.h> clock_t start,stop; #define ...
- asp.net mvc中html helper的一大优势
刚上手这个框架,发现其中的html helper用起来很方便,让我们这些从web form 过渡来的coder有一种使用控件的快感,嘻嘻! 言归正传,我要说的是在使用它时,系统会自动执行表单的现场恢复 ...
- 单例模式与静态变量在PHP中
在PHP中,没有普遍意义上的静态变量.与Java.C++不同,PHP中的静态变量的存活周期仅仅是每次PHP的会话周期,所以注定了不会有Java或者C++那种静态变量. 1. 静态变量在PHP中 在PH ...
- jquery.qrcode.min.js(支持中文转化二维码)
详情请看:http://www.ncloud.hk/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/jqueryqrcodeminjs/ 今天还是要讲一下关于二维码的知识,前 ...
- Android Studio常用快捷键使用
以下是我在编程中实际用上的Android Studio快捷键,基于Windows系统,在使用过程中会不断添加不断完善,OSX版本的在另外一篇博客 Ctrl+Alt+L 格式化代码,编写完成项目来一下, ...
- phpcms v9模版调用代码
首页调用栏目{pc:content action="category" siteid="$siteid" num="15" order=&q ...
- Vulkan Tutorial 23 Descriptor layout and buffer
操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 我们现在可以将任意属性传递给每个顶点的顶点着色器使用.但是 ...