听我这么说完后,你可能会觉得瀑布流的布局非常简单嘛,只需要使用三个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/8mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected 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,以及对图片进行压缩和读取。
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的高度,以及得到第一列的宽度值。并在这里开始加载第一页的图片。*/@Overrideprotected 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();}} /*** 监听用户的触屏事件,如果用户手指离开屏幕则开始进行滚动检测。*/@Overridepublic 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;} @Overrideprotected Bitmap doInBackground(String... params) {mImageUrl = params[0];Bitmap imageBitmap = imageLoader .getBitmapFromMemoryCache(mImageUrl);if (imageBitmap == null) {imageBitmap = loadImage(mImageUrl);}return imageBitmap;} @Overrideprotected 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()方法去加载下一页的图片。
<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" > <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal" > <LinearLayoutandroid:id="@+id/first_column"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical" ></LinearLayout> <LinearLayoutandroid:id="@+id/second_column"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical" ></LinearLayout> <LinearLayoutandroid: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里添加图片了。
<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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。