测试工具用例:Charles Android 文件选择器源码分析
白羽 2018-05-31 来源 :网络 阅读 986 评论 0

摘要: 本文将带你了解测试工具用例:Charles Android 文件选择器源码分析,现在的 App 很多功能都与多媒体有关, 例如图片, 视频, 音频, 文件等等. 在 Android 开发中, 如果需要从本地选择多媒体文件, 我们可以通过调用系统的 DocumentsUI 来实现, 当然通过这种方式往往存在着兼容性和 UI 风格方面的问题. 我们可以自己实现一个多媒体选择器. 首先来看看 Charles 是什么样子:希望本文对大家学测试工具有所帮助。



  介绍

  现在的 App 很多功能都与多媒体有关, 例如图片, 视频, 音频, 文件等等. 在 Android 开发中, 如果需要从本地选择多媒体文件, 我们可以通过调用系统的 DocumentsUI 来实现, 当然通过这种方式往往存在着兼容性和 UI 风格方面的问题. 我们可以自己实现一个多媒体选择器. 首先来看看 Charles 是什么样子:

 测试工具用例:Charles Android 文件选择器源码分析

  简单使用

  我们来看看 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 的整体项目结构:

  测试工具用例:Charles Android 文件选择器源码分析

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 接口之后, 就可以感应到数据的变化, 从而做相应的处理.

 


本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标软件测试之测试工具频道!


本文由 @白羽 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程