功能分析
在实现相册功能之前,我们先需要明确它的逻辑。参照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用来获取数据。
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec);}心有多懒,人就能有多懒。这样它的高度就被强制保持为何宽度一致了。
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;}@Overridepublic void draw(@NonNull Canvas canvas) { IChooseDrawable.this.draw(canvas,paint,state);}@Overridepublic void setAlpha(int alpha) {}@Overridepublic void setColorFilter(ColorFilter colorFilter) {}@Overridepublic 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); }} }}裁剪、单选和多选