一卷旌收千骑虏,万全身出百重围。这篇文章主要讲述Android 仿QQ新浪相册的实现相关的知识,希望能为你提供帮助。
在移动应用中,
很多时候都会用到图片选择、图片裁剪等功能。最近我也在准备一个开源的相册项目,
以方便以后开发应用的时候使用,
也尽可能的方便需要的人。一个完整的相册,
应该包含相册列表、图片列表、图片的单选和多选、图片的裁剪、拍照、多选图片的大图预览等功能。这也是我这个项目将要包含的功能。在本篇博客中,
将会讲述下我在这个项目中相册列表和图片列表的大致实现。
实现效果 结合几个常用的APP中的相册效果,
当前项目中已经实现了一些基本的功能和UI,
在后续完善的过程中还会有所变动。项目在Github上开源,
欢迎fork和star。先展示实现的效果(
后面会增加拍照功能)
:
文章图片
文章图片
文章图片
文章图片
文章图片
功能分析 在实现相册功能之前, 我们先需要明确它的逻辑。参照QQ、新浪、微博这中巨头级的APP, 当我们需要用选择图片时, 会先打开相册, 获取到最新的照片列表。然后点击一个按钮可以展开相册列表, 点击列表内容, 可以切换相册, 刷新当前照片列表中的内容。而且选择这篇的时候, 会有单选、多选、单选并裁剪等情况, 多选的时候还要出现选择效果和指示器等, 单选的时候如果需要裁剪则进入裁剪页, 不裁剪则默认确定选择, ( 拍照功能在后续博客中再说明) 。
这样, 我们就可以明确我们需要实现的功能有:
- 获取手机中的最新图片
- 获取手机中的相册列表
- 获取制定相册中的所有图片
- 展示图片和相册
- 多图选择时需要有选择效果和指示器
- 单选裁剪时需要用到裁剪功能
准备数据 为了使用方便, 我们可以将相册列表的查询、制定相册的查询、最新图片的查询都放到一个工具类中, 主要工具类代码如下:
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 图片裁剪。
其他
【Android 仿QQ新浪相册的实现】欢迎转载, 转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/53091606]
推荐阅读
- Android中解析读取复杂word,excel,ppt等的方法
- AndroidMVP模式
- android调用系统拍照那些事
- 使用Fiddler分析Android版API
- 编码标准和准则介绍和详细指南
- HTML DOM标题属性用法及其示例
- SASS @import用法介绍及其示例
- Java中的StringBuilder类用法及其示例
- PHP MySQL数据库开发介绍和指南