哈喽,大家好,好久不见了,很久没有更新 Android 方面的技术文章了,最近在忙公司的 AR 类的新产品,其中涉及到本地图片和视频的选择和上传功能。至于为什么不用系统提供的图片和视频选择器,原因你懂的,系统提供的选择器只能通过 Intent 方式去获取,这意味着需要离开当前页面前往系统的媒体库,选择完毕后在onActivityResult 方法中拿到结果。这显然存在很多弊端:

  • UI的定制化很差
  • 需要离开当前页面,体验不好
  • 不同机型可能会出现各种问题
  • 系统选择器并不支持多选功能

​其实,我们最希望的是拿到手机中的图片和视频数据,至于UI的绘制和交互细节都由我们自己来定制。你说你想用 ListView 或者 RecyclerView 来展示所有图片和视频,ok,当然可以,那是你的自由!让我们先来看一下最终实现的效果图吧:

不要直接一看效果图以为还是前往的另一个页面,那和其他图片选择器有什么分别?客官先别急,这里的效果图只是为了美观而已,反正数据给你了,想怎么安排UI就看你们设计喵了?~,比如可以这样:

看到这你可能会以为很复杂,其实不然,代码量很少,而且涉及到的核心知识点如:获取系统图片和视频数据、单选和多选功能,相信大家一看就明了。好了,喝口茶,且听我慢慢道来。

获取手机所有图片和视频数据

一般地,获取手机内部图片和视频数据有两种方式:通过遍历文件夹获取图片和视频资源,或者通过ContentResolver来获取。虽然第一种方式拿到的图片比较齐全,但文件遍历操作过于耗时,这里我推荐采用第二种方式。ContentResolver即内容解析器,可以对ContentProvider中的数据库进行增删改查操作,其中主要包含联系人、短信、相册、视频、音频等一系列数据。我们来看看具体获取系统图片数据实现代码吧:

/*** <pre>*     @author moosphon  (about me: <a>https://github.com/Moosphan<a/>)*     @date   2018/09/16*     @desc   get all pictures of the phone.* <pre/>*/
fun getLocalPictures(mContext: Context?): List<ImageMediaEntity>? {val images = ArrayList<ImageMediaEntity>()val resolver = mContext?.contentResolvervar cursor: Cursor? = nullqueryImageThumbnails(resolver!!, arrayOf(MediaStore.Images.Thumbnails.IMAGE_ID, MediaStore.Images.Thumbnails.DATA))try {cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,arrayOf(MediaStore.Images.ImageColumns.DATA,MediaStore.Images.ImageColumns._ID,MediaStore.Images.ImageColumns.SIZE,MediaStore.Images.ImageColumns.MIME_TYPE),null, null, null)return if (cursor == null || !cursor.moveToFirst()) {null} else {do {val picPath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA))val id = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID))val size = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.SIZE))val mimeType = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE))val image = ImageMediaEntity.Builder(id, picPath).setMimeType(mimeType).setSize(size).setThumbnailPath(mThumbnailMap?.get(id)).build()images.add(image)mThumbnailMap = null}while (cursor.moveToNext())return images}} finally {if (cursor != null) {cursor.close()}}
}/*** search for thumbnails for local images** @author moosphon*/private fun queryImageThumbnails(cr: ContentResolver, projection: Array<String>) {var cur: Cursor? = nulltry {cur = MediaStore.Images.Thumbnails.queryMiniThumbnails(cr, MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,MediaStore.Images.Thumbnails.MINI_KIND, projection)if (cur != null && cur.moveToFirst()) {do {val imageId = cur.getString(cur.getColumnIndex(MediaStore.Images.Thumbnails.IMAGE_ID))val imagePath = cur.getString(cur.getColumnIndex(MediaStore.Images.Thumbnails.DATA))mThumbnailMap = mapOf(imageId to imagePath)} while (cur.moveToNext() && !cur.isLast)}} finally {cur?.close()}}

可以通过代码看到,我们借助于 ContentResolver.query 方法来查询匹配的图片数据,我们可以设置需要获取的图片的数据字段,如 MediaStore.Images.ImageColumns.DATA 就表示图片存储的路径信息,其他的可以获取的信息还有图片ID、图片大小、图片类型等,大家可以参照代码去网上查看具体含义,这里不再赘述。此外,系统还为我们存储了图片以及视频的缩略图数据,我们为了提高图片加载速度,可以通过获取和展示缩略图的形式来增强体验效果。获取图片缩略图的方式采用系统自带的,也比较简单,大家可以自行查看一下文档。

另外,大家可能会发现 ImageMediaEntity 这个类,明白人应该很快就会知道这个数据类主要存储一些图片相关的数据。的确,这个是我个人封装的一层针对图片的数据类,而它还有个父类,名叫 BaseMediaEntity ,我们来看看里面都有些啥:

/*** base entity data for local media** @author Moosphon*/
public abstract class BaseMediaEntity implements Parcelable{protected enum TYPE{IMAGE,VIDEO}protected String path;protected String id;protected String size;public Boolean isSelected = false;public BaseMediaEntity() {}public BaseMediaEntity(String path, String id) {this.path = path;this.id = id;}public BaseMediaEntity(Parcel in) {this.path = in.readString();this.id   = in.readString();this.size = in.readString();}public abstract TYPE getMediaType();public String getPath() {return path;}public void setPath(String path) {this.path = path;}public String getId() {return id;}public void setId(String id) {this.id = id;}public String getSize() {return size;}public void setSize(String size) {this.size = size;}public Boolean getSelected() {return isSelected;}public void setSelected(Boolean selected) {isSelected = selected;}@Overridepublic int describeContents() {return 0;}@Overridepublic void writeToParcel(Parcel dest, int flags) {dest.writeString(this.path);dest.writeString(this.id);dest.writeString(this.size);}
}

可以看到,这是我们抽离出的公共基类,因为图片和视频等多媒体数据都有公共的数据字段id、path和size,差异性由它的子类来实现就OK了。至于 ImageMediaEntityVideoMediaEntity 具体代码就先省略不放了,影响篇幅长度,最后面会有完整的sample代码。

看完了本地图片数据的获取,自然而然就能知道视频数据也是采用相同的方式获取,没错,这里就直接上代码了,其实实现方式是一样的:

/*** <pre>*     @author moosphon  (about me: <a>https://github.com/Moosphan<a/>)*     @date   2018/09/16*     @desc   get all videos of the phone.* <pre/>*/
fun getLocalVideos(mContext: Context?) : List<VideoMediaEntity>?{val videos = ArrayList<VideoMediaEntity>()val resolver = mContext?.contentResolvervar cursor: Cursor? = nulltry {cursor = resolver?.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,arrayOf(MediaStore.Images.ImageColumns.DATA,MediaStore.Video.Media._ID,MediaStore.Video.Media.DISPLAY_NAME,MediaStore.Video.Media.RESOLUTION,MediaStore.Video.Media.SIZE,MediaStore.Video.Media.DURATION,MediaStore.Video.Media.DATE_MODIFIED),MediaStore.Video.Media.MIME_TYPE + "=?", arrayOf("video/mp4"), null)return if (cursor == null || !cursor.moveToFirst()) {null} else {while (cursor.moveToNext()){// video pathval path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA))// video idval id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID))// video display nameval name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME))// video resolutionval resolution = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.RESOLUTION))// video sizeval size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE))// video durationval duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION))val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED))val video = VideoMediaEntity.Builder(id.toString(), path).setTitle(name).setDateTaken(date.toString()).setDuration(duration.toString()).setSize(size.toString()).build()videos.add(video)}return videos}} finally {if (cursor != null) {cursor.close()}}
}

通过上面代码我们可以发现,这几乎和获取图片数据的代码一样啊,没错,是几乎一样,但留意的人会发现,这里我调用 ContentResolver.query 时多传了一个selection参数,它是query方法的第三个参数,主要用来设置一些查询的条件,已达到过滤功能,大家可以根据自己需要自行设置,这里我只是想拿到mp4格式的视频数据。还有人可能会问:为什么我这里没有获取视频的缩略图数据呢?系统虽为我们也提供了获取视频缩略图的方式,但是,并不是所有的视频都存在视频缩略图,这就造成你想加载视频的缩略图的时候会出现大片空白数据问题。同时,可能会有人想借助于其他方式获取,但主流的几种方式都比较耗时,不建议在正式项目中采用。其实,通过查看很多优秀的开源视频选择器框架发现,很多都采用了分批加载功能,比如手机中一共有一千个视频数据,如果一次性获取显然很耗时,而且体验不好,我们可以分批获取数据,每页100条限制,这就极大的节省了获取数据的时间,然后再在列表滑动到底部时加载下一批数据。这里我暂时使用的是 Glide 来加载我们的视频数据,后续会寻找更佳方案代替。

下面,我们来看看图片视频的多选、单选效果实现。用过 RecyclerView 和 CheckBox 组合的开发者都知道,RecyclerView复用性会导致 CheckBox 选择状态混乱,即onCheckChanged方法的“神秘回调”,解决方案也有很多种,网上有些方案没有解决问题的也有很多。常见的方案有:自定义 checkbox、通过 checkbox 的 onclick 事件来处理选中状态,adapter数据刷新或者 checkbox 每次选中前移除上次的选中事件等等,我只选两种进行简单说明。为了节省时间,我这里将实现图片多选和视频的单选功能,它们 checkbox 问题的处理各自采用不同的方式。

我们先来看看图片多选功能实现,前方高能,代码来袭:

/*** <pre>*    author: moosphon*    date:   2018/09/16*    desc:   本地视频的适配器* <pre/>*/
class LocalImageAdapter: RecyclerView.Adapter<LocalImageAdapter.LocalImageViewHolder>() {lateinit var context: Contextprivate var mSelectedPosition: Int = 0var listener: OnLocalImageSelectListener? = nullprivate lateinit var data: List<ImageMediaEntity>/** 存储选中的图片 */private var chosenImages : HashMap<Int, String>  = HashMap()/** 存储选中的状态 */private var checkStates  : HashMap<Int, Boolean> = HashMap()override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocalImageViewHolder {context = parent.contextval view = LayoutInflater.from(parent.context).inflate(R.layout.rv_item_local_video_layout, parent, false)return LocalImageViewHolder(view)}override fun getItemCount(): Int {return data.size}override fun onBindViewHolder(holder: LocalImageViewHolder, position: Int) {val thumbnailImage: ImageView = holder.view.find(R.id.local_video_item_thumbnail)val checkBox: CheckBox = holder.view.find(R.id.local_video_item_cb)/** 通过map存储checkbox选中状态,放置rv复用机制导致的状态混乱状态 */checkBox.setOnCheckedChangeListener(null)checkBox.isChecked = checkStates.containsKey(position)val options = RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE).error(R.mipmap.ic_launcher).placeholder(R.mipmap.ic_launcher)Glide.with(context).asBitmap().load(data[position].thumbnailPath).apply(options).thumbnail(0.2f).into(thumbnailImage)checkBox.setOnCheckedChangeListener{_, isChecked ->if (isChecked){checkStates[position] = true// 将当前选中的图片存入mapchosenImages[position] = data[position].path}else{// 从选中列表中移除checkStates.remove(position)chosenImages.remove(position)}if (listener != null){val selectedImages  = ArrayList<String>()for (v in chosenImages.values){selectedImages.add(v)}listener!!.onImageSelect(holder.view, position, selectedImages)}}}fun setData(data: List<ImageMediaEntity>){this.data = datafor (i in 0 until data.size) {if (data[i].isSelected) {mSelectedPosition = i}}}class LocalImageViewHolder(val view: View) : RecyclerView.ViewHolder(view)/** 自定义的本地视频选择监听器 */interface OnLocalImageSelectListener{fun onImageSelect(view: View, position:Int, images: List<String>)}}

可以看到,我们这里通过 HashMap 存储已选中 CheckBox 的状态,并在 checkBox.setOnCheckedChangeListener 前移除上一次 CheckBox 的监听器,然后再在 onCheckChanged 方法中判断当前选中状态,如果选中,那么map存入 CheckCox 选中状态,否则移除当前位置的value数据,这样,就解决了 滑动RecyclerViewCheckBox 状态混乱问题。同时,我们用 Map 存储每个选中后的图片路径信息,然后在自己的回调中返回这些选中的图片,最后在 Activity 或者 Fragment 中展示就可以了。

实现了图片的多选效果,我们就来看看视频单选的实现吧:

/*** <pre>*    author: moosphon*    date:   2018/09/16*    desc:   本地视频的适配器* <pre/>*/
class LocalVideoAdapter: RecyclerView.Adapter<LocalVideoAdapter.LocalVideoViewHolder>() {lateinit var context: Contextprivate var mSelectedPosition: Int = -1var listener: OnLocalVideoSelectListener? = nullprivate lateinit var data: List<VideoMediaEntity>private var checkState: HashSet<Int> = HashSet()override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocalVideoViewHolder {context = parent.contextval view = LayoutInflater.from(parent.context).inflate(R.layout.rv_item_local_video_layout, parent, false)return LocalVideoViewHolder(view)}override fun getItemCount(): Int {return data.size}override fun onBindViewHolder(holder: LocalVideoViewHolder, position: Int) {val thumbnailImage: ImageView = holder.view.find(R.id.local_video_item_thumbnail)val checkBox: CheckBox = holder.view.find(R.id.local_video_item_cb)checkBox.isChecked = checkState.contains(position)val options = RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE).error(R.mipmap.ic_launcher).placeholder(R.mipmap.ic_launcher)Glide.with(context).asBitmap().load(data[position].path).apply(options).thumbnail(0.2f).into(thumbnailImage)checkBox.setOnClickListener {if (mSelectedPosition!=position){//先取消上个item的勾选状态checkState.remove(mSelectedPosition)notifyItemChanged(mSelectedPosition)//设置新Item的勾选状态mSelectedPosition = positioncheckState.add(mSelectedPosition)notifyItemChanged(mSelectedPosition)}else if(checkBox.isChecked){checkState.add(position)}else if(!checkBox.isChecked){checkState.remove(position)}if (listener != null){listener!!.onVideoSelect(holder.view, position)}}}fun setData(data: List<VideoMediaEntity>){this.data = datafor (i in 0 until data.size) {if (data[i].isSelected) {mSelectedPosition = i}}}class LocalVideoViewHolder(val view: View) : RecyclerView.ViewHolder(view)/** 自定义的本地视频选择监听器 */interface OnLocalVideoSelectListener{fun onVideoSelect(view:View, position:Int)}}

此处主要利用 checkBox.setOnClickListener 以及 HashSet 来处理单选事件,先通过一个mSelectedPosition字段来保存当前选中的 Checkbox 的位置,然后在点击事件中进行分情况处理,由于这里是单选,所以在设置新的选中状态前移除上一次的CheckBox 选中状态。代码没什么复杂的,主要是一种思路,具体逻辑理清楚就好了,这里大家可以自己琢磨一下。

Github传送门:https://github.com/Moosphan/LocalVideoImage-selector

欢迎大家提出改进意见或者帮助我一起完善下去~

Android简单实现本地图片和视频选择器功能相关推荐

  1. android中调用系统功能 来显示本地相册图片 拍照 视频 音频功能

    android中调用系统功能 来显示本地相册图片 拍照 视频 音频功能 效果图如下: 本地相册跟拍照可直接调用系统功能 Intent img = new Intent(MediaStore.ACTIO ...

  2. Android使用ACTION_VIEW查看图片和视频

    Android使用ACTION_VIEW查看图片和视频 一.目标 二.实现方案 三.最终代码 四.过程回顾 五.接下来 六.Finally 神马笔记已经实现在笔记中插入图片和视频,但是不能全屏查看. ...

  3. Android 加载本地图片(文件管理器中的图片墙)

    Android 加载本地图片(文件管理器中的图片墙) --关于图片墙的一些感悟与疑问,希望大家共同探讨. (By伊叶也) 图片显示及监听 1.图片显示:基本上就5种显示形式(如果同时嵌入5种形式,采用 ...

  4. Android仿微信聊天记录“图片及视频”默认最新图片从底部显示(时间排序升序)

    Android仿微信聊天记录"图片及视频"默认最新图片从底部显示(时间排序升序) 1.设置recycler的LinearLayoutManager LinearLayoutMana ...

  5. 简单的本地图片服务器的搭建

    简单的本地图片服务器的搭建 第一步:安装部署 Nginx 下载 Nginx 下载完解压后 第二步: 搭建图片服务器 第一步:安装部署 Nginx 下载 Nginx 保存文件路径不要包含中文! Linu ...

  6. android picasso 显示本地图片,剖析Picasso加载压缩本地图片流程(解决Android 5.0部分机型无法加载本地图片的问题)...

    之前项目中使用Picasso遇到了一个问题:在Android 5.0以上版本的部分手机上使用Picasso加载本地图片会失败.为了解决这个问题,研究了一下Picasso加载和压缩本地图片的流程,才有了 ...

  7. android ImageButton显示本地图片

    2019独角兽企业重金招聘Python工程师标准>>> 得到本地图片(png,jpeg,gif)的路径后,将图片显示在ImageButton上.这里先读出图片大小,在设置采样率,使得 ...

  8. android分享图片到qq,Android 如何实现本地图片直接分享到微信、来往、QQ等,直接分享...

    在不使用第三方的SDK情况下,如何在Android上直接分享图片到指定的应用上,例如分享到微信.来往.QQ等.具体实现见贴出的代码.实现后才发现好简单的 例如:要在app中直接分享图片到微信.来往等, ...

  9. Android获取手机本地图片并显示

    一.序言 在安卓开发过程中,有时候我们的应用需要使用手机本地图片,这就需要本地图片访问权限以及相关的获取方法,本文将手机本地图片的获取流程和代码做了一个总结,希望能够对大家有一定帮助: 二.功能分析 ...

  10. 基于uniapp+unicloud的日记系统,可课设毕设,有地图定位、图片、视频等功能,可以在手机和模拟器上运行,真机运行

    日记系统 基于uniapp的日记系统,采用unicloud云数据库进行存储,拥有注册登录,发布.删除.修改和查看日记的功能,可以进行地图定位功能,图片.视频的增删改查,还可进行日记首页的封面和头像的更 ...

最新文章

  1. 量子科技概念大火,国内现状如何?国盾量子撑起量子通信,华为BAT均入局量子计算...
  2. operator did not match Pytorch‘s Interpolation until opset 11
  3. struts2.0获取各种表单的数据
  4. 《微服务:从设计到部署》中文版
  5. 查询工资最低的3名员工的职工工号、姓名和收入_关于工资条,这6个常识必须掌握,事关你的权益!...
  6. Direct3D学习_绘制
  7. 软件工程师的十个“不职业”行为
  8. MySQL中InnoDB引擎对索引的扩展
  9. 第三季-第8课-系统调用方式文件编程
  10. 线性分类器(Linear Classifier)
  11. phpstudy重置密码登录报错#1045
  12. 再添荣誉!青软集团获评「山东省科技小巨人企业」称号
  13. CSS实现炫酷动画背景
  14. 读书笔记: 《亿级流量网站架构核心技术》(开涛的那本)
  15. 值传递,指针传递,引用传递的区别
  16. Eclipse使用Log4j2的详细教程
  17. C语言——基本编写规范
  18. draw.io编辑工具
  19. IT女新加坡求职记(三篇)
  20. 任泽平:谏言、真相与几句心里话

热门文章

  1. 联通智能城域网,到底有什么特别?
  2. (Django开发)免费HTML模板资源集合
  3. 新趋势下的云计算安全行业前沿认证|CCSK
  4. Android MIDI音乐播放/生成相关总结
  5. windows系统上使用magic trackpad妙控触摸板
  6. docker之SonarQube集成阿里p3c规则
  7. Java实现zip文件压缩与解压缩--附完整代码
  8. ElasticJob分布式调度,监听器的使用附源码(四)
  9. php文本生成图片,php文本文字创建生成图片_PHP教程
  10. idea 因破解而无法打开的问题