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

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

时间: 2024-10-13 08:19:08

Android仿QQ、新浪相册的实现的相关文章

Android仿QQ、新浪相册的实现_Android

在移动应用中,很多时候都会用到图片选择.图片裁剪等功能.最近我也在准备一个开源的相册项目,以方便以后开发应用的时候使用,也尽可能的方便需要的人.一个完整的相册,应该包含相册列表.图片列表.图片的单选和多选.图片的裁剪.拍照.多选图片的大图预览等功能.这也是我这个项目将要包含的功能.在本篇博客中,将会讲述下我在这个项目中相册列表和图片列表的大致实现. 实现效果 结合几个常用的APP中的相册效果,当前项目中已经实现了一些基本的功能和UI,在后续完善的过程中还会有所变动.项目在Github上开源,欢迎

Android实现QQ新用户注册界面遇到问题及解决方法_Android

在上篇文章给大家介绍了Android实现QQ登录界面遇到问题及解决方法,本篇文章继续给大家介绍有关android qq界面知识. 先给大家展示下效果图: 问题: 1.下拉列表(因为还没看到这里...) 2.标题栏显示问题 3.按钮的 Enable 设置 以下是代码: 布局 fragment_main(问题1) <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools

Android仿QQ聊天撒花特效 很真实_Android

先看看效果图吧 实现这样的效果,你要知道贝塞尔曲线,何谓贝塞尔曲线?先在这里打个问号 下面就直接写了 1.activity_main.xml <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent

android仿qq头像点击放大?具体是怎么实现的?望大神指教

问题描述 android仿qq头像点击放大?具体是怎么实现的?望大神指教 android仿qq头像点击放大?具体是怎么实现的?望大神指教 解决方案 可以重新打开一个activity,里面布局放一个大一点的imageview,设置scaletype为充满,将图像传递过去 解决方案二: 他那个点击头像,其实就是跳转至另一个activity,把图片资源也传递了过去,在那个activity放置了一个imageview,然后通过onTouchEvent()触控事件来处理图片的缩放和移动.这个很简单的.

Android仿QQ、微信聊天界面长按提示框效果_Android

先来看看效果图 如何使用 示例代码 PromptViewHelper pvHelper = new PromptViewHelper(mActivity); pvHelper.setPromptViewManager(new ChatPromptViewManager(mActivity)); pvHelper.addPrompt(holder.itemView.findViewById(R.id.textview_content)); 使用起来还是很简单的 首先new一个PromptViewH

Android仿QQ列表左滑删除操作_Android

最近学习了如何做一个像QQ的左滑RecyclerView的item显示选项的,主要是用到Scroller 我们首先新建一个自己的RecyclerView 定义好一些要用的的变量 重写构造方法,把前两个构造方法改为如下,使无论如何构造都要执行第三个构造方法 在第三个构造方法里初始化Scroller public class LeftSwipeMenuRecyclerView extends RecyclerView { //置顶按钮 private TextView tvTop; //删除按钮 p

Android仿QQ空间底部菜单示例代码_Android

之前曾经在网上看到Android仿QQ空间底部菜单的Demo,发现这个Demo有很多Bug,布局用了很多神秘数字.于是研究了一下QQ空间底部菜单的实现,自己写了一个,供大家参考.效果如下图所示:   1.实现原理很简单,底部菜单是一个水平分布的LinearLayout,里面又是五个LinearLayout,它们的layout_weight都为1,意味着底部菜单的子控件将屏幕宽度平均分为5部分.五个LinearLayout除了中间那个,其余都在里面放置ImageView和TextView(中间先空

Android仿QQ附近的人搜索展示功能_Android

 1.概述 老规矩,先上图 原装货(就不录制gif了,大家可以自己在Q群助手开启共享地理位置,返回群聊天页面就看到看到附近的人): 看起来还是挺像的吧. 通过观察,我们可以获取得到如下关系 1.下面展示列表我们可以使用ViewPager来实现(当然如果你不觉得麻烦,你也可以用HorizontalScrollView来试试) 2.上面的扫描图,肯定是个ViewGroup(因为里面的小圆点是可以点击的,如果是View的话,对于这些小圆点的位置的判断,以及对小圆点缩放动画的处理都会超级麻烦,难以实现)

Android 仿QQ头像自定义截取功能_Android

看了Android版QQ的自定义头像功能,决定自己实现,随便熟悉下android绘制和图片处理这一块的知识. 先看看效果: 思路分析: 这个效果可以用两个View来完成,上层View是一个遮盖物,绘制半透明的颜色,中间挖了一个圆:下层的View用来显示图片,具备移动和缩放的功能,并且能截取某区域内的图片. 涉及到的知识点: 1.Matrix,图片的移动和缩放 2.Paint的setXfermode方法 3.图片放大移动后,截取一部分 编码实现: 自定义三个View: 1.下层View:ClipP

Android仿QQ空间底部菜单示例代码

之前曾经在网上看到Android仿QQ空间底部菜单的Demo,发现这个Demo有很多Bug,布局用了很多神秘数字.于是研究了一下QQ空间底部菜单的实现,自己写了一个,供大家参考.效果如下图所示:   1.实现原理很简单,底部菜单是一个水平分布的LinearLayout,里面又是五个LinearLayout,它们的layout_weight都为1,意味着底部菜单的子控件将屏幕宽度平均分为5部分.五个LinearLayout除了中间那个,其余都在里面放置ImageView和TextView(中间先空