传统界面的布局方式总是行列分明、坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳。这个时候瀑布流布局的出现,就给人带来了耳目一新的感觉,这种布局虽然看上去貌似毫无规律,但是却有一种说不上来的美感,以至于涌现出了大批的网站和应用纷纷使用这种新颖的布局来设计界面。
记得我在之前已经写过一篇关于如何在Android上实现照片墙功能的文章了,但那个时候是使用的GridView来进行布局的,这种布局方式只适用于“墙”上的每张图片大小都相同的情况,如果图片的大小参差不齐,在GridView中显示就会非常的难看。而使用瀑布流的布局方式就可以很好地解决这个问题,因此今天我们也来赶一下潮流,看看如何在Android上实现瀑布流照片墙的功能。
首先还是讲一下实现原理,瀑布流的布局方式虽然看起来好像排列的很随意,其实它是有很科学的排列规则的。整个界面会根据屏幕的宽度划分成等宽的若干列,由于手机的屏幕不是很大,这里我们就分成三列。每当需要添加一张图片时,会将这张图片的宽度压缩成和列一样宽,再按照同样的压缩比例对图片的高度进行压缩,然后在这三列中找出当前高度最小的一列,将图片添加到这一列中。之后每当需要添加一张新图片时,都去重复上面的操作,就会形成瀑布流格局的照片墙,示意图如下所示。
听我这么说完后,你可能会觉得瀑布流的布局非常简单嘛,只需要使用三个LinearLayout平分整个屏幕宽度,然后动态地addView()进去就好了。确实如此,如果只是为了实现功能的话,就是这么简单。可是别忘了,我们是在手机上进行开发,如果不停地往LinearLayout里添加图片,程序很快就会OOM。因此我们还需要一个合理的方案来对图片资源进行释放。
源码下载地址:http://download.csdn.net/detail/hcb1230/6613441
下面我们就来开始实现吧,新建一个Android项目,起名叫PhotoWallFallsDemo,并选择4.0的API。
第一个要考虑的问题是,我们到哪儿去收集这些大小参差不齐的图片呢?这里我事先在百度上搜索了很多张风景图片,并且为了保证它们访问的稳定性,我将这些图片都上传到了我的CSDN相册里,因此只要从这里下载图片就可以了。新建一个Images类,将所有相册中图片的网址都配置进去,代码如下所示:
- public class Images {
-
- public final static String[] imageUrls = new String[] {
- "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类,用于方便对图片进行管理,代码如下所示:
- public class ImageLoader {
-
-
-
-
- private static LruCache<String, Bitmap> mMemoryCache;
-
-
-
-
- private static ImageLoader mImageLoader;
-
- private ImageLoader() {
-
- int maxMemory = (int) Runtime.getRuntime().maxMemory();
- int cacheSize = maxMemory / 8;
-
- mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
- @Override
- protected int sizeOf(String key, Bitmap bitmap) {
- return bitmap.getByteCount();
- }
- };
- }
-
-
-
-
-
-
- public static ImageLoader getInstance() {
- if (mImageLoader == null) {
- mImageLoader = new ImageLoader();
- }
- return mImageLoader;
- }
-
-
-
-
-
-
-
-
-
- public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
- if (getBitmapFromMemoryCache(key) == null) {
- mMemoryCache.put(key, bitmap);
- }
- }
-
-
-
-
-
-
-
-
- public Bitmap getBitmapFromMemoryCache(String key) {
- return mMemoryCache.get(key);
- }
-
- public static int calculateInSampleSize(BitmapFactory.Options options,
- int reqWidth) {
-
- final int width = options.outWidth;
- int inSampleSize = 1;
- if (width > reqWidth) {
-
- final int widthRatio = Math.round((float) width / (float) reqWidth);
- inSampleSize = widthRatio;
- }
- return inSampleSize;
- }
-
- public static Bitmap decodeSampledBitmapFromResource(String pathName,
- int reqWidth) {
-
- final BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(pathName, options);
-
- options.inSampleSize = calculateInSampleSize(options, reqWidth);
-
- options.inJustDecodeBounds = false;
- return BitmapFactory.decodeFile(pathName, options);
- }
-
- }
这里我们将ImageLoader类设成单例,并在构造函数中初始化了LruCache类,把它的最大缓存容量设为最大可用内存的1/8。然后又提供了其它几个方法可以操作LruCache,以及对图片进行压缩和读取。
接下来新建MyScrollView继承自ScrollView,代码如下所示:
- public class MyScrollView extends ScrollView implements OnTouchListener {
-
-
-
-
- public static final int PAGE_SIZE = 15;
-
-
-
-
- private int page;
-
-
-
-
- private int columnWidth;
-
-
-
-
- private int firstColumnHeight;
-
-
-
-
- private int secondColumnHeight;
-
-
-
-
- private int thirdColumnHeight;
-
-
-
-
- private boolean loadOnce;
-
-
-
-
- private ImageLoader imageLoader;
-
-
-
-
- private LinearLayout firstColumn;
-
-
-
-
- private LinearLayout secondColumn;
-
-
-
-
- private LinearLayout thirdColumn;
-
-
-
-
- private static Set<LoadImageTask> taskCollection;
-
-
-
-
- private static View scrollLayout;
-
-
-
-
- private static int scrollViewHeight;
-
-
-
-
- private static int lastScrollY = -1;
-
-
-
-
- private List<ImageView> imageViewList = new ArrayList<ImageView>();
-
-
-
-
- private static Handler handler = new Handler() {
-
- public void handleMessage(android.os.Message msg) {
- MyScrollView myScrollView = (MyScrollView) msg.obj;
- int scrollY = myScrollView.getScrollY();
-
- if (scrollY == lastScrollY) {
-
- if (scrollViewHeight + scrollY >= scrollLayout.getHeight()
- && taskCollection.isEmpty()) {
- myScrollView.loadMoreImages();
- }
- myScrollView.checkVisibility();
- } else {
- lastScrollY = scrollY;
- Message message = new Message();
- message.obj = myScrollView;
-
- handler.sendMessageDelayed(message, 5);
- }
- };
-
- };
-
-
-
-
-
-
-
- public MyScrollView(Context context, AttributeSet attrs) {
- super(context, attrs);
- imageLoader = ImageLoader.getInstance();
- taskCollection = new HashSet<LoadImageTask>();
- setOnTouchListener(this);
- }
-
-
-
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- 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
- public boolean onTouch(View v, MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_UP) {
- Message message = new Message();
- message.obj = this;
- handler.sendMessageDelayed(message, 5);
- }
- return false;
- }
-
-
-
-
- public void loadMoreImages() {
- if (hasSDCard()) {
- int startIndex = page * PAGE_SIZE;
- int endIndex = page * PAGE_SIZE + PAGE_SIZE;
- if (startIndex < Images.imageUrls.length) {
- Toast.makeText(getContext(), "正在加载...", Toast.LENGTH_SHORT)
- .show();
- if (endIndex > Images.imageUrls.length) {
- endIndex = Images.imageUrls.length;
- }
- for (int i = startIndex; i < endIndex; i++) {
- LoadImageTask task = new LoadImageTask();
- 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();
- }
- }
-
-
-
-
- public void checkVisibility() {
- for (int i = 0; i < imageViewList.size(); i++) {
- ImageView imageView = imageViewList.get(i);
- int borderTop = (Integer) imageView.getTag(R.string.border_top);
- int borderBottom = (Integer) imageView
- .getTag(R.string.border_bottom);
- if (borderBottom > getScrollY()
- && borderTop < getScrollY() + scrollViewHeight) {
- String imageUrl = (String) imageView.getTag(R.string.image_url);
- Bitmap bitmap = imageLoader.getBitmapFromMemoryCache(imageUrl);
- if (bitmap != null) {
- imageView.setImageBitmap(bitmap);
- } else {
- LoadImageTask task = new LoadImageTask(imageView);
- task.execute(imageUrl);
- }
- } else {
- imageView.setImageResource(R.drawable.empty_photo);
- }
- }
- }
-
-
-
-
-
-
- private boolean hasSDCard() {
- return Environment.MEDIA_MOUNTED.equals(Environment
- .getExternalStorageState());
- }
-
-
-
-
-
-
- class LoadImageTask extends AsyncTask<String, Void, Bitmap> {
-
-
-
-
- private String mImageUrl;
-
-
-
-
- private ImageView mImageView;
-
- public LoadImageTask() {
- }
-
-
-
-
-
-
- public LoadImageTask(ImageView imageView) {
- mImageView = imageView;
- }
-
- @Override
- protected Bitmap doInBackground(String... params) {
- mImageUrl = params[0];
- Bitmap imageBitmap = imageLoader
- .getBitmapFromMemoryCache(mImageUrl);
- if (imageBitmap == null) {
- imageBitmap = loadImage(mImageUrl);
- }
- return imageBitmap;
- }
-
- @Override
- protected void onPostExecute(Bitmap bitmap) {
- if (bitmap != null) {
- double ratio = bitmap.getWidth() / (columnWidth * 1.0);
- int scaledHeight = (int) (bitmap.getHeight() / ratio);
- addImage(bitmap, columnWidth, scaledHeight);
- }
- taskCollection.remove(this);
- }
-
-
-
-
-
-
-
-
- private Bitmap loadImage(String imageUrl) {
- File imageFile = new File(getImagePath(imageUrl));
- if (!imageFile.exists()) {
- downloadImage(imageUrl);
- }
- if (imageUrl != null) {
- Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource(
- imageFile.getPath(), columnWidth);
- if (bitmap != null) {
- imageLoader.addBitmapToMemoryCache(imageUrl, bitmap);
- return bitmap;
- }
- }
- return null;
- }
-
-
-
-
-
-
-
-
-
-
-
- private void addImage(Bitmap bitmap, int imageWidth, int imageHeight) {
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- imageWidth, imageHeight);
- if (mImageView != null) {
- mImageView.setImageBitmap(bitmap);
- } else {
- ImageView imageView = new ImageView(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);
- }
- }
-
-
-
-
-
-
-
-
- private LinearLayout findColumnToAdd(ImageView imageView,
- int imageHeight) {
- if (firstColumnHeight <= secondColumnHeight) {
- if (firstColumnHeight <= thirdColumnHeight) {
- imageView.setTag(R.string.border_top, firstColumnHeight);
- firstColumnHeight += imageHeight;
- imageView.setTag(R.string.border_bottom, firstColumnHeight);
- return firstColumn;
- }
- imageView.setTag(R.string.border_top, thirdColumnHeight);
- thirdColumnHeight += imageHeight;
- imageView.setTag(R.string.border_bottom, thirdColumnHeight);
- return thirdColumn;
- } else {
- if (secondColumnHeight <= thirdColumnHeight) {
- imageView.setTag(R.string.border_top, secondColumnHeight);
- secondColumnHeight += imageHeight;
- imageView
- .setTag(R.string.border_bottom, secondColumnHeight);
- return secondColumn;
- }
- imageView.setTag(R.string.border_top, thirdColumnHeight);
- thirdColumnHeight += imageHeight;
- imageView.setTag(R.string.border_bottom, thirdColumnHeight);
- return thirdColumn;
- }
- }
-
-
-
-
-
-
-
- private void downloadImage(String imageUrl) {
- HttpURLConnection con = null;
- FileOutputStream fos = null;
- BufferedOutputStream bos = null;
- BufferedInputStream bis = null;
- File imageFile = null;
- try {
- URL url = new URL(imageUrl);
- con = (HttpURLConnection) url.openConnection();
- con.setConnectTimeout(5 * 1000);
- con.setReadTimeout(15 * 1000);
- con.setDoInput(true);
- con.setDoOutput(true);
- bis = new BufferedInputStream(con.getInputStream());
- imageFile = new File(getImagePath(imageUrl));
- fos = new FileOutputStream(imageFile);
- bos = new BufferedOutputStream(fos);
- byte[] b = new byte[1024];
- int length;
- while ((length = bis.read(b)) != -1) {
- bos.write(b, 0, length);
- bos.flush();
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (bis != null) {
- bis.close();
- }
- if (bos != null) {
- bos.close();
- }
- if (con != null) {
- con.disconnect();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (imageFile != null) {
- Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource(
- imageFile.getPath(), columnWidth);
- if (bitmap != null) {
- imageLoader.addBitmapToMemoryCache(imageUrl, bitmap);
- }
- }
- }
-
-
-
-
-
-
-
-
- private String getImagePath(String imageUrl) {
- int lastSlashIndex = imageUrl.lastIndexOf("/");
- String imageName = imageUrl.substring(lastSlashIndex + 1);
- String imageDir = Environment.getExternalStorageDirectory()
- .getPath() + "/PhotoWallFalls/";
- File file = new File(imageDir);
- if (!file.exists()) {
- file.mkdirs();
- }
- String imagePath = imageDir + imageName;
- return imagePath;
- }
- }
-
- }
MyScrollView是实现瀑布流照片墙的核心类,这里我来重点给大家介绍一下。首先它是继承自ScrollView的,这样就允许用户可以通过滚动的方式来浏览更多的图片。这里提供了一个loadMoreImages()方法,是专门用于加载下一页的图片的,因此在onLayout()方法中我们要先调用一次这个方法,以初始化第一页的图片。然后在onTouch方法中每当监听到手指离开屏幕的事件,就会通过一个handler来对当前ScrollView的滚动状态进行判断,如果发现已经滚动到了最底部,就会再次调用loadMoreImages()方法去加载下一页的图片。
那我们就要来看一看loadMoreImages()方法的内部细节了。在这个方法中,使用了一个循环来加载这一页中的每一张图片,每次都会开启一个LoadImageTask,用于对图片进行异步加载。然后在LoadImageTask中,首先会先检查一下这张图片是不是已经存在于SD卡中了,如果还没存在,就从网络上下载,然后把这张图片存放在LruCache中。接着将这张图按照一定的比例进行压缩,并找出当前高度最小的一列,把压缩后的图片添加进去就可以了。
另外,为了保证照片墙上的图片都能够合适地被回收,这里还加入了一个可见性检查的方法,即checkVisibility()方法。这个方法的核心思想就是检查目前照片墙上的所有图片,判断出哪些是可见的,哪些是不可见。然后将那些不可见的图片都替换成一张空图,这样就可以保证程序始终不会占用过高的内存。当这些图片又重新变为可见的时候,只需要再从LruCache中将这些图片重新取出即可。如果某张图片已经从LruCache中被移除了,就会开启一个LoadImageTask,将这张图片重新加载到内存中。
然后打开或新建activity_main.xml,在里面设置好瀑布流的布局方式,如下所示:
- <com.example.photowallfallsdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/my_scroll_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:rientation="horizontal" >
-
- <LinearLayout
- android:id="@+id/first_column"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:rientation="vertical" >
- </LinearLayout>
-
- <LinearLayout
- android:id="@+id/second_column"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:rientation="vertical" >
- </LinearLayout>
-
- <LinearLayout
- android:id="@+id/third_column"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:rientation="vertical" >
- </LinearLayout>
- </LinearLayout>
-
- </com.example.photowallfallsdemo.MyScrollView>
可以看到,这里我们使用了刚才编写好的MyScrollView作为根布局,然后在里面放入了一个直接子布局LinearLayout用于统计当前滑动布局的高度,然后在这个布局下又添加了三个等宽的LinearLayout分别作为第一列、第二列和第三列的布局,这样在MyScrollView中就可以动态地向这三个LinearLayout里添加图片了。
最后,由于我们使用到了网络和SD卡存储的功能,因此还需要在AndroidManifest.xml中添加以下权限:
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.INTERNET" />
瀑布流模式的照片墙果真非常美观吧,而且由于我们有非常完善的资源释放机制,不管你在照片墙上添加了多少图片,程序占用内存始终都会保持在一个合理的范围内。