Android瀑布流照片墙实现 体验不规则排列的美感

传统界面的布局方式总是行列分明、坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳。这个时候瀑布流布局的出现,就给人带来了耳目一新的感觉,这种布局虽然看上去貌似毫无规律,但是却有一种说不上来的美感,以至于涌现出了大批的网站和应用纷纷使用这种新颖的布局来设计界面。

记得我在之前已经写过一篇关于如何在Android上实现照片墙功能的文章了,但那个时候是使用的GridView来进行布局的,这种布局方式只适用于“墙”上的每张图片大小都相同的情况,如果图片的大小参差不齐,在GridView中显示就会非常的难看。而使用瀑布流的布局方式就可以很好地解决这个问题,因此今天我们也来赶一下潮流,看看如何在Android上实现瀑布流照片墙的功能。

首先还是讲一下实现原理,瀑布流的布局方式虽然看起来好像排列的很随意,其实它是有很科学的排列规则的。整个界面会根据屏幕的宽度划分成等宽的若干列,由于手机的屏幕不是很大,这里我们就分成三列。每当需要添加一张图片时,会将这张图片的宽度压缩成和列一样宽,再按照同样的压缩比例对图片的高度进行压缩,然后在这三列中找出当前高度最小的一列,将图片添加到这一列中。之后每当需要添加一张新图片时,都去重复上面的操作,就会形成瀑布流格局的照片墙,示意图如下所示。

听我这么说完后,你可能会觉得瀑布流的布局非常简单嘛,只需要使用三个LinearLayout平分整个屏幕宽度,然后动态地addView()进去就好了。确实如此,如果只是为了实现功能的话,就是这么简单。可是别忘了,我们是在手机上进行开发,如果不停地往LinearLayout里添加图片,程序很快就会OOM。因此我们还需要一个合理的方案来对图片资源进行释放,这里仍然是准备使用LruCache算法,对这个算法不熟悉的朋友可以先参考Android高效加载大图、多图方案,有效避免程序OOM。

下面我们就来开始实现吧,新建一个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; 

 /**
 * ImageLoader的实例。
 */
 private static ImageLoader mImageLoader; 

 private ImageLoader() {
 // 获取应用程序最大可用内存
 int maxMemory = (int) Runtime.getRuntime().maxMemory();
 int cacheSize = maxMemory / 8;
 // 设置图片缓存大小为程序最大可用内存的1/8
 mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
 @Override
 protected int sizeOf(String key, Bitmap bitmap) {
 return bitmap.getByteCount();
 }
 };
 } 

 /**
 * 获取ImageLoader的实例。
 *
 * @return ImageLoader的实例。
 */
 public static ImageLoader getInstance() {
 if (mImageLoader == null) {
 mImageLoader = new ImageLoader();
 }
 return mImageLoader;
 } 

 /**
 * 将一张图片存储到LruCache中。
 *
 * @param key
 * LruCache的键,这里传入图片的URL地址。
 * @param bitmap
 * LruCache的键,这里传入从网络上下载的Bitmap对象。
 */
 public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
 if (getBitmapFromMemoryCache(key) == null) {
 mMemoryCache.put(key, bitmap);
 }
 } 

 /**
 * 从LruCache中获取一张图片,如果不存在就返回null。
 *
 * @param key
 * LruCache的键,这里传入图片的URL地址。
 * @return 对应传入键的Bitmap对象,或者null。
 */
 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) {
 // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
 final BitmapFactory.Options options = new BitmapFactory.Options();
 options.inJustDecodeBounds = true;
 BitmapFactory.decodeFile(pathName, options);
 // 调用上面定义的方法计算inSampleSize值
 options.inSampleSize = calculateInSampleSize(options, reqWidth);
 // 使用获取到的inSampleSize值再次解析图片
 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; 

 /**
 * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
 */
 private boolean loadOnce; 

 /**
 * 对图片进行管理的工具类
 */
 private ImageLoader imageLoader; 

 /**
 * 第一列的布局
 */
 private LinearLayout firstColumn; 

 /**
 * 第二列的布局
 */
 private LinearLayout secondColumn; 

 /**
 * 第三列的布局
 */
 private LinearLayout thirdColumn; 

 /**
 * 记录所有正在下载或等待下载的任务。
 */
 private static Set<LoadImageTask> taskCollection; 

 /**
 * MyScrollView下的直接子布局。
 */
 private static View scrollLayout; 

 /**
 * MyScrollView布局的高度。
 */
 private static int scrollViewHeight; 

 /**
 * 记录上垂直方向的滚动距离。
 */
 private static int lastScrollY = -1; 

 /**
 * 记录所有界面上的图片,用以可以随时控制对图片的释放。
 */
 private List<ImageView> imageViewList = new ArrayList<ImageView>(); 

 /**
 * 在Handler中进行图片可见性检查的判断,以及加载更多图片的操作。
 */
 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;
 // 5毫秒后再次对滚动位置进行判断
 handler.sendMessageDelayed(message, 5);
 }
 }; 

 }; 

 /**
 * MyScrollView的构造函数。
 *
 * @param context
 * @param attrs
 */
 public MyScrollView(Context context, AttributeSet attrs) {
 super(context, attrs);
 imageLoader = ImageLoader.getInstance();
 taskCollection = new HashSet<LoadImageTask>();
 setOnTouchListener(this);
 } 

 /**
 * 进行一些关键性的初始化操作,获取MyScrollView的高度,以及得到第一列的宽度值。并在这里开始加载第一页的图片。
 */
 @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();
 }
 } 

 /**
 * 遍历imageViewList中的每张图片,对图片的可见性进行检查,如果图片已经离开屏幕可见范围,则将图片替换成一张空图。
 */
 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);
 }
 }
 } 

 /**
 * 判断手机是否有SD卡。
 *
 * @return 有SD卡返回true,没有返回false。
 */
 private boolean hasSDCard() {
 return Environment.MEDIA_MOUNTED.equals(Environment
 .getExternalStorageState());
 } 

 /**
 * 异步下载图片的任务。
 *
 * @author guolin
 */
 class LoadImageTask extends AsyncTask<String, Void, Bitmap> { 

 /**
 * 图片的URL地址
 */
 private String mImageUrl; 

 /**
 * 可重复使用的ImageView
 */
 private ImageView mImageView; 

 public LoadImageTask() {
 } 

 /**
 * 将可重复使用的ImageView传入
 *
 * @param imageView
 */
 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);
 } 

 /**
 * 根据传入的URL,对图片进行加载。如果这张图片已经存在于SD卡中,则直接从SD卡里读取,否则就从网络上下载。
 *
 * @param imageUrl
 * 图片的URL地址
 * @return 加载到内存的图片。
 */
 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;
 } 

 /**
 * 向ImageView中添加一张图片
 *
 * @param bitmap
 * 待添加的图片
 * @param imageWidth
 * 图片的宽度
 * @param imageHeight
 * 图片的高度
 */
 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);
 }
 } 

 /**
 * 找到此时应该添加图片的一列。原则就是对三列的高度进行判断,当前高度最小的一列就是应该添加的一列。
 *
 * @param imageView
 * @param imageHeight
 * @return 应该添加图片的一列
 */
 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;
 }
 } 

 /**
 * 将图片下载到SD卡缓存起来。
 *
 * @param imageUrl
 * 图片的URL地址。
 */
 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);
 }
 }
 } 

 /**
 * 获取图片的本地存储路径。
 *
 * @param imageUrl
 * 图片的URL地址。
 * @return 图片的本地存储路径。
 */
 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:orientation="horizontal" > 

 <LinearLayout
 android:id="@+id/first_column"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:layout_weight="1"
 android:orientation="vertical" >
 </LinearLayout> 

 <LinearLayout
 android:id="@+id/second_column"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:layout_weight="1"
 android:orientation="vertical" >
 </LinearLayout> 

 <LinearLayout
 android:id="@+id/third_column"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:layout_weight="1"
 android:orientation="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" />

这样我们所有的编码工作就已经完成了,现在可以尝试运行一下,效果如下图所示:

瀑布流模式的照片墙果真非常美观吧,而且由于我们有非常完善的资源释放机制,不管你在照片墙上添加了多少图片,程序占用内存始终都会保持在一个合理的范围内。在下一篇文章中,我会带着大家对这个程序进行进一步的完善,加入点击查看大图,以及多点触控缩放的功能,感觉兴趣的朋友请继续阅读Android多点触控技术实战,自由地对图片进行缩放和移动 。

源码下载:http://xiazai.jb51.net/201610/yuanma/AndroidPhotoWallFalls(jb51.net).rar

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

(0)

相关推荐

  • android ListView和ProgressBar(进度条控件)的使用方法

    ListView控件的使用:ListView控件里面装的是一行一行的数据,一行中可能有多列,选中一行,则该行的几列都被选中,同时可以触发一个事件,这种控件在平时还是用得很多的.使用ListView时主要是要设置一个适配器,适配器主要是用来放置一些数据.使用起来稍微有些复杂,这里用的是android自带的SimpleAdapter,形式如下:android.widget.SimpleAdapter.SimpleAdapter(Context context, List<? extends Map<

  • android中UIColletionView瀑布流布局实现思路以及封装的实现

    瀑布流实现思路 第一种就是用ScrollView来进行实现,由于它不具备复用的功能,因此我们需要自己写一套类似复用的模块来进行优化 第二种就是利用apple做好的复用模块,自定义UIColletionLayout来实现瀑布流,想想也是第二种实现起来更快更优,OK,封装一个小小的框架来试试 默认两列 其他案例 上面的动画切换布局也是自定义UICollectionLayout来进行布局的,简单的静态图片布局展示其实就重写几个方法就可以了 1.prepareLayout 每次重新刷新collectio

  • Android控件之ListView用法实例详解

    本文实例讲述了Android控件之ListView用法.分享给大家供大家参考.具体如下: 示例一: 在android开发中ListView是比较常用的组件,它以列表的形式展示具体内容,并且能够根据数据的长度自适应显示. main.xml布局文件: <?xml version="1.0" encoding="utf-8"?> <LinearLayout android:id="@+id/LinearLayout01" androi

  • Android RecyclerView详解之实现 ListView GridView瀑布流效果

     什么是RecyclerView RecyclerView 是Google推出的最新的 替代ListView.GridView的组件,RecyclerView是用来显示大量数据的容器,并通过有限数量的子View,来提高滚动时的性能. 与ListView不同,RecyclerView 不再负责布局,而是专注于布局复用.布局主要通过 LayoutManager来管理,目前提供了3种常用的布局管理: LinearLayoutManager 线性布局管理器 (ListView效果) GridLayout

  • Android中Spinner(下拉框)控件的使用详解

    android给我们提供了一个spinner控件,这个控件主要就是一个列表,那么我们就来说说这个控件吧,这个控件在以前的也看见过,但今天还是从新介绍一遍吧. Spinner位于 android.widget包下,每次只显示用户选中的元素,当用户再次点击时,会弹出选择列表供用户选择,而选择列表中的元素同样来自适配器.Spinner是View类得一个子类. 1.效果图 2.创建页面文件(main.xml) <Spinner android:id="@+id/spinner1" and

  • Android下拉刷新上拉加载控件(适用于所有View)

    前面写过一篇关于下拉刷新控件的文章下拉刷新控件终结者:PullToRefreshLayout,后来看到好多人还有上拉加载更多的需求,于是就在前面下拉刷新控件的基础上进行了改进,加了上拉加载的功能.不仅如此,我已经把它改成了对所有View都通用!可以随心所欲使用这两个功能~~ 我做了一个大集合的demo,实现了ListView.GridView.ExpandableListView.ScrollView.WebView.ImageView.TextView的下拉刷新和上拉加载.后面会提供demo的

  • Android开发之瀑布流控件的实现与使用方法示例

    本文实例讲述了Android开发之瀑布流控件的实现与使用方法.分享给大家供大家参考,具体如下: public class FlowLayout extends ViewGroup { /**行里子view之间的行距离*/ public int mHorizontolSpace = Util.getDimen(R.dimen.top_padding); /**行里子view之间的垂直距离*/ public int mVerticalSpace = Util.getDimen(R.dimen.top

  • android 自定义控件 自定义属性详细介绍

    自定义控件在android中无处不见,自定义控件给了我们很大的方便.比如说,一个视图为imageview ,imagebutton ,textview 等诸多控件的组合,用的地方有很多,我们不可能每次都来写3个的组合,既浪费时间,效率又低.在这种情况下,我们就可以自定义一个view来替换他们,不仅提升了效率并且在xml中运用也是相当的美观. 一.控件自定义属性介绍 以下示例中代码均在values/attrs.xml 中定义,属性均可随意命名. 1. reference:参考某一资源ID. 示例:

  • Android控件系列之TextView使用介绍

    学习目的: 1.了解在Android中如何使用TextView控件 2.掌握TextView控件重要属性 作用:TextView类似一般UI中的Label,TextBlock等控件,只是为了单纯的显示一行或多行文本 上图的XML布局如下: 复制代码 代码如下: <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_c

  • Android App中实现相册瀑布流展示的实例分享

    传统界面的布局方式总是行列分明.坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳.这个时候瀑布流布局的出现,就给人带来了耳目一新的感觉,这种布局虽然看上去貌似毫无规律,但是却有一种说不上来的美感,以至于涌现出了大批的网站和应用纷纷使用这种新颖的布局来设计界面. 记得我在之前已经写过一篇关于如何在Android上实现照片墙功能的文章了,但那个时候是使用的GridView来进行布局的,这种布局方式只适用于"墙"上的每张图片大小都相同的情况,如果图片的大小参差不齐,

  • android控件封装 自己封装的dialog控件

    自定义dialog肯定是用的很多了但是感觉每次做都是很乱 单纯完成任务而已,现在封装了一下 以后用到直接copy 先上图: 主activity 复制代码 代码如下: package com.su.testcustomdialog; import com.su.testcustomdialog.MyDialog.Dialogcallback; import android.app.Activity; import android.os.Bundle; import android.view.Vie

随机推荐