配色: 字号:
Android 仿QQ、新浪相册的实现
2016-11-09 | 阅:  转:  |  分享 
  
Android仿QQ、新浪相册的实现

在移动应用中,很多时候都会用到图片选择、图片裁剪等功能。最近我也在准备一个开源的相册项目,以方便以后开发应用的时候使用,也尽可能的方便需要的人。一个完整的相册,应该包含相册列表、图片列表、图片的单选和多选、图片的裁剪、拍照、多选图片的大图预览等功能。这也是我这个项目将要包含的功能。在本篇博客中,将会讲述下我在这个项目中相册列表和图片列表的大致实现。



实现效果



结合几个常用的APP中的相册效果,当前项目中已经实现了一些基本的功能和UI,在后续完善的过程中还会有所变动。项目在Github上开源,欢迎fork和star。先展示实现的效果(后面会增加拍照功能):





功能分析



在实现相册功能之前,我们先需要明确它的逻辑。参照QQ、新浪、微博这中巨头级的APP,当我们需要用选择图片时,会先打开相册,获取到最新的照片列表。然后点击一个按钮可以展开相册列表,点击列表内容,可以切换相册,刷新当前照片列表中的内容。而且选择这篇的时候,会有单选、多选、单选并裁剪等情况,多选的时候还要出现选择效果和指示器等,单选的时候如果需要裁剪则进入裁剪页,不裁剪则默认确定选择,(拍照功能在后续博客中再说明)。

这样,我们就可以明确我们需要实现的功能有:



获取手机中的最新图片

获取手机中的相册列表

获取制定相册中的所有图片

展示图片和相册

多图选择时需要有选择效果和指示器

单选裁剪时需要用到裁剪功能

另外,扫描手机中的图片也是一个相对耗时的工作,所以这个工作还需要主要避免放到主线程中。



准备数据



为了使用方便,我们可以将相册列表的查询、制定相册的查询、最新图片的查询都放到一个工具类中,主要工具类代码如下:



publicclassAlbumTool{



privateHandlerhandler;

//privateSemaphoresemaphore;

privateCallbackcallback;

privateContextcontext;



privatefinalintTYPE_FOLDER=1;

privatefinalintTYPE_ALBUM=2;



publicAlbumTool(Contextcontext){

this.context=context;

handler=newHandler(Looper.getMainLooper()){

@Override

publicvoidhandleMessage(Messagemsg){

if(callback!=null){

switch(msg.what){

caseTYPE_FOLDER:

callback.onFolderFinish((ImageFolder)msg.obj);

break;

caseTYPE_ALBUM:

callback.onAlbumFinish((ArrayList)msg.obj);

break;

}

}

super.handleMessage(msg);

}

};

}



publicvoidsetCallback(Callbackcallback){

this.callback=callback;

}



publicvoidfindAlbumsAsync(){

newThread(newRunnable(){

@Override

publicvoidrun(){

getAlbums(context);

}

}).start();

}



publicvoidfindFolderAsync(finalImageFolderfolder){

newThread(newRunnable(){

@Override

publicvoidrun(){

getFolder(context,folder);

}

}).start();

}



//获取所有图片集

privateArrayListgetAlbums(Contextcontext){

ArrayListalbums=newArrayList<>();

albums.add(getNewestPhotos(context));

//利用ContentResolver查询数据库,找出所有包含图片的文件夹,保存到相册列表中

ContentResolverresolver=context.getContentResolver();

Cursorcursor=resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,

newString[]{

MediaStore.Images.Media.DATA,

MediaStore.Images.ImageColumns.BUCKET_ID,

MediaStore.Images.Media.DATE_MODIFIED,

"count()ascount"

},

MediaStore.Images.Media.MIME_TYPE+"=?or"+

MediaStore.Images.Media.MIME_TYPE+"=?or"+

MediaStore.Images.Media.MIME_TYPE+"=?)"+

"groupby("+MediaStore.Images.ImageColumns.BUCKET_ID,

newString[]{"image/jpeg","image/png","image/jpg"},

MediaStore.Images.Media.DATE_MODIFIED+"desc");

if(cursor!=null){

while(cursor.moveToNext()){

finalFilefile=newFile(cursor.getString(0));

ImageFolderimageFolder=newImageFolder();

imageFolder.setDir(file.getParent());

imageFolder.setId(cursor.getString(1));

imageFolder.setFirstImagePath(cursor.getString(0));

String[]all=file.getParentFile().list(newFilenameFilter(){



privatebooleane(Stringfilename,Stringends){

returnfilename.toLowerCase().endsWith(ends);

}



@Override

publicbooleanaccept(Filedir,Stringfilename){

returne(filename,".png")||e(filename,".jpg")||e(filename,"jpeg");

}

});

if(all!=null&&all.length>0){

imageFolder.setCount(all.length);

albums.add(imageFolder);

}

}

cursor.close();

}

sendMessage(TYPE_ALBUM,albums);

returnalbums;

}



//获取《最新图片》集

privateImageFoldergetNewestPhotos(Contextcontext){

ImageFoldernewestFolder=newImageFolder();

newestFolder.setName(ChooserSetting.newestAlbumName);

ArrayListimageBeans=newArrayList<>();

ContentResolverresolver=context.getContentResolver();

Cursorcursor=resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,

newString[]{

MediaStore.Images.Media.DATA,

MediaStore.Images.Media.DISPLAY_NAME,

MediaStore.Images.Media.DATE_MODIFIED,

},

MediaStore.Images.Media.MIME_TYPE+"=?or"

+MediaStore.Images.Media.MIME_TYPE+"=?or"

+MediaStore.Images.Media.MIME_TYPE+"=?",

newString[]{"image/jpeg","image/png","image/jpg"},

MediaStore.Images.Media.DATE_MODIFIED+"desc"

+(ChooserSetting.newestAlbumSize<0?""

:("limit"+ChooserSetting.newestAlbumSize)));

if(cursor!=null){

while(cursor.moveToNext()){

ImageInfoinfo=newImageInfo();

info.path=cursor.getString(0);

info.displayName=cursor.getString(1);

info.time=cursor.getLong(2);

imageBeans.add(info);

}

cursor.close();

newestFolder.setFirstImagePath(imageBeans.get(0).path);

newestFolder.setDatas(imageBeans);

newestFolder.setCount(imageBeans.size());

}

sendMessage(TYPE_FOLDER,newestFolder);

returnnewestFolder;

}



//获取具体图片集,确保图片数据已被查询

privateImageFoldergetFolder(Contextcontext,ImageFolderfolder){

ContentResolverresolver=context.getContentResolver();

Cursorcursor;

if(folder!=null&&folder.getDatas()!=null&&folder.getDatas().size()>0){

sendMessage(TYPE_FOLDER,folder);

returnfolder;

}

if(folder==null){

returngetNewestPhotos(context);

}else{

cursor=resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,

newString[]{

MediaStore.Images.Media.DATA,

MediaStore.Images.Media.DISPLAY_NAME,

MediaStore.Images.Media.DATE_MODIFIED

},

MediaStore.Images.ImageColumns.BUCKET_ID+"=?and("+

MediaStore.Images.Media.MIME_TYPE+"=?or"

+MediaStore.Images.Media.MIME_TYPE+"=?or"

+MediaStore.Images.Media.MIME_TYPE+"=?)",

newString[]{folder.getId(),"image/jpeg","image/png","image/jpg"},

MediaStore.Images.Media.DATE_MODIFIED+"desc");

}

ArrayListdatas=newArrayList<>();

folder.setDatas(dwww.shanxiwang.netatas);

if(cursor!=null){

while(cursor.moveToNext()){

ImageInfoinfo=newImageInfo();

info.path=cursor.getString(0);

info.displayName=cursor.getString(1);

info.time=cursor.getLong(2);

datas.add(info);

}

cursor.close();

}

sendMessage(TYPE_FOLDER,folder);

returnfolder;

}



privatevoidsendMessage(intwhat,Objectobj){

Messagemsg=newMessage();

msg.what=what;

msg.obj=obj;

handler.sendMessage(msg);

}



publicinterfaceCallback{



//文件夹查找完毕

voidonFolderFinish(ImageFolderfolder);

//成功搜索出所有的图片集

voidonAlbumFinish(ArrayListalbums);



}



}

这样,我们就可以利用这个工具类方便的获取相册列表、获取制定相册的图片了(最新照片合集当做是一个相册)。里面主要就是使用ContentResolver来做查询,Android入门级问题,四大组件——Activity、Service、ContentProvider和BroadcastReceiver,中的ContentProvider和ContentResolver就是一对CP了,ContentProvider用来提供数据,ContentResolver用来获取数据。



展示相册和相册列表



有了获取相册列表和获取指定相册的方法,展示相册和相册列表就容易了,按照通常的方式,我们直接使用GridView来展示相册,用ListView来展示相册列表。当然,你也可以选择使用RecyclerView来替代掉GridView和ListView,其实也都一样。

显示图片直接使用成熟的第三方框架即可,我使用的是Glide。

值得注意的是,在相册中,我们展示出来的图片都是正方块、并且需要三个(你也可以设置四个或者五个,只要你高兴)铺满宽度。在这里我使用的是比较懒的方式,直接用一个自定义的布局作为Item的跟布局,这个自定义布局继承RelativeLayout,然后将复写它的onMeasure方法:



@Override

protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){

super.onMeasure(widthMeasureSpec,widthMeasureSpec);

}



心有多懒,人就能有多懒。这样它的高度就被强制保持为何宽度一致了。



选择指示器



像QQ中,选择图片时,图片会根据选择的顺序,在图片上的那个圈圈里面显示出1234……等数字,然后取消选择时,被选的数字会顺序补位,比如你选了七张图片、然后取消了显示数字3的那张,这时4就变成3了、5变成了4、6变成了5。

像新浪微博中的图片选择,不会出现数字,而是出现一个勾,选中的时候这个勾还有动画效果。

这样的功能怎么实现呢?

我实现的方式是,在每个Item中都有一个固定大小的View,根据图片是否被选中,加载不同的Drawable。当然,写这个项目既然是为了以后在不同的项目中使用,这个自然要方便被使用者自行设置。所以我写一个抽象类:



publicabstractclassIChooseDrawable{



privatePaintpaint;

protectedintwidth=0;

protectedintheight=0;



privateSparseArraydrawables;



publicIChooseDrawable(){

paint=newPaint();

paint.setAntiAlias(true);

paint.setColor(0x88000000);

drawables=newSparseArray<>();

}



publicDrawableget(intstate){

if(drawables.indexOfKey(state)>=0){

returndrawables.get(state);

}else{

InDrawabledrawable=newInDrawable(state);

drawables.put(state,drawable);

returndrawable;

}

}



publicvoidclear(){

drawables.clear();

}



publicintgetBaseline(Paintpaint,inttop,intbottom){

Paint.FontMetricsi=paint.getFontMetrics();

return(int)((bottom+top-i.top-i.bottom)/2);

}



//state表示第几个被选择,0表示未选中

publicabstractvoiddraw(Canvascanvas,Paintpaint,intstate);



privateclassInDrawableextendsDrawable{



privateintstate=0;



InDrawable(intstate){

this.state=state;

}



@Override

publicvoiddraw(@NonNullCanvascanvas){

IChooseDrawable.this.draw(canvas,paint,state);

}



@Override

publicvoidsetAlpha(intalpha){



}



@Override

publicvoidsetColorFilter(ColorFiltercolorFilter){



}



@Override

publicintgetOpacity(){

returnPixelFormat.TRANSPARENT;

}

}

}



在相册的Adapter的构造函数中会传入一个IChooseDrawable实体,在显示每个Item时,会根据当前状态通过drawable.get(intstate)取得指定的Drawable,设置为指示器View的背景。

上面效果图中的指示器(也可配置为只显示对号)实现为:



publicclassCircleChooseDrawableextendsIChooseDrawable{



privatebooleanisShowNum=true;

privateintchooseBgColor=0xFFFF6600;

privatePathpath;



publicCircleChooseDrawable(){

super();

}



publicCircleChooseDrawable(booleanisShowNum,intchooseBgColor){

super();

this.isShowNum=isShowNum;

this.chooseBgColor=chooseBgColor;

}



@Override

publicvoiddraw(Canvascanvas,Paintpaint,intstate){

width=canvas.getWidth();

height=canvas.getHeight();

if(state==0){//未选择状态

paint.setColor(0x55000000);

paint.setStyle(Paint.Style.FILL);

canvas.drawCircle(width/2,height/2,width/2-2,paint);

paint.setColor(0xDDFFFFFF);

paint.setStrokeWidth(2);

paint.setStyle(Paint.Style.STROKE);

canvas.drawCircle(width/2,height/2,width/2-2,paint);

}else{//选中状态

paint.setColor(chooseBgColor);

paint.setStyle(Paint.Style.FILL);

canvas.drawCircle(width/2,height/2,width/2-2,paint);

paint.setColor(0xDDFFFFFF);

paint.setStrokeWidth(2);

paint.setStyle(Paint.Style.STROKE);

canvas.drawCircle(width/2,height/2,width/2-2,paint);

paint.setColor(0xDDFFFFFF);

if(isShowNum){//显示数字

paint.setStyle(Paint.Style.FILL);

paint.setTextAlign(Paint.Align.CENTER);

paint.setTextSize(width0.53f);

canvas.drawText(state+"",width/2,getBaseline(paint,0,height),paint);

}else{//显示一个√号

paint.setStyle(Paint.Style.STROKE);

paint.setStrokeWidth(3);

paint.setStrokeCap(Paint.Cap.ROUND);

if(path==null){

path=newPath();

path.moveTo(width/4f,height/2f);

path.lineTo(width2/5f,height5/7f);

path.lineTo(width3/4f,height/3f);

}

canvas.drawPath(path,paint);

}

}

}

}



裁剪、单选和多选



单选和多选的区别在于单选的时候,没有选择指示器,选中直接携带数据返回。而多选时,有选择指示器,选择完成后,需要确定后携带数据返回,在确定前可以取消之前所选的内容。

所以实现的时候,只需要判断用户传入的选择意图,做出相应的处理。如果是裁剪,则选择一张图片后,进入到裁剪页面,裁剪结束后携带裁剪结果返回到进入到相册前的页面。如果是单选,则选择一张图片后,直接携带数据返回到进入相册前的页面。如果是多选,则要在点击确认按钮后,携带数据返回到进入相册前的页面。裁剪的实现见上一篇博客——Android图片裁剪。



其他



其他的一些功能,主要是拍照的功能、和大图切换预览现在还未添加进项目中,目前准备是利用OpenGl做拍照预览和拍照(也许会添加些许常用滤镜),实现的相关细节也会在后续单独写博客来介绍。

献花(0)
+1
(本文系网络学习天...首藏)