Charles Android 文件选择器源码分析

发表于:2018-5-29 10:14

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:TonnyL    来源:简书

  介绍
  现在的 App 很多功能都与多媒体有关, 例如图片, 视频, 音频, 文件等等. 在 Android 开发中, 如果需要从本地选择多媒体文件, 我们可以通过调用系统的 DocumentsUI 来实现, 当然通过这种方式往往存在着兼容性和 UI 风格方面的问题. 我们可以自己实现一个多媒体选择器. 首先来看看 Charles 是什么样子:
  简单使用
  我们来看看 Charles 应该如何使用, 首先启动 Charles :
  Charles.from(this@MainActivity)
  .choose()
  .maxSelectable(9)
  .progressRate(true)
  .theme(R.style.Charles)
  .imageEngine(GlideEngine())
  .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
  .forResult(REQUEST_CODE_CHOOSE)
  接收返回结果:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_CHOOSE && resultCode == Activity.RESULT_OK) {
val uris = Charles.obtainResult(data)
val paths = Charles.obtainPathResult(data)
mAdapter.setData(uris, paths)
Log.d("uris", "$uris")
Log.d("paths", "$paths")
}
}
  是不是很简单呢?
  特色
  从代码和上面的截图中可以发现, Charles 是支持主题的. Charles 的特性并不仅仅如此, 使用 Charles, 你可以:
  直接在 Activity 或者 Fragment 中调用;
  选择多种媒体文件, 包括图片, 视频, 音频和文件;
  应用不同的主题, 其中 Charles 内置了两套 theme: 日间模式(Charles)和夜间模式(CharlesDark). 如果这两套主题不符合你的需求, 你仍然可以自定义主题;
  指定最大可选数量;
  支持横竖屏. Charles 内部实现了状态保存, 因此 Configuration 的变化并不会对 Charles 带来不利的影响;
  支持多种图片加载库. Charles 内部提供了两种图片加载引擎: Glide 和 Picasso. 当然, 如果以上两种都不是你所需要的, 你还可以自定义图片加载引擎, 只需要实现一个接口就好了. Charles 目前并不支持 Fresco.
  在 Charles 的内部, 采用了 Loader 作为多媒体文件的加载器. 如果你对 Loader 感兴趣, 可以参考官方文档(https://developer.android.com/guide/components/loaders.html). Charles 内部并没有使用 MVP 或者 MVVM 等 MV* 实现解耦, 而且也没有采用过多的第三方库(实际上只依赖了 Kotlin, RecyclerView, AppCompat等少量的必须库).
  进阶使用
  图片引擎
  内置图片引擎
  Charles 内置了两种图片加载引擎: GlideEngine 和 PicassoEngine. 图片引擎可以通过下面的方式使用:
Charles.from(this@MainActivity)
...
.imageEngine(GlideEngine()) // 或 PicassoEngine()
...
.forResult(REQUEST_CODE_CHOOSE)
  自定义图片引擎
  通过实现 ImageEngine 接口实现自定义图片加载引擎. 举个例子, 如果你想要使用 Glide v4 的 generated API, 你可以定义如下类:
class GlideLoader : ImageEngine {
override fun loadImage(context: Context, imageView: ImageView, uri: Uri) {
GlideApp.with(context)
.load(uri)
.centerCrop()
.into(imageView)
}
}
  主题
  内置主题
  Charles 内置了两套主题: R.style.Charles (日间模式) 和 R.style.CharlesDark (夜间模式). 可以在启动 Charles 时执行 theme(@StyleRes themeId: Int) 方法指定主题:
  Charles.from(this@MainActivity)
  ...
  .theme(R.style.Charles) // 或 R.style.CharlesDark
  ...
  .forResult(REQUEST_CODE_CHOOSE)
  自定义主题
  通过内置的两套主题甚至是他们的父主题, 你可以自定义 Charles 的外观. 下面是你可以修改的属性(定义在 attrs.xml 文件中):
  colorPrimary: app bar 的 branding 颜色
  colorPrimaryDark: 状态栏的颜色
  toolbar: toolbar 主题
  toolbar.titleColor: toolbar 上已选类别的标题颜色
  categories.dropdown.titleColor: 可选类别下拉列表中类别的标题色
  categories.dropdown.icon.tint: 可选类别下拉列表中类别图标的颜色
  media.emptyView: 文件列表中空视图的图片
  media.emptyView.textColor: 文件列表中空视图的文字颜色
  media.checkView.iconColor: 已选文件的选择对勾的颜色
  media.checkView.backgroundColor: 已选文件视图的背景色
  media.checkView.borderColor: 文件的选择视图的边框颜色
  media.selected.backgroundColor: 已选文件的背景色
  media.titleColor: 文件的标题颜色
  media.descColor: 文件的描述文字的颜色
  page.backgroundColor: Activity 或 Fragment 页面的背景色
  bottomToolbar.backgroundColor: 底部工具条的背景色
  bottomToolbar.progress.textColor: 底部工具条的进度比率的文字颜色
  bottomToolbar.apply.textColor: 底部工具条应用按钮的文字颜色
  listPopupWindowStyle: 可选类别下拉列表的主题
  计数
  最大可选数量
  使用 maxSelectable(maxSelectable: Int) 限制文件的最大可选数量.
  已选进度
  使用 progressRate(toShow: Boolean) 决定是否显示当前的进度比率.
  屏幕方向
  使用 restrictOrientation(orientation: Int) 设置文件选择 Activity 的屏幕方向. 所有的待选值可以在 android.content.pm.ActivityInfo 类中找到.
  源码分析
  Charles 的整体项目结构:
  
Architecture
  Charles & SelectionCreator
  我们先看一下 Charles 类和 SelectionCreator 类:
class Charles {
// ...
private constructor(fragment: Fragment) : this(fragment.activity, fragment)
// other constructor and more...
companion object {
@JvmStatic
fun from(activity: Activity) = Charles(activity)
@JvmStatic
fun from(fragment: Fragment) = Charles(fragment)
@JvmStatic
fun obtainResult(data: Intent?) = data?.getParcelableArrayListExtra<Uri>(CharlesActivity.EXTRA_RESULT_SELECTION)
@JvmStatic
fun obtainPathResult(data: Intent?) = data?.getStringArrayListExtra(CharlesActivity.EXTRA_RESULT_SELECTION_PATH)
}
// ...
fun choose(): SelectionCreator = SelectionCreator(this)
}
  在 Charles 类中, 包含了4个静态方法, 按用途可划分为两种类型: Charles#obtainResult() 和 Charles#obtainPathResult() 用于完成文件选择后获取结果; Charles#from() 方法返回了 Charles 对象, 用于对 Charles 进行进一步的定制. Charles#choose() 方法创建了 SelectionCreator 对象并返回, 我们再来看看 SelectionCreator 类:
class SelectionCreator(private val mCharles: Charles) {
private val mSelectionSpec = SelectionSpec.cleanInstance
// ...
fun maxSelectable(maxSelectable: Int): SelectionCreator {
// ...
mSelectionSpec.maxSelectable = maxSelectable
return this
}
fun progressRate(isShow: Boolean): SelectionCreator {
mSelectionSpec.isShowProgressRate = isShow
return this
}
fun theme(@StyleRes themeId: Int): SelectionCreator {
mSelectionSpec.themeId = themeId
return this
}
fun imageEngine(imageEngine: ImageEngine): SelectionCreator {
mSelectionSpec.imageEngine = imageEngine
return this
}
fun restrictOrientation(screenOrientation: Int): SelectionCreator {
mSelectionSpec.orientation = screenOrientation
return this
}
/**
* Start to select media and wait for result.
*
* @param requestCode Identity of the request Activity or Fragment.
*/
fun forResult(requestCode: Int) {
// ...
}
}
  正如你所见到的, SelectionCreator 的主要作用就是对 Charles 进行各种参数的定制, 以符合各种不同的需求. 而可定制的项目均以成员变量的形式存储在 SelectionSpec 类中, 这里就不再列出了.
  CharlesActivity & CharlesFragment
  CharlesActivity 主要是用于显示 UI 组件, 例如包含了图片, 视频, 音频等选项的一个 ListPopupWindow, 另外, 在极端情况下, 数据的恢复也是在其中完成的. 我们重点来看看 CharlesFragment:
class CharlesFragment : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
private var mFilterType: MediaFilterType? = null
private lateinit var mMediaItemsAdapter: MediaItemsAdapter
private lateinit var mSelectionProvider: SelectionProvider
companion object {
private const val ARG_TYPE = "ARG_TYPE"
@JvmStatic
fun newInstance(filterType: MediaFilterType): CharlesFragment {
val fragment = CharlesFragment()
val bundle = Bundle()
bundle.putSerializable(ARG_TYPE, filterType)
fragment.arguments = bundle
return fragment
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is SelectionProvider) {
mSelectionProvider = context
} else {
throw IllegalStateException("Context must implement SelectionProvider.")
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_charles, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mFilterType = arguments?.getSerializable(ARG_TYPE) as MediaFilterType?
initViews()
mMediaItemsAdapter.registerOnMediaClickListener(object : MediaItemsAdapter.OnMediaItemClickListener {
override fun onItemClick(item: MediaItem, position: Int) {
if (activity != null && activity is CharlesActivity) {
(activity as CharlesActivity).updateBottomToolbar()
}
}
})
activity?.supportLoaderManager?.initLoader(0, null, this)
}
override fun onDestroyView() {
super.onDestroyView()
activity?.supportLoaderManager?.destroyLoader(MediaItemLoader.LOADER_ID)
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> = MediaItemLoader.newInstance(context!!, mFilterType!!)
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
mMediaItemsAdapter.swapCursor(data)
emptyTextView.visibility = if (mMediaItemsAdapter.itemCount == 0) View.VISIBLE else View.GONE
}
override fun onLoaderReset(loader: Loader<Cursor>) {
mMediaItemsAdapter.swapCursor(null)
}
private fun initViews() {
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.setHasFixedSize(true)
mMediaItemsAdapter = MediaItemsAdapter(mFilterType, mSelectionProvider.provideSelectedItemCollection())
recyclerView.adapter = mMediaItemsAdapter
}
interface SelectionProvider {
fun provideSelectedItemCollection(): SelectedItemCollection
}
}
  CharlesFragment 的布局为一个纯列表布局, 使用了 RecyclerView, 当然也就会使用到相应的 Adapter, 这里不贴出 Adapter 的代码. 我们重点看一下 CharlesFragment 实现的 LoaderManager.LoaderCallbacks 接口. 实际上, 这个接口和 Loader 相关. 为什么 Charles 会选用 Loader 呢?
  在Android中任何耗时的操作都不能放在UI主线程中, 所以耗时的操作都需要使用异步实现. 同样的, 在ContentProvider中也可能存在耗时操作, 这时也该使用异步操作, 而3.0之后最推荐的异步操作就是Loader. 它可以方便我们在Activity和Fragment中异步加载数据, 而不是用线程或AsyncTask, 他的优点如下:
  · 提供异步加载数据机制;
  · 对数据源变化进行监听, 实时更新数据;
  · 在Activity配置发生变化(如横竖屏切换)时不用重复加载数据;
  · 适用于任何Activity和Fragment.
  CharlesFragment 实现了 LoaderManager.LoaderCallbacks 接口之后, 就可以感应到数据的变化, 从而做相应的处理.

上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号