我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

传统界面的布局方式总是行列分明、坐落有序的,这种布局已是司空见惯,在不知不觉中大家都已经对它产生了审美疲劳。这个时候瀑布流布局的出现,就给人带来了耳目一新的感觉,这种布局虽然看上去貌似毫无规律,但是却有一种说不上来的美感,以至于涌现出了大批的网站和应用纷纷使用这种新颖的布局来设计界面。
记得我在之前已经写过一篇关于如何在Android上实现照片墙功能的文章了,但那个时候是使用的GridView来进行布局的,这种布局方式只适用于“墙”上的每张图片大小都相同的情况,如果图片的大小参差不齐,在GridView中显示就会非常的难看。而使用瀑布流的布局方式就可以很好地解决这个问题,因此今天我们也来赶一下潮流,看看如何在Android上实现瀑布流照片墙的功能。
首先还是讲一下实现原理,瀑布流的布局方式虽然看起来好像排列的很随意,其实它是有很科学的排列规则的。整个界面会根据屏幕的宽度划分成等宽的若干列,由于手机的屏幕不是很大,这里我们就分成三列。每当需要添加一张图片时,会将这张图片的宽度压缩成和列一样宽,再按照同样的压缩比例对图片的高度进行压缩,然后在这三列中找出当前高度最小的一列,将图片添加到这一列中。之后每当需要添加一张新图片时,都去重复上面的操作,就会形成瀑布流格局的照片墙,示意图如下所示。

201648235457901.png (347×459)

听我这么说完后,你可能会觉得瀑布流的布局非常简单嘛,只需要使用三个LinearLayout平分整个屏幕宽度,然后动态地addView()进去就好了。确实如此,如果只是为了实现功能的话,就是这么简单。可是别忘了,我们是在手机上进行开发,如果不停地往LinearLayout里添加图片,程序很快就会OOM。因此我们还需要一个合理的方案来对图片资源进行释放,这里仍然是准备使用LruCache算法,这个具体的在文后会专门讲,先知道是用这么回事~
下面我们就来开始实现吧,新建一个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; 
  // 设置图片缓存大小为程序最大可用内存的1/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) { 
  // 第一次解析将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; 
  
 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; 
    // 5毫秒后再次对滚动位置进行判断 
    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: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" /> 

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

201648235542999.gif (235×418)

LruCache图片缓存技术
在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
你的设备可以为每个应用程序分配多大的内存?
设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
图片的尺寸和大小,还有每张图片会占据多少内存空间。
图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。
下面是一个使用 LruCache 来缓存图片的例子:


private LruCache<String, Bitmap> mMemoryCache; 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
  // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 
  // LruCache通过构造函数传入缓存值,以KB为单位。 
  int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 
  // 使用最大可用内存值的1/8作为缓存的大小。 
  int cacheSize = maxMemory / 8; 
  mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { 
    @Override 
    protected int sizeOf(String key, Bitmap bitmap) { 
      // 重写此方法来衡量每张图片的大小,默认返回图片数量。 
      return bitmap.getByteCount() / 1024; 
    } 
  }; 
} 
public void addBitmapToMemoryCache(String key, Bitmap bitmap) { 
  if (getBitmapFromMemCache(key) == null) { 
    mMemoryCache.put(key, bitmap); 
  } 
} 
public Bitmap getBitmapFromMemCache(String key) { 
  return mMemoryCache.get(key); 
} 

在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。


public void loadBitmap(int resId, ImageView imageView) { 
  final String imageKey = String.valueOf(resId); 
  final Bitmap bitmap = getBitmapFromMemCache(imageKey); 
  if (bitmap != null) { 
    imageView.setImageBitmap(bitmap); 
  } else { 
    imageView.setImageResource(R.drawable.image_placeholder); 
    BitmapWorkerTask task = new BitmapWorkerTask(imageView); 
    task.execute(resId); 
  } 
} 

BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。


class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { 
  // 在后台加载图片。 
  @Override 
  protected Bitmap doInBackground(Integer... params) { 
    final Bitmap bitmap = decodeSampledBitmapFromResource( 
        getResources(), params[0], 100, 100); 
    addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); 
    return bitmap; 
  } 
} 

掌握了以上两种方法,不管是要在程序中加载超大图片,还是要加载大量图片,都不用担心OOM的问题了!

您可能感兴趣的文章:Android瀑布流照片墙实现 体验不规则排列的美感Android RecyclerView详解之实现 ListView GridView瀑布流效果android中UIColletionView瀑布流布局实现思路以及封装的实现android控件封装 自己封装的dialog控件android 自定义控件 自定义属性详细介绍Android中Spinner(下拉框)控件的使用详解Android下拉刷新上拉加载控件(适用于所有View)Android控件系列之TextView使用介绍android ListView和ProgressBar(进度条控件)的使用方法Android控件之ListView用法实例详解Android开发之瀑布流控件的实现与使用方法示例


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

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

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

Android App中实现图片异步加载的实例分享

一、概述 一般大量图片的加载,比如GridView实现手机的相册功能,一般会用到LruCache,线程池,任务队列等;那么异步消息处理可以用哪呢? 1、用于UI线程当Bitmap加载完成后更新ImageView 2、在图片加载类初始化时,我
2022-06-06

Android自定义View设定到FrameLayout布局中实现多组件显示的方法 分享

如果想在自定义的View上面显示Button 等View组件需要完成如下任务1.在自定义View的类中覆盖父类的构造(注意是2个参数的) 代码如下:  public class MyView2 extends View{public MyV
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录