Android 仿QQ新浪相册的实现

一卷旌收千骑虏,万全身出百重围。这篇文章主要讲述Android 仿QQ新浪相册的实现相关的知识,希望能为你提供帮助。
在移动应用中, 很多时候都会用到图片选择、图片裁剪等功能。最近我也在准备一个开源的相册项目, 以方便以后开发应用的时候使用, 也尽可能的方便需要的人。一个完整的相册, 应该包含相册列表、图片列表、图片的单选和多选、图片的裁剪、拍照、多选图片的大图预览等功能。这也是我这个项目将要包含的功能。在本篇博客中, 将会讲述下我在这个项目中相册列表和图片列表的大致实现。
实现效果 结合几个常用的APP中的相册效果, 当前项目中已经实现了一些基本的功能和UI, 在后续完善的过程中还会有所变动。项目在Github上开源, 欢迎fork和star。先展示实现的效果( 后面会增加拍照功能) :

Android 仿QQ新浪相册的实现

文章图片
Android 仿QQ新浪相册的实现

文章图片
Android 仿QQ新浪相册的实现

文章图片

Android 仿QQ新浪相册的实现

文章图片
Android 仿QQ新浪相册的实现

文章图片

功能分析 在实现相册功能之前, 我们先需要明确它的逻辑。参照QQ、新浪、微博这中巨头级的APP, 当我们需要用选择图片时, 会先打开相册, 获取到最新的照片列表。然后点击一个按钮可以展开相册列表, 点击列表内容, 可以切换相册, 刷新当前照片列表中的内容。而且选择这篇的时候, 会有单选、多选、单选并裁剪等情况, 多选的时候还要出现选择效果和指示器等, 单选的时候如果需要裁剪则进入裁剪页, 不裁剪则默认确定选择, ( 拍照功能在后续博客中再说明) 。
这样, 我们就可以明确我们需要实现的功能有:
  1. 获取手机中的最新图片
  2. 获取手机中的相册列表
  3. 获取制定相册中的所有图片
  4. 展示图片和相册
  5. 多图选择时需要有选择效果和指示器
  6. 单选裁剪时需要用到裁剪功能
另外, 扫描手机中的图片也是一个相对耗时的工作, 所以这个工作还需要主要避免放到主线程中。
准备数据 为了使用方便, 我们可以将相册列表的查询、制定相册的查询、最新图片的查询都放到一个工具类中, 主要工具类代码如下:
public class AlbumTool {private Handler handler; //private Semaphore semaphore; private Callback callback; private Context context; private final int TYPE_FOLDER= 1; private final int TYPE_ALBUM= 2; public AlbumTool(Context context){ this.context= context; handler= new Handler(Looper.getMainLooper()){ @ Override public void handleMessage(Message msg) { if(callback!= null){ switch (msg.what){ case TYPE_FOLDER: callback.onFolderFinish((ImageFolder) msg.obj); break; case TYPE_ALBUM: callback.onAlbumFinish((ArrayList< ImageFolder> ) msg.obj); break; } } super.handleMessage(msg); } }; }public void setCallback(Callback callback){ this.callback= callback; }public void findAlbumsAsync(){ new Thread(new Runnable() { @ Override public void run() { getAlbums(context); } }).start(); }public void findFolderAsync(final ImageFolder folder){ new Thread(new Runnable() { @ Override public void run() { getFolder(context,folder); } }).start(); }//获取所有图片集 private ArrayList< ImageFolder> getAlbums(Context context) { ArrayList< ImageFolder> albums= new ArrayList< > (); albums.add(getNewestPhotos(context)); //利用ContentResolver查询数据库, 找出所有包含图片的文件夹, 保存到相册列表中 ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{ MediaStore.Images.Media.DATA, MediaStore.Images.ImageColumns.BUCKET_ID, MediaStore.Images.Media.DATE_MODIFIED, " count(*) as count" }, MediaStore.Images.Media.MIME_TYPE + " = ? or " + MediaStore.Images.Media.MIME_TYPE + " = ? or " + MediaStore.Images.Media.MIME_TYPE + " = ?) " + " group by (" + MediaStore.Images.ImageColumns.BUCKET_ID, new String[]{" image/jpeg" , " image/png" , " image/jpg" }, MediaStore.Images.Media.DATE_MODIFIED + " desc" ); if (cursor != null) { while (cursor.moveToNext()) { final File file = new File(cursor.getString(0)); ImageFolder imageFolder = new ImageFolder(); imageFolder.setDir(file.getParent()); imageFolder.setId(cursor.getString(1)); imageFolder.setFirstImagePath(cursor.getString(0)); String[] all= file.getParentFile().list(new FilenameFilter() {private boolean e(String filename,String ends){ return filename.toLowerCase().endsWith(ends); }@ Override public boolean accept(File dir, String filename) { return e(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); return albums; }//获取《最新图片》集 private ImageFolder getNewestPhotos(Context context) { ImageFolder newestFolder= new ImageFolder(); newestFolder.setName(ChooserSetting.newestAlbumName); ArrayList< ImageInfo> imageBeans = new ArrayList< > (); ContentResolver resolver = context.getContentResolver(); Cursor cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{ 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 + " = ?" , new String[]{" image/jpeg" , " image/png" , " image/jpg" }, MediaStore.Images.Media.DATE_MODIFIED + " desc" + (ChooserSetting.newestAlbumSize < 0 ? " " : (" limit " + ChooserSetting.newestAlbumSize))); if (cursor != null){ while (cursor.moveToNext()) { ImageInfo info= new ImageInfo(); 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); return newestFolder; }//获取具体图片集, 确保图片数据已被查询 private ImageFolder getFolder(Context context,ImageFolder folder) { ContentResolver resolver = context.getContentResolver(); Cursor cursor; if(folder!= null& & folder.getDatas()!= null& & folder.getDatas().size()> 0){ sendMessage(TYPE_FOLDER,folder); return folder; } if (folder = = null) { return getNewestPhotos(context); } else { cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{ 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 + " = ?) " , new String[]{folder.getId(), " image/jpeg" , " image/png" , " image/jpg" }, MediaStore.Images.Media.DATE_MODIFIED + " desc" ); } ArrayList< ImageInfo> datas= new ArrayList< > (); folder.setDatas(datas); if (cursor != null){ while (cursor.moveToNext()) { ImageInfo info= new ImageInfo(); info.path= cursor.getString(0); info.displayName= cursor.getString(1); info.time= cursor.getLong(2); datas.add(info); } cursor.close(); } sendMessage(TYPE_FOLDER,folder); return folder; }private void sendMessage(int what,Object obj){ Message msg= new Message(); msg.what= what; msg.obj= obj; handler.sendMessage(msg); }public interface Callback{//文件夹查找完毕 void onFolderFinish(ImageFolder folder); //成功搜索出所有的图片集 void onAlbumFinish(ArrayList< ImageFolder> albums); }}

这样, 我们就可以利用这个工具类方便的获取相册列表、获取制定相册的图片了( 最新照片合集当做是一个相册) 。里面主要就是使用ContentResolver来做查询, android入门级问题, 四大组件——Activity、Service、ContentProvider和BroadcastReceiver, 中的ContentProvider和ContentResolver就是一对CP了, ContentProvider用来提供数据, ContentResolver用来获取数据。
展示相册和相册列表 有了获取相册列表和获取指定相册的方法, 展示相册和相册列表就容易了, 按照通常的方式, 我们直接使用GridView来展示相册, 用ListView来展示相册列表。当然, 你也可以选择使用RecyclerView来替代掉GridView和ListView, 其实也都一样。
显示图片直接使用成熟的第三方框架即可, 我使用的是Glide。
值得注意的是, 在相册中, 我们展示出来的图片都是正方块、并且需要三个( 你也可以设置四个或者五个, 只要你高兴) 铺满宽度。在这里我使用的是比较懒的方式, 直接用一个自定义的布局作为Item的跟布局, 这个自定义布局继承RelativeLayout, 然后将复写它的onMeasure方法:
@ Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); }

心有多懒, 人就能有多懒。这样它的高度就被强制保持为何宽度一致了。
选择指示器 像QQ中, 选择图片时, 图片会根据选择的顺序, 在图片上的那个圈圈里面显示出1234……等数字, 然后取消选择时, 被选的数字会顺序补位, 比如你选了七张图片、然后取消了显示数字3的那张, 这时4就变成3了、5变成了4、6变成了5。
像新浪微博中的图片选择, 不会出现数字, 而是出现一个勾, 选中的时候这个勾还有动画效果。
这样的功能怎么实现呢?
我实现的方式是, 在每个Item中都有一个固定大小的View, 根据图片是否被选中, 加载不同的Drawable。当然, 写这个项目既然是为了以后在不同的项目中使用, 这个自然要方便被使用者自行设置。所以我写一个抽象类:
public abstract class IChooseDrawable{private Paint paint; protected int width= 0; protected int height= 0; private SparseArray< Drawable> drawables; public IChooseDrawable(){ paint= new Paint(); paint.setAntiAlias(true); paint.setColor(0x88000000); drawables= new SparseArray< > (); }public Drawable get(int state){ if(drawables.indexOfKey(state)> = 0){ return drawables.get(state); }else{ InDrawable drawable= new InDrawable(state); drawables.put(state,drawable); return drawable; } }public void clear(){ drawables.clear(); }public int getBaseline(Paint paint,int top,int bottom){ Paint.FontMetrics i= paint.getFontMetrics(); return (int) ((bottom+ top-i.top-i.bottom)/2); }//state表示第几个被选择, 0表示未选中 public abstract void draw(Canvas canvas,Paint paint,int state); private class InDrawable extends Drawable{private int state= 0; InDrawable(int state){ this.state= state; }@ Override public void draw(@ NonNull Canvas canvas) { IChooseDrawable.this.draw(canvas,paint,state); }@ Override public void setAlpha(int alpha) {}@ Override public void setColorFilter(ColorFilter colorFilter) {}@ Override public int getOpacity() { return PixelFormat.TRANSPARENT; } } }

在相册的Adapter的构造函数中会传入一个IChooseDrawable实体, 在显示每个Item时, 会根据当前状态通过drawable.get(int state)取得指定的Drawable, 设置为指示器View的背景。
上面效果图中的指示器( 也可配置为只显示对号) 实现为:
public class CircleChooseDrawable extends IChooseDrawable {private boolean isShowNum= true; private int chooseBgColor= 0xFFFF6600; private Path path; public CircleChooseDrawable(){ super(); }public CircleChooseDrawable(boolean isShowNum,int chooseBgColor){ super(); this.isShowNum= isShowNum; this.chooseBgColor= chooseBgColor; }@ Override public void draw(Canvas canvas, Paint paint, int state) { 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(width*0.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= new Path(); path.moveTo(width/4f,height/2f); path.lineTo(width*2/5f,height*5/7f); path.lineTo(width*3/4f,height/3f); } canvas.drawPath(path,paint); } } } }

裁剪、单选和多选 单选和多选的区别在于单选的时候, 没有选择指示器, 选中直接携带数据返回。而多选时, 有选择指示器, 选择完成后, 需要确定后携带数据返回, 在确定前可以取消之前所选的内容。
所以实现的时候, 只需要判断用户传入的选择意图, 做出相应的处理。如果是裁剪, 则选择一张图片后, 进入到裁剪页面, 裁剪结束后携带裁剪结果返回到进入到相册前的页面。如果是单选, 则选择一张图片后, 直接携带数据返回到进入相册前的页面。如果是多选, 则要在点击确认按钮后, 携带数据返回到进入相册前的页面。裁剪的实现见上一篇博客——Android 图片裁剪。
其他 其他的一些功能, 主要是拍照的功能、和大图切换预览现在还未添加进项目中, 目前准备是利用OpenGl做拍照预览和拍照( 也许会添加些许常用滤镜) 。目前已加入调用系统相机拍照功能( 与微信相同) , 自定义拍照( 新浪) 将在后续增加。实现的相关细节也会在后续单独写博客来介绍。
【Android 仿QQ新浪相册的实现】欢迎转载, 转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/53091606]

    推荐阅读