Picture Selector

挣扎无比渺小

若黑暗来临前一丝光明

终究闪亮不了整个夜空

挣扎无比的渺小

若黎明来临前一丝黑暗

终究阻挡不了一丝光明

云卷云舒,道一声变迁无常

花开花落,奏一曲世态炎凉

床头搁着你的梦

小楼东风,拂碎芳香

人尽染

愿岁月静好,来日方长

Picture Selector --- --- 打造属于你的视图选择器

作为前端与移动端的基础需求,一个功能强大与视觉效果巨佳的视图选择器是最基础,也是最必不可少的模块之一。

我们在进行一个项目的过程中,往往注重项目框架的建立与逻辑的实现。

在没有充分时间的情况下,是很难有时间去专注的做一款与自己项目、需求适配的选择器出来

实际情况是即使你有时间,有精力。一款完美的选择器也非一人之功而为之。

java层的代码实现无法完美的做到一些底层的算法处理,对于图片和视频的操作也有一定的局限性

所以涉及到裁剪与压缩,java的效率还是低于一个良好的c库去进行

基于这种情况,网上出现了很多第三方或者个人的裁剪压缩类库,经常见到的有

SoundCloud cropping library,Edmodo Cropper,Scissors,uCrop。在进行一些比对只后,我更倾向于uCrop。uCrop高度的自定义选择性可以满足你遇到的各种需求。

而且uCrop对自己的裁剪与压缩做了c算法的处理。运行的流畅度也有了很大的提升。

选择好了裁剪与压缩的三方类库uCrop,我们需要去定义自己的选择器,并将与uCorp进行关联。

之前用过3个视图选择器,但是效果都不理想,实现了基本功能,但是界面,可操作性,都有很多瑕疵。

最重要的是集成的东西一定要具有可自定义性,高度的可扩展性,必要的时候可以进行二次开发。

【首先感谢picture_library这个库的创作团队,让我有了开发的模板,从而让项目顺利的进行】

结尾有彩蛋哦!

一、集成

picture_library  module

picture_library modle自身使用了uCrop进行图片的裁剪与压缩,所以我们不用再去集成uCrop,这会给我们节省大量的时间,来让我们将picture_library打造的更加适应我们的项目。

通过Flie ---> New ---> Import Moudle 来选择我们要集成的Moudle

然后选择我们要集成的Moudle,因为其对uCrop有依赖,所以我们同时要将uCrop集成进项目

点击Finish,这样我们的picture_library已经添加进我们的项目了,但是我们还需要最后一步需要做。

就是将picture_library和我们的项目添加依赖。

使用快捷键Ctrl+Shift+Alt+S打开项目结构面板,app --->Denendencies ---> + --->Module Dependency选择picture_library

进行依赖。

现在,我们的picture_library已经成功的集成进入我们的项目当中了。

这就是picture_library的第一个优点,易于集成

二、使用picture_library

因为以后需要大量的使用picture_library,我将简写成PL。

PL使用了链式开发,并且大量使用接口回调,与观察者模式。项目架构值得学习,在这种高度模式化的开发下,PL提供了大量易于理解和操作的API。使用这些API,我们可以很轻松的实现我们需求特定的视图选择器。

我们只需要一路点点点,将我们的属性设置进去,就能达到我们想要的效果:

//进入相册 以下是例子:不需要的api可以不写

PictureSelector.create(MainActivity.this)

.openGallery(chooseMode)//全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()

.theme(themeId)//主题样式设置 具体参考values/styles用法:R.style.picture.white.style

.maxSelectNum(maxSelectNum)//最大图片选择数量

.minSelectNum(1)//最小选择数量

.selectionMode(cb_choose_mode.isChecked() ?

PictureConfig.MULTIPLE: PictureConfig.SINGLE)//多选or单选

.previewImage(cb_preview_img.isChecked())//是否可预览图片

.previewVideo(cb_preview_video.isChecked())//是否可预览视频

.compressGrade(Luban.THIRD_GEAR)// luban压缩档次,默认3档Luban.FIRST_GEAR、Luban.CUSTOM_GEAR

.isCamera(cb_isCamera.isChecked())//是否显示拍照按钮

.enableCrop(cb_crop.isChecked())//是否裁剪

.compress(cb_compress.isChecked())//是否压缩

.compressMode(compressMode)//系统自带or鲁班压缩.glideOverride(160,160)// glide加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度

.withAspectRatio(aspect_ratio_x,aspect_ratio_y)//裁剪比例 如16:9 3:2 3:4 1:1可自定义

.hideBottomControls(cb_hide.isChecked() ?false:true)//是否显示uCrop工具栏,默认不显示

.isGif(cb_isGif.isChecked())//是否显示gif图片

.freeStyleCropEnabled(cb_styleCrop.isChecked())//裁剪框是否可拖拽

.circleDimmedLayer(cb_crop_circular.isChecked())//是否圆形裁剪

.showCropFrame(cb_showCropFrame.isChecked())//是否显示裁剪矩形边框 圆形裁剪时建议设为false

.showCropGrid(cb_showCropGrid.isChecked())//是否显示裁剪矩形网格 圆形裁剪时建议设为false

.openClickSound(cb_voice.isChecked())//是否开启点击声音

.selectionMedia(selectList)//是否传入已选图片

.previewEggs(false)//预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中)

.isRemove(true)//是否移除图片列表已损坏的图片

.cropCompressQuality(90)//裁剪压缩质量

.compressMaxKB(1024)//压缩最大值kb compressGrade()为Luban.CUSTOM_GEAR有效

.compressWH(4,5)//压缩宽高比compressGrade()为Luban.CUSTOM_GEAR有效

.cropWH(4,5)//裁剪宽高比,设置如果大于图片本身宽高则无效

.rotateEnabled(false)//裁剪是否可旋转图片

.scaleEnabled(true)//裁剪是否可放大缩小图片

.videoQuality(1)//视频录制质量0 or 1

.videoSecond(0)//显示多少秒以内的视频

.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code

如此多的属性设置,你又能用到几个能。从预览模式到裁剪到压缩,应有尽有。基本很难遇到这么庞大的需求,让我们把每个api都开放,在你不想使用的时候,关闭特定的api便可。

这就是PL的第二个优点,高度的可自定义性。

使用上面的代码我们可以控制传入变量private intchooseMode= PictureMimeType.ofAll();

对选择器进行功能选择,PL提供了三种模式  .ofAll()   .onImage()   .onVideo() 分别为图片/视频同时显示,只显示图片,只显示视频。

其参数总体配置在了Pl Activity继承的一个超类里面,我们可以很方便的获取到需要的参数,对PL进行代码优化与需求定制。我们不再需要去寻找一个视频选择器来使我们的项目变得臃肿。

这就是PL的第三个优点,高度的可扩展性。

至于需求定制,下面我会提到。我们继续操作我们的PL,当我们打开选择器成功后,我们往往需要回调出一张图片的路径,做需求的处理。PL是这样实现的

@Overrideprotected voidonActivityResult(intrequestCode, intresultCode,Intent data) {

super.onActivityResult(requestCode,resultCode,data);

if(resultCode ==RESULT_OK) {

switch(requestCode) {

casePictureConfig.CHOOSE_REQUEST:

//图片选择结果回调

selectList= PictureSelector.obtainMultipleResult(data);

adapter.setList(selectList);

adapter.notifyDataSetChanged();

DebugUtil.i(TAG,"onActivityResult:"+selectList.size());

break;

}

}

}

通过一个List集合,将图片的路径回调出来,我们就可以进行我们自己对图片/视频的操作了。

PL提供了两种回调,一个是基于Fragment,一个基于Activity,本质并没有多大区别,我们只需要注意在打开选择器时传入的第一个参数。

如果当前环境Context为Activity,我们需要传入的参数应为XXXActivity.this

如果为Fragment,我们需要传入的参数为XXXFragment.this

到此,我们的基本功能已经完全实现了,而且PL自带图片预览与视频预览,并且提供了3种界面的Style,我们可以根据自己的主题进行适配。 PL自身主题已经非常美观,符合大众审美的水准,当你使用后,你一定会喜欢。

这就是PL的第四个优点,适应大众需求且界面美观

接下来,我们需要深入的去学习PL的源码,从而在需要的时候,进行代码优化与二次开发。

三、深入picture_library

我们自己的项目,一般都会有一个自己的文件夹,或者缓冲一些缓存,或者存储一些资源,这些都是非常普遍的,之所以去了解PL的源码构成,是项目出现一个我自己也很认可的需求。就是我们在录制完视频后,需要对刚录制的视频进行选择,但是当打开选择器后,刚录制的视频没有在第一个条目,如果用户视频较多,则很难在繁杂的视频堆里挑选出刚录制的视频,这对用户体验几乎是致命的打击,虽然功能实现了,但是用户体验下降,那么代码也是不完美的。

在这种情况下,我们已经无法通过预植的参数去响应我们的需求,那么们只能去改变PL的源码,以适应我们的需求。

改写第三方的东西永远是一件痛苦的事情,但人总是在痛苦中成长,越痛苦,成长的也就越快,温床上永远诞生不了君王。

我的思路如下

1、 刚拍摄的视频无法出现在PL的第一位,肯定是PL没有对加载的数据进行排序,导致List乱序的排列。

解决方法,在PL gridAdapter设置数据时,对list进行排序。但是这时问题出现了,排序需要一个规则,我们拿什么模板来映射这个规则。

最好的方法是通过时间来排序,但是查看PL源码,其并没有时间这个属性。

像LocalMedia中添加属性,工作量太大,牵扯到的引用太多,而且也无法准确的获取到每个视频录制的时间来作为排序的模板。

同时产品经理需要在一进入选择器第一个显示的就是我们自己的文件夹,这时我们可以改变思路

2、我们可以对我们自己录制的文件名加时间作为参数,然后对其进行降序排序。同时改变视频选择时默认显示的文件夹。这样就可以实现列表第一个是我们刚录制的视频。

实际操作如下

1、对加载进gridAdapter的list按名称进行降序排序

2、对PL视频选择情况下默认文件夹显示做更改

遇到的问题是,对图片选择情况下不做任何变动,这是我们必须要有一个参数来控制我们各种显示模式下的显示情况

这时我想到了我们create PL的时候,传入了一个参数

private intchooseMode= PictureMimeType.ofAll();

如果我们能够获得这个参数作为标识符,那么我们的逻辑是很顺利的。

我们开始使用

.openGallery(chooseMode)来选择浏览模式,发现这个方法属于类PictureSelector

publicPictureSelectionModelopenGallery(intmimeType) {

return newPictureSelectionModel(this,mimeType);

}并且通过PictureSelectionModel的构造器将参数传入。继续追踪,发现在PictureSelectionModel构造器中又将参数设置给了

PictureSelectionConfig。到这里,我们基本上找到了源头,在我们链式开发的时候,我们将每一个参数通过构造器或者赋值给一个基层的配置管理器。然后在其他地方需要调用的时候,直接从配置管理器中拿参数。

那么我们也可以在我们需要添加标识的地方从

PictureSelectionConfig拿出我们的浏览模式作为标识。查看我们实际操作的类是PL的主Activity------  PictureSelectorActivity 发现其继承与 PictureBaseActivity。而PictureBaseActivity有许多同包开放的参数我们可以静态的获得

public classPictureBaseActivityextendsFragmentActivity {

protectedContextmContext;

protectedPictureSelectionConfigconfig;

protected intspanCount,maxSelectNum,minSelectNum,compressQuality,

selectionMode,mimeType,videoSecond,compressMaxKB,compressMode,

compressGrade,compressWidth,compressHeight,aspect_ratio_x,aspect_ratio_y,

recordVideoSecond,videoQuality,cropWidth,cropHeight;

protected booleanisGif,isCamera,enablePreview,enableCrop,isCompress,

enPreviewVideo,checkNumMode,openClickSound,numComplete,camera,freeStyleCropEnabled,

circleDimmedLayer,hideBottomControls,rotateEnabled,scaleEnabled,previewEggs,statusFont,

showCropFrame,showCropGrid,previewStatusFont;

protectedStringcameraPath;

protectedStringoriginalPath;

protectedPictureDialogdialog;

protectedPictureDialog

protected list selectionMedias;

而这些参数里面,正好有我们需要的,在刚开始创建的PictureSelectionConfig对象。

有了这个标识,我们只需要找到初始化

PictureSelectorActivity 数据的地方,然后对其做一定的修改便可。

我们在bindImagesData()方法对adapter设置了数据,那么初始化数据的地方,也就是调用bindImagesData的地方。查看方法调用,我们得到了两个地方调用了bindImagesData();

(1)一进入PL选择器就调用了一次bindImagesData()

(2)在PL选择器控制目录结构发生改变的时候,调用了一侧bindImagesData()

从此分析,我们只需要在第一次进入PL的时候做调整,而目录结构发生改变的时候,我们是不需要做任何改变的。

到此,我们需要用到断点调试,对bindImagesData()方法中的每一句代码和每一个参数,做一个深入性的理解

protected voidreadLocalMedia() {

mediaLoader.loadAllMedia(newLocalMediaLoader.LocalMediaLoadListener() {

@Override

public voidloadComplete(List folders) {

DebugUtil.i("loadComplete:"+ folders.size());

if(folders.size() >0) {

foldersList= folders;

LocalMediaFolder folder = folders.get(0);

folder.setChecked(true);

folderWindow.bindFolder(folders);

List localImg = folder.getImages();if(localImg.size() >=images.size()) {

images= localImg;

}

}

if(adapter!=null) {

adapter.bindImagesData(images);

tv_empty.setVisibility(folders.size() >0

? View.INVISIBLE: View.VISIBLE);

}

dismissDialog();

}

});

}

此方法传入了一个文件集合,实体为LocalMediaFolder,断点后发现list.get(0)是一个恒不变的定值,表示的是主目录相机胶卷。而其后的值,则是存放图片/视频的各个文件夹。

那么 这时便简单了,我们只需要遍历folders集合,对其文件名为自己项目文件的路径做展示,也只加载自己文件夹下的数据,这样就实现了第一次进入RL直接就现在在自己的文件结构下。

/***解决视频选择默认显示问题

* 1,如果是选择视频。 则默认显示到MoieFile,并在向RecyclerView中加载图片时倒序排序

* 2,如果是选择照片或者全选,则默认显示全盘文件,并对文件进行倒序排序。

*/

protected voidreadLocalMedia() {

mediaLoader.loadAllMedia(newLocalMediaLoader.LocalMediaLoadListener() {

@Override

public voidloadComplete(List folders) {

DebugUtil.i("loadComplete:"+ folders.size());

if(config.mimeType== PictureMimeType.ofVideo()) {//如果是视频选择

if(folders.size() >0) {

foldersList= folders;

for(inti =0;i

LocalMediaFolder mediaFolder = folders.get(i);

if(mediaFolder.getName().equals("MoieFile")) {//如果是自己项目文件夹

mediaFolder.setChecked(true);

picture_title.setText(mediaFolder.getName());//设置文件夹名

folderWindow.bindFolder(folders);

List localImg = mediaFolder.getImages();

if(localImg.size() >=images.size()) {

images= localImg;

}

}

}

}

}else{//如果是图片选择模式或者全选模式

if(folders.size() >0) {

foldersList= folders;

LocalMediaFolder folder = folders.get(0);

folder.setChecked(true);

folderWindow.bindFolder(folders);

List localImg = folder.getImages();if(localImg.size() >=images.size()) {

images= localImg;

}

}

}

if(adapter!=null) {

adapter.bindImagesData(images);

tv_empty.setVisibility(folders.size() >0

? View.INVISIBLE: View.VISIBLE);

}

dismissDialog();

}

});

}

RL源码的改写虽然有一定的曲折在里面,但是经过不断的了解,很顺利的实现了功能的需求。

所以这就是RL的第五个优点,使用架构清晰,便于二次开发

好了,到此,我们的所有功能已经完全实现,但是对RL的探究还远远不够,RL的观察者模式还未触及,待设计模式学习的更加深入,再去探究RL的观察者模式。

最终实现效果图:

结尾小彩蛋 :android媒体库更新

android从4.0之后,不再对媒体库更新提供稳定的支持,即使你通过广播任何方法,也无法稳定的去更新媒体库。

这样就造成了你刚录制的视频,无法立马出现在你自己的选择器当中。

那么什么时候才会出现呢:在你重启手机的时候,因为系统的媒体库在每次开关机操作都会进行一次更新,这时你录制的视频才能显示出来。

如果你问为什么手机自带的拍摄系统可以立马显示录制的视频或者图片。原因很简单啊,系统给自己有特殊的权限,我们的外部程序是很难拿到这个权限的。

Google对这个官方的解释是:随着手机内存的增加,更新一次媒体库需要耗费大量的时间与资源。因此不建议外部程序去更新媒体库。

但是任何事情是难不倒程序狗的。我们无法通过广播去让系统更新一次媒体库,但是我们可以将我们刚录制的视频插入媒体库呀。

这样我们的设备几乎不用耗费任何时间与资源,就能快速的展示出我们刚刚录制的视频或图片。

同样,这种方法也是Google所提倡的,直接上我的更新指定路径文件进媒体库的静态方法,让其做你的工具类。

/**

*结束录制,并是否添加进媒体库

*@paramisAddMediaLibrarytrue添加false不添加

*/

private voidstopRecord(booleanisAddMediaLibrary) {

if(currentState==RECORDING) {//如果正在录制

releaseRecord();//当结束录制之后,就将当前的资源都释放

stopTimer();

if(isAddMediaLibrary) {

newThread(newRunnable() {

@Override

public voidrun() {

updateGallery(videoFile.getAbsolutePath());

}

}).start();

}

}

}

在录制结束后,我们启动一条线程,让其去更新媒体库,需要将我们刚录制好的文件的路径传入进去/**将刚录制的视频更新到媒体库

*@paramfilename文件路径 文件全名,包括后缀

*/

private voidupdateGallery(String filename) {

MediaScannerConnection.scanFile(RecordVideoActivity.this,

newString[]{filename}, null,

newMediaScannerConnection.OnScanCompletedListener() {

public voidonScanCompleted(String path,Uri uri) {

}

});

}

就这么简单,能够很方便的解决问题。

谨给以后再次集成的时候做一个记录,避免再走弯路。

千里黄云白日熏,北风吹雁雪纷纷。

莫愁前路无知己,天下谁人不识君。

前路漫漫,任重道远,继续加油!

推荐阅读更多精彩内容