热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

Android仿QQ、新浪相册的实现

这篇文章主要为大家详细介绍了Android仿QQ、新浪相册的实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

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

实现效果

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

功能分析

在实现相册功能之前,我们先需要明确它的逻辑。参照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) 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 getAlbums(Context context) {
  ArrayList 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 + "=&#63; or " +
      MediaStore.Images.Media.MIME_TYPE + "=&#63; or " +
      MediaStore.Images.Media.MIME_TYPE + "=&#63;) " +
      "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 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 + "=&#63; or "
      + MediaStore.Images.Media.MIME_TYPE + "=&#63; or "
      + MediaStore.Images.Media.MIME_TYPE + "=&#63;",
    new String[]{"image/jpeg", "image/png", "image/jpg"},
    MediaStore.Images.Media.DATE_MODIFIED + " desc"
      + (ChooserSetting.newestAlbumSize <0 &#63; ""
      : (" 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 + "=&#63; and (" +
       MediaStore.Images.Media.MIME_TYPE + "=&#63; or "
       + MediaStore.Images.Media.MIME_TYPE + "=&#63; or "
       + MediaStore.Images.Media.MIME_TYPE + "=&#63;) ",
     new String[]{folder.getId(), "image/jpeg", "image/png", "image/jpg"},
     MediaStore.Images.Media.DATE_MODIFIED + " desc");
  }
  ArrayList 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 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 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做拍照预览和拍照(也许会添加些许常用滤镜),实现的相关细节也会在后续单独写博客来介绍。

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


推荐阅读
  • 如何高效利用Hackbar插件提升网页调试效率
    通过合理利用Hackbar插件,可以显著提升网页调试的效率。本文介绍了如何获取并使用未包含收费功能的2.1.3版本,以确保在不升级到最新2.2.2版本的情况下,依然能够高效进行网页调试。此外,文章还提供了详细的使用技巧和常见问题解决方案,帮助开发者更好地掌握这一工具。 ... [详细]
  • 深入解析HTTP网络请求API:从基础到进阶的全面指南
    本文全面解析了HTTP网络请求API,从基础到进阶,详细介绍了Android平台上的两种原生API——HttpUrlConnection和HttpClient。这两种API通过对底层Socket的封装,提供了高效、灵活的网络通信功能。文章不仅涵盖了基本的使用方法,还深入探讨了性能优化、错误处理和安全性等方面的高级主题,帮助开发者更好地理解和应用这些工具。 ... [详细]
  • Docker入门指南:初探容器化技术
    Docker入门指南:初探容器化技术摘要:Docker 是一个使用 Go 语言开发的开源容器平台,旨在实现应用程序的构建、分发和运行的标准化。通过将应用及其依赖打包成轻量级的容器,Docker 能够确保应用在任何环境中都能一致地运行,从而提高开发和部署的效率。本文将详细介绍 Docker 的基本概念、核心功能以及如何快速上手使用这一强大的容器化工具。 ... [详细]
  • 本文详细介绍了在 Vue.js 前端框架中集成 vue-i18n 插件以实现多语言支持的方法。通过具体的配置步骤和示例代码,帮助开发者快速掌握如何在项目中实现国际化功能,提升用户体验。同时,文章还探讨了常见的多语言切换问题及解决方案,为开发人员提供了实用的参考。 ... [详细]
  • 在GitHub上克隆vue-element-admin项目时遇到依赖安装错误
    在 GitHub 上克隆 vue-element-admin 项目后,使用 `npm install` 安装依赖时遇到了未知的 Git 错误。具体错误信息为 `npm ERR! code 128`,提示命令执行失败。这可能是由于网络问题、Git 配置不正确或某些依赖包的仓库地址无效导致的。建议检查网络连接、更新 Git 版本并确保所有依赖项的 URL 正确无误。 ... [详细]
  • 基址获取与驱动开发:内核中提取ntoskrnl模块的基地址方法解析
    基址获取与驱动开发:内核中提取ntoskrnl模块的基地址方法解析 ... [详细]
  • 本指南旨在帮助Swoole初学者快速掌握异步并发编程的基本概念和实践方法。通过实例演示,我们将使用Swoole PHP扩展构建一个简单的客户端与服务器模型,并实现基本的通信功能。首先,我们将从客户端的实现入手(文件名为:client.php)。 ... [详细]
  • 在 Visual Studio 中,未选中文本时,使用 `Ctrl+X` 可以剪切并删除当前行,适用于快速删除整行代码;`Ctrl+C` 用于复制当前行的代码;`Ctrl+L` 则用于删除当前行。此外,通过组合键 `Ctrl+K, Ctrl+C` 可以注释选定的代码行,提升代码编辑效率。这些快捷键和技巧能够显著提高开发者的生产力,建议开发者熟练掌握并灵活运用。 ... [详细]
  • 七款高效编辑器与笔记工具推荐:KindEditor自动换行功能解析
    本文推荐了七款高效的编辑器与笔记工具,并详细解析了KindEditor的自动换行功能。其中,轻笔记QingBiJi是一款完全免费的记事本软件,用户可以通过其简洁的界面和强大的功能轻松记录和管理日常事务。此外,该软件还支持多平台同步,确保用户在不同设备间无缝切换。 ... [详细]
  • 在Android 4.4系统中,通过使用 `Intent` 对象并设置动作 `ACTION_GET_CONTENT` 或 `ACTION_OPEN_DOCUMENT`,可以从相册中选择图片并获取其路径。具体实现时,需要为 `Intent` 添加相应的类别,并处理返回的 Uri 以提取图片的文件路径。此方法适用于需要从用户相册中选择图片的应用场景,能够确保兼容性和用户体验。 ... [详细]
  • Jeecg开源社区正式启动第12届架构技术培训班,现已开放报名。本次培训采用师徒制模式,深入探讨Java架构技术。类似于大学导师指导研究生的方式,特别适合在职人员。导师将为学员布置课题,提供丰富的视频资料,并进行一对一指导,帮助学员高效学习和完成任务。我们的教学方法注重实践与理论结合,旨在培养学员的综合技术能力。 ... [详细]
  • 使用for循环构建标准等腰三角形
    通过使用 `for` 循环,可以构建一个标准的等腰三角形。具体来说,每层的星号数量为 `2*i-1`,而空格的数量则为 `(n-i)*2`,其中 `n` 是总层数,`i` 是当前层的索引。通过合理地控制星号和空格的数量,可以确保生成的三角形在视觉上是标准且对称的。例如,对于一个 4 层的等腰三角形,第一层有 1 个星号和 6 个空格,第二层有 3 个星号和 4 个空格,依此类推。这种算法不仅简单高效,而且易于实现。 ... [详细]
  • 在 Android Studio 中创建新项目时 Flutter 未显示的解决方法
    在 Android Studio 中创建新项目时,如果 Flutter 选项未显示,可以通过检查 Flutter 插件安装情况、配置 Flutter SDK 路径以及重启 IDE 等方法来解决。此外,加入 Go 语言社区和 Golang 程序员人脉圈也有助于获取更多技术支持和经验分享。 ... [详细]
  • 当遇到“Warning: Default Activity not found”这一警告时,通常是由于上次强制退出导致 Android Studio 的配置文件损坏所致。解决方法如下:1. 关闭 Android Studio;2. 导航至 Android Studio 的安装目录,找到并删除 `.idea` 和 `build` 文件夹;3. 重新启动 Android Studio 并同步项目。这些步骤有助于恢复项目的正常配置,确保应用能够顺利运行。 ... [详细]
  • 动态壁纸 LiveWallPaper:让您的桌面栩栩如生(第二篇)
    在本文中,我们将继续探讨如何开发动态壁纸 LiveWallPaper,使您的桌面更加生动有趣。作为 2010 年 Google 暑期大学生博客分享大赛 Android 篇的一部分,我们将详细介绍 Ed Burnette 的《Hello, Android》第三版中的相关内容,并分享一些实用的开发技巧和经验。通过本篇文章,您将了解到如何利用 Android SDK 创建引人入胜的动态壁纸,提升用户体验。 ... [详细]
author-avatar
浪费小创_512
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有