Android仿微信朋友圈图片查看器
再看文章之前,希望大家先打开自己的微信点到朋友圈中去,仔细观察是不是发现朋友圈里的有个“九宫格”的图片区域,点击图片又会跳到图片的详细查看页面,并且支持图片的滑动和缩放?这个功能是不是很常用呢?!那么我今天正好做了这个demo,下面为大家讲解一下。首先按照惯例先看一下效果图吧,尤其不会录制gif动画(哎~没办法,模拟器不支持多点触控,刚好我的手机又没有root,不能录屏,悲催啊,大家见谅,想要看真实效果的话,烦请移到文章最下方转载文章中进行源码下载,点击下载源码,运行后再看效果哈~~),这里先就拿几张静态的图片顶替一下好了。见谅!
效果嘛,将就着看吧!实在看不明白就想想微信朋友圈,或者拖到下方,点击下载源码!这里,首先分析一下主界面吧,布局都是很简单的,主界面仅仅就是一个listview的控件,listview的item上值得注意的是,item上包含了一个gridview,这个gridview呗用作实现“九宫格”的效果,主界面布局就是一个listview,这里不说了,我们先来看看listview的item的布局吧,以下是item_list.xml
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingbottom="5dp" android:paddingtop="5dp" > <imageview android:id="@+id/iv_avatar" android:layout_width="50dp" android:layout_height="50dp" android:background="@drawable/ic_launcher" android:scaletype="centercrop" /> <textview android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginleft="5dp" android:layout_torightof="@id/iv_avatar" android:text="爷,今天心情好!" android:textsize="16sp" /> <textview android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/tv_title" android:layout_marginleft="5dp" android:layout_margintop="3dp" android:layout_torightof="@id/iv_avatar" android:text="今天又是雾霾!" android:textsize="16sp" /> <com.example.imagedemo.noscrollgridview android:id="@+id/gridview" android:layout_width="220dp" android:layout_height="wrap_content" android:layout_below="@id/tv_content" android:layout_marginleft="5dp" android:layout_margintop="3dp" android:layout_torightof="@id/iv_avatar" android:columnwidth="70dp" android:gravity="center" android:horizontalspacing="2.5dp" android:numcolumns="3" android:stretchmode="columnwidth" android:verticalspacing="2.5dp" /> </relativelayout>
好了,大家看到了,布局也是极其简单的,但是有个问题就是listview嵌套进了gridview,那么就会出现一个问题,导致gridview显示的不全,那么该怎么解决这个问题呢?其实也简单,就是重写一个gridview,测量一下gridview的高度,再设置上去。具体解决方案请看上篇博文listview嵌套gridview显示不全解决方法或者源码,如下noscrollgridview.java
package com.example.imagedemo; import android.content.context; import android.util.attributeset; import android.widget.gridview; /** * 自定义的“九宫格”——用在显示帖子详情的图片集合 解决的问题:gridview显示不全,只显示了一行的图片,比较奇怪,尝试重写gridview来解决 * * @author lichao * @since 2014-10-16 16:41 * */ public class noscrollgridview extends gridview { public noscrollgridview(context context) { super(context); // todo auto-generated constructor stub } public noscrollgridview(context context, attributeset attrs) { super(context, attrs); // todo auto-generated constructor stub } public noscrollgridview(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); // todo auto-generated constructor stub } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { // todo auto-generated method stub int expandspec = measurespec.makemeasurespec(integer.max_value >> 2, measurespec.at_most); super.onmeasure(widthmeasurespec, expandspec); } }
接下来看看listview上面item的实体是什么样的数据结构,这就显得非常简单了。
public class itementity { private string avatar; // 用户头像url private string title; // 标题 private string content; // 内容 private arraylist<string> imageurls; // 九宫格图片的url集合 public itementity(string avatar, string title, string content, arraylist<string> imageurls) { super(); this.avatar = avatar; this.title = title; this.content = content; this.imageurls = imageurls; } ... }
好了,有了listview,那么不可避免的就是做item上的数据适配了。继承一个baseadapter,代码如下,都比较简单:
/** * 首页listview的数据适配器 * * @author administrator * */ public class listitemadapter extends baseadapter { private context mcontext; private arraylist<itementity> items; public listitemadapter(context ctx, arraylist<itementity> items) { this.mcontext = ctx; this.items = items; } @override public int getcount() { return items == null ? 0 : items.size(); } @override public object getitem(int position) { return items.get(position); } @override public long getitemid(int position) { return position; } @override public view getview(int position, view convertview, viewgroup parent) { viewholder holder; if (convertview == null) { holder = new viewholder(); convertview = view.inflate(mcontext, r.layout.item_list, null); holder.iv_avatar = (imageview) convertview .findviewbyid(r.id.iv_avatar); holder.tv_title = (textview) convertview .findviewbyid(r.id.tv_title); holder.tv_content = (textview) convertview .findviewbyid(r.id.tv_content); holder.gridview = (noscrollgridview) convertview .findviewbyid(r.id.gridview); convertview.settag(holder); } else { holder = (viewholder) convertview.gettag(); } itementity itementity = items.get(position); holder.tv_title.settext(itementity.gettitle()); holder.tv_content.settext(itementity.getcontent()); // 使用imageloader加载网络图片 displayimageoptions options = new displayimageoptions.builder()// .showimageonloading(r.drawable.ic_launcher) // 加载中显示的默认图片 .showimageonfail(r.drawable.ic_launcher) // 设置加载失败的默认图片 .cacheinmemory(true) // 内存缓存 .cacheondisk(true) // sdcard缓存 .bitmapconfig(config.rgb_565)// 设置最低配置 .build();// imageloader.getinstance().displayimage(itementity.getavatar(), holder.iv_avatar, options); final arraylist<string> imageurls = itementity.getimageurls(); if (imageurls == null || imageurls.size() == 0) { // 没有图片资源就隐藏gridview holder.gridview.setvisibility(view.gone); } else { holder.gridview.setadapter(new noscrollgridadapter(mcontext, imageurls)); } // 点击回帖九宫格,查看大图 holder.gridview.setonitemclicklistener(new onitemclicklistener() { @override public void onitemclick(adapterview<?> parent, view view, int position, long id) { // todo auto-generated method stub imagebrower(position, imageurls); } }); return convertview; } /** * 打开图片查看器 * * @param position * @param urls2 */ protected void imagebrower(int position, arraylist<string> urls2) { intent intent = new intent(mcontext, imagepageractivity.class); // 图片url,为了演示这里使用常量,一般从数据库中或网络中获取 intent.putextra(imagepageractivity.extra_image_urls, urls2); intent.putextra(imagepageractivity.extra_image_index, position); mcontext.startactivity(intent); } /** * listview组件复用,防止“卡顿” * * @author administrator * */ class viewholder { private imageview iv_avatar; private textview tv_title; private textview tv_content; private noscrollgridview gridview; } }
这里有需要解释的地方了,看看listview上的图片处理,由于图片都是从网络获取的,为了避免图片过多造成oom,那么这里加载图片的时候必不可少的需要做内存优化,图片的优化方式有很多,我这里采取了最简单最直接得方式,使用了开源的imageloader这个图片加载框架,这个框架简直是太优秀了,减少了开发者一系列不必要而且时常会出现的麻烦,关于imageloader并不是本篇博文需要讲解的知识,关于imageloader,欢迎在github主页上下载,地址是https://github.com/nostra13/android-universal-image-loader,既然使用了imageloader这个框架,就不得不在程序上做一些初始化的操作,首先需要自定义一个全局的上下文application类,将imageloader的相关属性初始化上去,直接看代码好了,见名知意:myapplication.java
public class myapplication extends application { @override public void oncreate() { super.oncreate(); displayimageoptions defaultoptions = new displayimageoptions.builder() // .showimageforemptyuri(r.drawable.ic_launcher) // .showimageonfail(r.drawable.ic_launcher) // .cacheinmemory(true) // .cacheondisk(true) // .build();// imageloaderconfiguration config = new imageloaderconfiguration// .builder(getapplicationcontext())// .defaultdisplayimageoptions(defaultoptions)// .disccachesize(50 * 1024 * 1024)// .disccachefilecount(100)// 缓存一百张图片 .writedebuglogs()// .build();// imageloader.getinstance().init(config); } }
定义这个application之后,需要在清单文件中配置一下,在manifest.xml中的application节点上添加:
android:name="com.example.imagedemo.myapplication"
此外由于imageloader是网络获取图片,又需要本地sdcard缓存图片,所以需要加上一下的权限,这是imageloader标准权限:
<uses-permission android:name="android.permission.internet" /> <uses-permission android:name="android.permission.write_external_storage" /> <uses-permission android:name="android.permission.access_network_state" />
再看看上面的item上数据,里面有个gridview,显然这个gridview也是需要做数据适配的,这个数据反应的是从网络加载图片,比较简单,看代码noscrollgridadapter.java
...... override public view getview(int position, view convertview, viewgroup parent) { view view = view.inflate(ctx, r.layout.item_gridview, null); imageview imageview = (imageview) view.findviewbyid(r.id.iv_image); displayimageoptions options = new displayimageoptions.builder()// .cacheinmemory(true)// .cacheondisk(true)// .bitmapconfig(config.rgb_565)// .build(); imageloader.getinstance().displayimage(imageurls.get(position), imageview, options); return view; } ......
这样,所有的数据适配就做好了,接下来就需要做图片查看器了,当我们点击listview上item里的“九宫格”——noscrollgridview的某张图片的时候,需要把这个图片的url传给一个图片查看器,图片查看器里会根据传递进来的url去网络加载这张图片,那么其实图片查看器就是一个新的单独的activity,这个activity会包含一个viewpager,用来管理多张图片的查看。image_detail_pager.xml
<?xml version="1.0" encoding="utf-8"?> <framelayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.imagedemo.hackyviewpager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/black" /> <textview android:id="@+id/indicator" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="@android:color/transparent" android:gravity="center" android:text="@string/viewpager_indicator" android:textcolor="@android:color/white" android:textsize="18sp" /> </framelayout>
hackyviewpager.java
public class hackyviewpager extends viewpager { private static final string tag = "hackyviewpager"; public hackyviewpager(context context) { super(context); } public hackyviewpager(context context, attributeset attrs) { super(context, attrs); } @override public boolean onintercepttouchevent(motionevent ev) { try { return super.onintercepttouchevent(ev); } catch (illegalargumentexception e) { // 不理会 log.e(tag, "hacky viewpager error1"); return false; } catch (arrayindexoutofboundsexception e) { // 不理会 log.e(tag, "hacky viewpager error2"); return false; } } }
imagepageractivity.java
/** * 图片查看器 */ public class imagepageractivity extends fragmentactivity { private static final string state_position = "state_position"; public static final string extra_image_index = "image_index"; public static final string extra_image_urls = "image_urls"; private hackyviewpager mpager; private int pagerposition; private textview indicator; @override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.image_detail_pager); pagerposition = getintent().getintextra(extra_image_index, 0); arraylist<string> urls = getintent().getstringarraylistextra( extra_image_urls); mpager = (hackyviewpager) findviewbyid(r.id.pager); imagepageradapter madapter = new imagepageradapter( getsupportfragmentmanager(), urls); mpager.setadapter(madapter); indicator = (textview) findviewbyid(r.id.indicator); charsequence text = getstring(r.string.viewpager_indicator, 1, mpager .getadapter().getcount()); indicator.settext(text); // 更新下标 mpager.setonpagechangelistener(new onpagechangelistener() { @override public void onpagescrollstatechanged(int arg0) { } @override public void onpagescrolled(int arg0, float arg1, int arg2) { } @override public void onpageselected(int arg0) { charsequence text = getstring(r.string.viewpager_indicator, arg0 + 1, mpager.getadapter().getcount()); indicator.settext(text); } }); if (savedinstancestate != null) { pagerposition = savedinstancestate.getint(state_position); } mpager.setcurrentitem(pagerposition); } @override public void onsaveinstancestate(bundle outstate) { outstate.putint(state_position, mpager.getcurrentitem()); } private class imagepageradapter extends fragmentstatepageradapter { public arraylist<string> filelist; public imagepageradapter(fragmentmanager fm, arraylist<string> filelist) { super(fm); this.filelist = filelist; } @override public int getcount() { return filelist == null ? 0 : filelist.size(); } @override public fragment getitem(int position) { string url = filelist.get(position); return imagedetailfragment.newinstance(url); } } }
已知图片查看的界面是继承自fragmentactivity的,所以支持显示的界面必须需要fragment来实现,那么就自定义个frangment吧,用这个fragment来从url中获取图片资源,显示图片。image_detail_fragment.xml
<?xml version="1.0" encoding="utf-8"?> <framelayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/black" > <imageview android:id="@+id/image" android:layout_width="match_parent" android:layout_height="match_parent" android:adjustviewbounds="true" android:contentdescription="@string/app_name" android:scaletype="centercrop" /> <progressbar android:id="@+id/loading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone" /> </framelayout>
imagedetailfragment.java
/** * 单张图片显示fragment */ public class imagedetailfragment extends fragment { private string mimageurl; private imageview mimageview; private progressbar progressbar; private photoviewattacher mattacher; public static imagedetailfragment newinstance(string imageurl) { final imagedetailfragment f = new imagedetailfragment(); final bundle args = new bundle(); args.putstring("url", imageurl); f.setarguments(args); return f; } @override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); mimageurl = getarguments() != null ? getarguments().getstring("url") : null; } @override public view oncreateview(layoutinflater inflater, viewgroup container, bundle savedinstancestate) { final view v = inflater.inflate(r.layout.image_detail_fragment, container, false); mimageview = (imageview) v.findviewbyid(r.id.image); mattacher = new photoviewattacher(mimageview); mattacher.setonphototaplistener(new onphototaplistener() { @override public void onphototap(view arg0, float arg1, float arg2) { getactivity().finish(); } }); progressbar = (progressbar) v.findviewbyid(r.id.loading); return v; } @override public void onactivitycreated(bundle savedinstancestate) { super.onactivitycreated(savedinstancestate); imageloader.getinstance().displayimage(mimageurl, mimageview, new simpleimageloadinglistener() { @override public void onloadingstarted(string imageuri, view view) { progressbar.setvisibility(view.visible); } @override public void onloadingfailed(string imageuri, view view, failreason failreason) { string message = null; switch (failreason.gettype()) { case io_error: message = "下载错误"; break; case decoding_error: message = "图片无法显示"; break; case network_denied: message = "网络有问题,无法下载"; break; case out_of_memory: message = "图片太大无法显示"; break; case unknown: message = "未知的错误"; break; } toast.maketext(getactivity(), message, toast.length_short).show(); progressbar.setvisibility(view.gone); } @override public void onloadingcomplete(string imageuri, view view, bitmap loadedimage) { progressbar.setvisibility(view.gone); mattacher.update(); } }); } }
写到这里,此篇博文也宣告结束了。需要提出的是,我这里的图片查看器实现的图片的缩放效果使用的是开源组件photoview,关于photoview的github项目地址在这里,https://github.com/chrisbanes/photoview 需要点进去这个项目的网址,去下载源码,将源码全部拷贝到项目中来,使用也是相当方便的,demo如下:
imageview mimageview; photoviewattacher mattacher; @override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); // any implementation of imageview can be used! mimageview = (imageview) findviewbyid(r.id.iv_photo); // set the drawable displayed drawable bitmap = getresources().getdrawable(r.drawable.wallpaper); mimageview.setimagedrawable(bitmap); // attach a photoviewattacher, which takes care of all of the zooming functionality. mattacher = new photoviewattacher(mimageview); } // if you later call mimageview.setimagedrawable/setimagebitmap/setimageresource/etc then you just need to call attacher.update();
刚开始这个图片查看器是我自己自定义view来实现的,其实需要实现图片的手势识别+多点触控+缩放,是可以使用矩阵matrix来实现的,只不过这样显得特别的麻烦不说,而且极易出现bug,这对于某些“急功近利”的项目来说,是个不好的兆头。所以,我这里摒弃了我用matrix自定义的效果,改用github大牛为我们写好的开源组件,这样效率就上去了,大家也可以用matrix自己去实现一下图片的多点触摸缩放的效果,关于matrix的学习,请参加我以前的博文,android自定义控件——3d画廊和图像矩阵。其实关于android上的图片缩放真没什么其它的方式,唯一能使用的还是matrix这个类,不信先来瞧瞧github大牛写的开源组件photoview是怎么实现的,查看以下部分源码:
// these are set so we don't keep allocating them on the heap private final matrix mbasematrix = new matrix(); private final matrix mdrawmatrix = new matrix(); private final matrix msuppmatrix = new matrix(); private final rectf mdisplayrect = new rectf(); private final float[] mmatrixvalues = new float[9];
/** * set's the imageview's scaletype to matrix. */ private static void setimageviewscaletypematrix(imageview imageview) { /** * photoview sets it's own scaletype to matrix, then diverts all calls * setscaletype to this.setscaletype automatically. */ if (null != imageview && !(imageview instanceof iphotoview)) { if (!scaletype.matrix.equals(imageview.getscaletype())) { imageview.setscaletype(scaletype.matrix); } } }
以上只是photoview的部分源码,一目了然的发现它的实现也是基于matrix的,时间与篇幅的局限性,大家需要更好的了解photoview的实现的话,就下载它的源码查看吧,要理解大神的想法是需要一些扎实的基础,关于photoview的具体实现细节,我也弄不太明白,可能是我对matrix了解的不深刻吧,希望以后加强学习,也希望以后跟你们交流学习,共同进步!
本文转载: