Android瀑布流照片墙实现,体验不规则排列的美感
传统界面的布局方式总是行列分明、坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳。这个时候瀑布流布局的出现,就给人带来了耳目一新的感觉,这种布局虽然看上去貌似毫无规律,但是却有一种说不上来的美感,以至于涌现出了大批的网站和应用纷纷使用这种新颖的布局来设计界面。
首先还是讲一下实现原理,瀑布流的布局方式虽然看起来好像排列的很随意,其实它是有很科学的排列规则的。整个界面会根据屏幕的宽度划分成等宽的若干列,由于手机的屏幕不是很大,这里我们就分成三列。每当需要添加一张图片时,会将这张图片的宽度压缩成和列一样宽,再按照同样的压缩比例对图片的高度进行压缩,然后在这三列中找出当前高度最小的一列,将图片添加到这一列中。之后每当需要添加一张新图片时,都去重复上面的操作,就会形成瀑布流格局的照片墙,示意图如下所示。
听我这么说完后,你可能会觉得瀑布流的布局非常简单嘛,只需要使用三个LinearLayout平分整个屏幕宽度,然后动态地addView()进去就好了。确实如此,如果只是为了实现功能的话,就是这么简单。可是别忘了,我们是在手机上进行开发,如果不停地往LinearLayout里添加图片,程序很快就会OOM。因此我们还需要一个合理的方案来对图片资源进行释放,这里仍然是准备使用LruCache算法,对这个算法不熟悉的朋友可以先参考Android高效加载大图、多图方案,有效避免程序OOM。
下面我们就来开始实现吧,新建一个Android项目,起名叫PhotoWallFallsDemo,并选择4.0的API。
第一个要考虑的问题是,我们到哪儿去收集这些大小参差不齐的图片呢?这里我事先在百度上搜索了很多张风景图片,并且为了保证它们访问的稳定性,我将这些图片都上传到了我的CSDN相册里,因此只要从这里下载图片就可以了。新建一个Images类,将所有相册中图片的网址都配置进去,代码如下所示:
[java]viewplaincopy
publicclassImages{
publicfinalstaticString[]imageUrls=newString[]{
"http://img.my.csdn.net/uploads/201309/01/1378037235_3453.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037235_9280.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037234_3539.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037234_6318.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037193_1687.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037193_1286.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037192_8379.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037178_9374.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037177_1254.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037177_6203.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037152_6352.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037151_9565.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037151_7904.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037148_7104.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037129_8825.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037128_3531.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037127_1085.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037095_7515.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037094_8001.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037093_7168.jpg",
"http://img.my.csdn.net/uploads/201309/01/1378037091_4950.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949643_6410.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949642_6939.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949630_4505.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949630_4593.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949629_7309.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949629_8247.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949615_1986.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949614_8482.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949614_3743.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949614_4199.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949599_3416.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949599_5269.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949598_7858.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949598_9982.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949578_2770.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949578_8744.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949577_5210.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949577_1998.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949482_8813.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949481_6577.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949480_4490.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949455_6792.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949455_6345.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949442_4553.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949441_8987.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949441_5454.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949454_6367.jpg",
"http://img.my.csdn.net/uploads/201308/31/1377949442_4562.jpg"};
}
然后新建一个ImageLoader类,用于方便对图片进行管理,代码如下所示:
[java]viewplaincopy
publicclassImageLoader{
/
图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
/
privatestaticLruCachemMemoryCache;
/
ImageLoader的实例。
/
privatestaticImageLoadermImageLoader;
privateImageLoader(){
//获取应用程序最大可用内存
intmaxMemory=(int)Runtime.getRuntime().maxMemory();
intcacheSize=maxMemory/8;
//设置图片缓存大小为程序最大可用内存的1/8
mMemoryCache=newLruCache(cacheSize){
@Override
protectedintsizeOf(Stringkey,Bitmapbitmap){
returnbitmap.getByteCount();
}
};
}
/
获取ImageLoader的实例。
@returnImageLoader的实例。
/
publicstaticImageLoadergetInstance(){
if(mImageLoader==null){
mImageLoader=newImageLoader();
}
returnmImageLoader;
}
/
将一张图片存储到LruCache中。
@paramkey
LruCache的键,这里传入图片的URL地址。
@parambitmap
LruCache的键,这里传入从网络上下载的Bitmap对象。
/
publicvoidaddBitmapToMemoryCache(Stringkey,Bitmapbitmap){
if(getBitmapFromMemoryCache(key)==null){
mMemoryCache.put(key,bitmap);
}
}
/
从LruCache中获取一张图片,如果不存在就返回null。
@paramkey
LruCache的键,这里传入图片的URL地址。
@return对应传入键的Bitmap对象,或者null。
/
publicBitmapgetBitmapFromMemoryCache(Stringkey){
returnmMemoryCache.get(key);
}
publicstaticintcalculateInSampleSize(BitmapFactory.Optionsoptions,
intreqWidth){
//源图片的宽度
finalintwidth=options.outWidth;
intinSampleSize=1;
if(width>reqWidth){
//计算出实际宽度和目标宽度的比率
finalintwidthRatio=Math.round((float)width/(float)reqWidth);
inSampleSize=widthRatio;
}
returninSampleSize;
}
publicstaticBitmapdecodeSampledBitmapFromResource(StringpathName,
intreqWidth){
//第一次解析将inJustDecodeBounds设置为true,来获取图片大小
finalBitmapFactory.Optionsoptions=newBitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeFile(pathName,options);
//调用上面定义的方法计算inSampleSize值
options.inSampleSize=calculateInSampleSize(options,reqWidth);
//使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds=false;
returnBitmapFactory.decodeFile(pathName,options);
}
}
这里我们将ImageLoader类设成单例,并在构造函数中初始化了LruCache类,把它的最大缓存容量设为最大可用内存的1/8。然后又提供了其它几个方法可以操作LruCache,以及对图片进行压缩和读取。
接下来新建MyScrollView继承自ScrollView,代码如下所示:
[java]viewplaincopy
publicclassMyScrollViewextendsScrollViewimplementsOnTouchListener{
/
每页要加载的图片数量
/
publicstaticfinalintPAGE_SIZE=15;
/
记录当前已加载到第几页
/
privateintpage;
/
每一列的宽度
/
privateintcolumnWidth;
/
当前第一列的高度
/
privateintfirstColumnHeight;
/
当前第二列的高度
/
privateintsecondColumnHeight;
/
当前第三列的高度
/
privateintthirdColumnHeight;
/
是否已加载过一次layout,这里onLayout中的初始化只需加载一次
/
privatebooleanloadOnce;
/
对图片进行管理的工具类
/
privateImageLoaderimageLoader;
/
第一列的布局
/
privateLinearLayoutfirstColumn;
/
第二列的布局
/
privateLinearLayoutsecondColumn;
/
第三列的布局
/
privateLinearLayoutthirdColumn;
/
记录所有正在下载或等待下载的任务。
/
privatestaticSettaskCollection;
/
MyScrollView下的直接子布局。
/
privatestaticViewscrollLayout;
/
MyScrollView布局的高度。
/
privatestaticintscrollViewHeight;
/
记录上垂直方向的滚动距离。
/
privatestaticintlastScrollY=-1;
/
记录所有界面上的图片,用以可以随时控制对图片的释放。
/
privateListimageViewList=newArrayList();
/
在Handler中进行图片可见性检查的判断,以及加载更多图片的操作。
/
privatestaticHandlerhandler=newHandler(){
publicvoidhandleMessage(android.os.Messagemsg){
MyScrollViewmyScrollView=(MyScrollView)msg.obj;
intscrollY=myScrollView.getScrollY();
//如果当前的滚动位置和上次相同,表示已停止滚动
if(scrollY==lastScrollY){
//当滚动的最底部,并且当前没有正在下载的任务时,开始加载下一页的图片
if(scrollViewHeight+scrollY>=scrollLayout.getHeight()
&&taskCollection.isEmpty()){
myScrollView.loadMoreImages();
}
myScrollView.checkVisibility();
}else{
lastScrollY=scrollY;
Messagemessage=newMessage();
message.obj=myScrollView;
//5毫秒后再次对滚动位置进行判断
handler.sendMessageDelayed(message,5);
}
};
};
/
MyScrollView的构造函数。
@paramcontext
@paramattrs
/
publicMyScrollView(Contextcontext,AttributeSetattrs){
super(context,attrs);
imageLoader=ImageLoader.getInstance();
taskCollection=newHashSet();
setOnTouchListener(this);
}
/
进行一些关键性的初始化操作,获取MyScrollView的高度,以及得到第一列的宽度值。并在这里开始加载第一页的图片。
/
@Override
protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){
super.onLayout(changed,l,t,r,b);
if(changed&&!loadOnce){
scrollViewHeight=getHeight();
scrollLayout=getChildAt(0);
firstColumn=(LinearLayout)findViewById(R.id.first_column);
secondColumn=(LinearLayout)findViewById(R.id.second_column);
thirdColumn=(LinearLayout)findViewById(R.id.third_column);
columnWidth=firstColumn.getWidth();
loadOnce=true;
loadMoreImages();
}
}
/
监听用户的触屏事件,如果用户手指离开屏幕则开始进行滚动检测。
/
@Override
publicbooleanonTouch(Viewv,MotionEventevent){
if(event.getAction()==MotionEvent.ACTION_UP){
Messagemessage=newMessage();
message.obj=this;
handler.sendMessageDelayed(message,5);
}
returnfalse;
}
/
开始加载下一页的图片,每张图片都会开启一个异步线程去下载。
/
publicvoidloadMoreImages(){
if(hasSDCard()){
intstartIndex=pagePAGE_SIZE;
intendIndex=pagePAGE_SIZE+PAGE_SIZE;
if(startIndex Toast.makeText(getContext(),"正在加载...",Toast.LENGTH_SHORT)
.show();
if(endIndex>Images.imageUrls.length){
endIndex=Images.imageUrls.length;
}
for(inti=startIndex;i LoadImageTasktask=newLoadImageTask();
taskCollection.add(task);
task.execute(Images.imageUrls[i]);
}
page++;
}else{
Toast.makeText(getContext(),"已没有更多图片",Toast.LENGTH_SHORT)
.show();
}
}else{
Toast.makeText(getContext(),"未发现SD卡",Toast.LENGTH_SHORT).show();
}
}
/
遍历imageViewList中的每张图片,对图片的可见性进行检查,如果图片已经离开屏幕可见范围,则将图片替换成一张空图。
/
publicvoidcheckVisibility(){
for(inti=0;i ImageViewimageView=imageViewList.get(i);
intborderTop=(Integer)imageView.getTag(R.string.border_top);
intborderBottom=(Integer)imageView
.getTag(R.string.border_bottom);
if(borderBottom>getScrollY()
&&borderTop StringimageUrl=(String)imageView.getTag(R.string.image_url);
Bitmapbitmap=imageLoader.getBitmapFromMemoryCache(imageUrl);
if(bitmap!=null){
imageView.setImageBitmap(bitmap);
}else{
LoadImageTasktask=newLoadImageTask(imageView);
task.execute(imageUrl);
}
}else{
imageView.setImageResource(R.drawable.empty_photo);
}
}
}
/
判断手机是否有SD卡。
@return有SD卡返回true,没有返回false。
/
privatebooleanhasSDCard(){
returnEnvironment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState());
}
/
异步下载图片的任务。
@authorguolin
/
classLoadImageTaskextendsAsyncTask{
/
图片的URL地址
/
privateStringmImageUrl;
/
可重复使用的ImageView
/
privateImageViewmImageView;
publicLoadImageTask(){
}
/
将可重复使用的ImageView传入
@paramimageView
/
publicLoadImageTask(ImageViewimageView){
mImageView=imageView;
}
@Override
protectedBitmapdoInBackground(String...params){
mImageUrl=params[0];
BitmapimageBitmap=imageLoader
.getBitmapFromMemoryCache(mImageUrl);
if(imageBitmap==null){
imageBitmap=loadImage(mImageUrl);
}
returnimageBitmap;
}
@Override
protectedvoidonPostExecute(Bitmapbitmap){
if(bitmap!=null){
doubleratio=bitmap.getWidth()/(columnWidth1.0);
intscaledHeight=(int)(bitmap.getHeight()/ratio);
addImage(bitmap,columnWidth,scaledHeight);
}
taskCollection.remove(this);
}
/
根据传入的URL,对图片进行加载。如果这张图片已经存在于SD卡中,则直接从SD卡里读取,否则就从网络上下载。
@paramimageUrl
图片的URL地址
@return加载到内存的图片。
/
privateBitmaploadImage(StringimageUrl){
FileimageFile=newFile(getImagePath(imageUrl));
if(!imageFile.exists()){
downloadImage(imageUrl);
}
if(imageUrl!=null){
Bitmapbitmap=ImageLoader.decodeSampledBitmapFromResource(
imageFile.getPath(),columnWidth);
if(bitmap!=null){
imageLoader.addBitmapToMemoryCache(imageUrl,bitmap);
returnbitmap;
}
}
returnnull;
}
/
向ImageView中添加一张图片
@parambitmap
待添加的图片
@paramimageWidth
图片的宽度
@paramimageHeight
图片的高度
/
privatevoidaddImage(Bitmapbitmap,intimageWidth,intimageHeight){
LinearLayout.LayoutParamsparams=newLinearLayout.LayoutParams(
imageWidth,imageHeight);
if(mImageView!=null){
mImageView.setImageBitmap(bitmap);
}else{
ImageViewimageView=newImageView(getContext());
imageView.setLayoutParams(params);
imageView.setImageBitmap(bitmap);
imageView.setScaleType(ScaleType.FIT_XY);
imageView.setPadding(5,5,5,5);
imageView.setTag(R.string.image_url,mImageUrl);
findColumnToAdd(imageView,imageHeight).addView(imageView);
imageViewList.add(imageView);
}
}
/
找到此时应该添加图片的一列。原则就是对三列的高度进行判断,当前高度最小的一列就是应该添加的一列。
@paramimageView
@paramimageHeight
@return应该添加图片的一列
/
privateLinearLayoutfindColumnToAdd(ImageViewimageView,
intimageHeight){
if(firstColumnHeight<=secondColumnHeight){
if(firstColumnHeight<=thirdColumnHeight){
imageView.setTag(R.string.border_top,firstColumnHeight);
firstColumnHeight+=imageHeight;
imageView.setTag(R.string.border_bottom,firstColumnHeight);
returnfirstColumn;
}
imageView.setTag(R.string.border_top,thirdColumnHeight);
thirdColumnHeight+=imageHeight;
imageView.setTag(R.string.border_bottom,thirdColumnHeight);
returnthirdColumn;
}else{
if(secondColumnHeight<=thirdColumnHeight){
imageView.setTag(R.string.border_top,secondColumnHeight);
secondColumnHeight+=imageHeight;
imageView
.setTag(R.string.border_bottom,secondColumnHeight);
returnsecondColumn;
}
imageView.setTag(R.string.border_top,thirdColumnHeight);
thirdColumnHeight+=imageHeight;
imageView.setTag(R.string.border_bottom,thirdColumnHeight);
returnthirdColumn;
}
}
/
将图片下载到SD卡缓存起来。
@paramimageUrl
图片的URL地址。
/
privatevoiddownloadImage(StringimageUrl){
HttpURLConnectioncon=null;
FileOutputStreamfos=null;
BufferedOutputStreambos=null;
BufferedInputStreambis=null;
FileimageFile=null;
try{
URLurl=newURL(imageUrl);
con=(HttpURLConnection)url.openConnection();
con.setConnectTimeout(51000);
con.setReadTimeout(151000);
con.setDoInput(true);
con.setDoOutput(true);
bis=newBufferedInputStream(con.getInputStream());
imageFile=newFile(getImagePath(imageUrl));
fos=newFileOutputStream(imageFile);
bos=newBufferedOutputStream(fos);
byte[]b=newbyte[1024];
intlength;
while((length=bis.read(b))!=-1){
bos.write(b,0,length);
bos.flush();
}
}catch(Exceptione){
e.printStackTrace();
}finally{
try{
if(bis!=null){
bis.close();
}
if(bos!=null){
bos.close();
}
if(con!=null){
con.disconnect();
}
}catch(IOExceptione){
e.printStackTrace();
}
}
if(imageFile!=null){
Bitmapbitmap=ImageLoader.decodeSampledBitmapFromResource(
imageFile.getPath(),colwww.shanxiwang.netumnWidth);
if(bitmap!=null){
imageLoader.addBitmapToMemoryCache(imageUrl,bitmap);
}
}
}
/
获取图片的本地存储路径。
@paramimageUrl
图片的URL地址。
@return图片的本地存储路径。
/
privateStringgetImagePath(StringimageUrl){
intlastSlashIndex=imageUrl.lastIndexOf("/");
StringimageName=imageUrl.substring(lastSlashIndex+1);
StringimageDir=Environment.getExternalStorageDirectory()
.getPath()+"/PhotoWallFalls/";
Filefile=newFile(imageDir);
if(!file.exists()){
file.mkdirs();
}
StringimagePath=imageDir+imageName;
returnimagePath;
}
}
}
MyScrollView是实现瀑布流照片墙的核心类,这里我来重点给大家介绍一下。首先它是继承自ScrollView的,这样就允许用户可以通过滚动的方式来浏览更多的图片。这里提供了一个loadMoreImages()方法,是专门用于加载下一页的图片的,因此在onLayout()方法中我们要先调用一次这个方法,以初始化第一页的图片。然后在onTouch方法中每当监听到手指离开屏幕的事件,就会通过一个handler来对当前ScrollView的滚动状态进行判断,如果发现已经滚动到了最底部,就会再次调用loadMoreImages()方法去加载下一页的图片。
那我们就要来看一看loadMoreImages()方法的内部细节了。在这个方法中,使用了一个循环来加载这一页中的每一张图片,每次都会开启一个LoadImageTask,用于对图片进行异步加载。然后在LoadImageTask中,首先会先检查一下这张图片是不是已经存在于SD卡中了,如果还没存在,就从网络上下载,然后把这张图片存放在LruCache中。接着将这张图按照一定的比例进行压缩,并找出当前高度最小的一列,把压缩后的图片添加进去就可以了。
另外,为了保证照片墙上的图片都能够合适地被回收,这里还加入了一个可见性检查的方法,即checkVisibility()方法。这个方法的核心思想就是检查目前照片墙上的所有图片,判断出哪些是可见的,哪些是不可见。然后将那些不可见的图片都替换成一张空图,这样就可以保证程序始终不会占用过高的内存。当这些图片又重新变为可见的时候,只需要再从LruCache中将这些图片重新取出即可。如果某张图片已经从LruCache中被移除了,就会开启一个LoadImageTask,将这张图片重新加载到内存中。
然后打开或新建activity_main.xml,在里面设置好瀑布流的布局方式,如下所示:
[html]viewplaincopy
android:id="@+id/my_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:id="@+id/first_column"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
android:id="@+id/second_column"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
android:id="@+id/third_column"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
可以看到,这里我们使用了刚才编写好的MyScrollView作为根布局,然后在里面放入了一个直接子布局LinearLayout用于统计当前滑动布局的高度,然后在这个布局下又添加了三个等宽的LinearLayout分别作为第一列、第二列和第三列的布局,这样在MyScrollView中就可以动态地向这三个LinearLayout里添加图片了。
最后,由于我们使用到了网络和SD卡存储的功能,因此还需要在AndroidManifest.xml中添加以下权限:
[html]viewplaincopy
这样我们所有的编码工作就已经完成了,现在可以尝试运行一下,效果如下图所示:
瀑布流模式的照片墙果真非常美观吧,而且由于我们有非常完善的资源释放机制,不管你在照片墙上添加了多少图片,程序占用内存始终都会保持在一个合理的范围内。在下一篇文章中,我会带着大家对这个程序进行进一步的完善,加入点击查看大图,以及多点触控缩放的功能,感觉兴趣的朋友请继续阅读Android多点触控技术实战,自由地对图片进行缩放和移动。
|
|