欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android仿QQ、新浪相册的实现

程序员文章站 2024-03-02 22:16:46
在移动应用中,很多时候都会用到图片选择、图片裁剪等功能。最近我也在准备一个开源的相册项目,以方便以后开发应用的时候使用,也尽可能的方便需要的人。一个完整的相册,应该包含相册...

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

实现效果

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

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做拍照预览和拍照(也许会添加些许常用滤镜),实现的相关细节也会在后续单独写博客来介绍。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。