本文的思路主要是根据郭老师的博客来写的,能够实现,也遇到了一些问题。
郭老师真的非常厉害,可以去看看他相关的其它文章。
http://blog.csdn.net/guolin_blog/article/details/9526203

http://blog.csdn.net/guolin_blog/article/details/10470797

http://blog.csdn.net/guolin_blog/article/details/9316683

郭老师的思路是:用一个GridView控件当作“墙”,然后随着GridView的滚动将一张张照片贴在“墙”上,这些照片可以是手机本地中存储的,也可以是从网上下载的。

制作类似于这种的功能的应用,有一个非常重要的问题需要考虑,就是图片资源何时应该释放。因为随着GridView的滚动,加载的图片可能会越来越多,如果没有一种合理的机制对图片进行释放,那么当图片达到一定上限时,程序就必然会崩溃。

今天我们照片墙应用的实现,重点也是放在了如何防止由于图片过多导致程序崩溃上面。主要的核心算法使用了Android中提供的LruCache类,需要导入android-support-v4的jar包。

Android Studio的项目结构是这样的:


代码类主要分为:activity,adapter适配器和utils类(其实在一般工程开发中,会放在一个常量类中)。
布局为两个,一个activity_main,一个griditem类

先贴上代码吧:
activity_main.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" tools:context=".MainActivity">    <GridView  android:id="@+id/photo_wall" android:layout_width="match_parent" android:layout_height="wrap_content" android:columnWidth="90dip" android:stretchMode="columnWidth" android:numColumns="auto_fit" android:verticalSpacing="10dip" android:gravity="center" ></GridView></LinearLayout>

griditem.xml

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" >    <ImageView  android:id="@+id/photo" android:layout_width="90dip" android:layout_height="90dip" android:src="@mipmap/empty_photo" android:layout_centerInParent="true" /></RelativeLayout>

然后是适配器类,这个是核心所在

PhotoWallAdapter

package com.photowall.adapter;import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.AsyncTask;import android.util.LruCache;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.AbsListView;import android.widget.ArrayAdapter;import android.widget.GridView;import android.widget.ImageView;import com.photowall.R;import com.photowall.utils.Images;import java.net.HttpURLConnection;import java.net.URL;import java.util.HashSet;import java.util.Set;/** * 照片墙适配器,负责异步从网络上下载图片展示在照片墙上。 * Created by Administrator on 2015/8/27. */public class PhotoWallAdapter extends ArrayAdapter<String> implements AbsListView.OnScrollListener{    /** * 记录所有正在下载或等待下载的任务。 */    private Set<BitmapWorkerTask> taskCollection;    /** * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。 */    private LruCache<String, Bitmap> mMemoryCache;    /** * GridView的实例 */    private GridView mPhotoWall;    /** * 第一张可见图片的下标,可看OnScroll方法的参数 */    private int mFirstVisibleItem;    /** * 一屏有多少张图片可见 */    private int mVisibleItemCount;    /** * 记录是否刚打开程序,用于解决进入程序不滚动屏幕,不会下载图片的问题。 */    private boolean isFirstEnter = true;    public PhotoWallAdapter(Context context,  int textViewResourceId, String[] objects,                            GridView photoWall) {        super(context,  textViewResourceId,objects);        mPhotoWall = photoWall;        taskCollection = new HashSet<BitmapWorkerTask>(); // SET不会重复        // 获取应用程序最大可用内存        int maxMemory = (int) Runtime.getRuntime().maxMemory();        // 设置图片缓存大小为程序最大可用内存的1/8        int cacheSize = maxMemory / 8;        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {            @Override            // 返回Bitmap的大小            protected int sizeOf(String key, Bitmap bitmap) {                return bitmap.getByteCount();                // 或者重写此方法,来测量Bitmap的大小                // return bitmap.getRowBytes()*bitmap.getHeight();            }        };        mPhotoWall.setOnScrollListener(this);    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        final String url = getItem(position);        View view;        if (convertView == null) {            view = LayoutInflater.from(getContext()).inflate(R.layout.griditem, null);        } else {            view = convertView;        }        final ImageView photo = (ImageView) view.findViewById(R.id.photo);        // 给ImageView设置一个Tag,保证异步加载图片时不会乱序        photo.setTag(url);        setImageView(url, photo);        return view;        // 老是出错。。。。。        /** if(convertView==null) { // griditem中得到布局 convertView= LayoutInflater.from(parent.getContext()).inflate(R.layout.griditem,null); ViewHolder vh=new ViewHolder(); vh.imageView= (ImageView) convertView.findViewById(R.id.photo); convertView.setTag(vh); } ViewHolder vh= (ViewHolder) convertView.getTag(); final ImageView photo= (ImageView) convertView.findViewById(R.id.photo); // 给ImageView设置一个Tag,保证异步加载图片时不会乱序 photo.setTag(url); setImageView(url, photo); return convertView; */    }    class ViewHolder{        private ImageView imageView;    }    /** * 给ImageView设置图片。首先从LruCache中取出图片的缓存,设置到ImageView上。如果LruCache中没有该图片的缓存, * 就给ImageView设置一张默认图片。 * * @param imageUrl * 图片的URL地址,用于作为LruCache的键。 * @param imageView * 用于显示图片的控件。 */    private void setImageView(String imageUrl, ImageView imageView) {        // 从缓存中获取图片        Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);        //控件设置图片        if (bitmap != null) {            imageView.setImageBitmap(bitmap);        } else {            imageView.setImageResource(R.mipmap.empty_photo);        }    }    /** * 从LruCache中获取一张图片,如果不存在就返回null。 * * @param key * LruCache的键,这里传入图片的URL地址。 * @return 对应传入键的Bitmap对象,或者null。 */    private Bitmap getBitmapFromMemoryCache(String key) {        return mMemoryCache.get(key);    }    /** * 将一张图片存储到LruCache中。 * * @param key * LruCache的键,这里传入图片的URL地址。 * @param bitmap * LruCache的键,这里传入从网络上下载的Bitmap对象。 */    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {        if (getBitmapFromMemoryCache(key) == null) {            mMemoryCache.put(key, bitmap);        }    }    @Override    public void onScrollStateChanged(AbsListView view, int scrollState) {        // 仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务        if (scrollState == SCROLL_STATE_IDLE) {            loadBitmaps(mFirstVisibleItem, mVisibleItemCount);        } else {            cancelAllTasks();        }    }    @Override    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {        mFirstVisibleItem=firstVisibleItem;        mVisibleItemCount=visibleItemCount;        // 下载的任务应该由onScrollStateChanged里调用,但首次进入程序时onScrollStateChanged并不会调用,        // 因此在这里为首次进入程序开启下载任务。        if (isFirstEnter && visibleItemCount > 0) {            loadBitmaps(firstVisibleItem, visibleItemCount);            isFirstEnter = false;        }    }    /** * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageView的Bitmap对象, * 如果发现任何一个ImageView的Bitmap对象不在缓存中,就会开启异步线程去下载图片。 * * @param firstVisibleItem * 第一个可见的ImageView的下标 * @param visibleItemCount * 屏幕中总共可见的元素数 */    private void loadBitmaps(int firstVisibleItem, int visibleItemCount ) {        try{            for(int i=firstVisibleItem;i<firstVisibleItem+visibleItemCount;i++)            {                String imageUrl = Images.imageThumbUrls[i];                Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);                //判断是否在缓存中,不在的话就去下载图片                if (bitmap == null) {                    BitmapWorkerTask task = new BitmapWorkerTask();                    taskCollection.add(task);                    task.execute(imageUrl);                } else {                    //原来imageview设置了tag                    ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);                    if (imageView != null && bitmap != null) {                        imageView.setImageBitmap(bitmap);                    }                }            }        }        catch (Exception e){            e.printStackTrace();        }    }    /** * 取消所有正在下载或等待下载的任务。 */    public void cancelAllTasks() {        if (taskCollection != null) {            for (BitmapWorkerTask task : taskCollection) {                //true和false都行的,false就表示如果线程已经开始,就不去中断,                // 只取消那些还没开始的线程。当然设置成true其实也不一定能中断正在运行的线程,                // java的线程机制就这样。                task.cancel(false);            }        }    }     /** * 异步下载图片的任务。 * 感谢郭神精彩代码 */     class BitmapWorkerTask extends AsyncTask<String,Void,Bitmap>{         private String imageUrl;        @Override        protected Bitmap doInBackground(String... params) {            imageUrl = params[0];            // 在后台开始下载图片            Bitmap bitmap = downloadBitmap(params[0]);            if (bitmap != null) {                // 图片下载完成后缓存到LrcCache中                addBitmapToMemoryCache(params[0], bitmap);            }            return bitmap;        }         @Override         protected void onPostExecute(Bitmap bitmap) {             super.onPostExecute(bitmap);             // 根据Tag找到相应的ImageView控件,将下载好的图片显示出来。             ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);             if (imageView != null && bitmap != null) {                 imageView.setImageBitmap(bitmap);             }             taskCollection.remove(this);         }         /** * 建立HTTP请求,并获取Bitmap对象。 * * @param imageUrl * 图片的URL地址 * @return 解析后的Bitmap对象 */         private Bitmap downloadBitmap(String imageUrl) {             Bitmap bitmap = null;             HttpURLConnection con = null;             try {                 URL url = new URL(imageUrl);                 con = (HttpURLConnection) url.openConnection();                 con.setConnectTimeout(5 * 1000);                 con.setReadTimeout(10 * 1000);                 con.setDoInput(true);                 //换成其它的地址不能显示图片的,你把con.setDoOutput(true)去掉就好了                 con.setDoOutput(true);                 bitmap = BitmapFactory.decodeStream(con.getInputStream());             } catch (Exception e) {                 e.printStackTrace();             } finally {                 if (con != null) {                     con.disconnect();                 }             }             return bitmap;         }     }}

PhotoWallAdapter是整个照片墙程序中最关键的一个类了,这里我来重点给大家讲解一下。首先在PhotoWallAdapter的构造函数中,我们初始化了LruCache类,并设置了最大缓存容量为程序最大可用内存的1/8,接下来又为GridView注册了一个滚动监听器。然后在getView()方法中,我们为每个ImageView设置了一个唯一的Tag,这个Tag的作用是为了后面能够准确地找回这个ImageView,不然异步加载图片会出现乱序的情况。之后调用了setImageView()方法为ImageView设置一张图片,这个方法首先会从LruCache缓存中查找是否已经缓存了这张图片,如果成功找到则将缓存中的图片显示在ImageView上,否则就显示一张默认的空图片。

看了半天,那到底是在哪里下载图片的呢?这是在GridView的滚动监听器中进行的,在onScrollStateChanged()方法中,我们对GridView的滚动状态进行了判断,如果当前GridView是静止的,则调用loadBitmaps()方法去下载图片,如果GridView正在滚动,则取消掉所有下载任务,这样可以保证GridView滚动的流畅性。在loadBitmaps()方法中,我们为屏幕上所有可见的GridView子元素开启了一个线程去执行下载任务,下载成功后将图片存储到LruCache当中,然后通过Tag找到相应的ImageView控件,把下载好的图片显示出来。

由于我们使用了LruCache来缓存图片,所以不需要担心内存溢出的情况,当LruCache中存储图片的总大小达到容量上限的时候,会自动把最近最少使用的图片从缓存中移除。

还有Images为一长串的图片网址,就简单贴一部分了

Images类

package com.photowall.utils;/** * Created by Administrator on 2015/8/27. */public class Images {    public final static String[] imageThumbUrls = new String[] {            "http://my.csdn.net/uploads/avatar/1/1/1/1_linshijun33.jpg",            "https://lh4.googleusercontent.com/-zAvf__52ONk/URqutT_IuxI/AAAAAAAAAbs/D_bcuc0thoU/s160-c/Highway%2525201.jpg",            "https://lh6.googleusercontent.com/-H4SrUg615rA/URquuL27fXI/AAAAAAAAAbs/4aEqJfiMsOU/s160-c/Horseshoe%252520Bend%252520Sunset.jpg",            "https://lh4.googleusercontent.com/-JhFi4fb_Pqw/URquuX-QXbI/AAAAAAAAAbs/IXpYUxuweYM/s160-c/Horseshoe%252520Bend.jpg",            "https://lh5.googleusercontent.com/-UGgssvFRJ7g/URquueyJzGI/AAAAAAAAAbs/yYIBlLT0toM/s160-c/Into%252520the%252520Blue.jpg",            "https://lh3.googleusercontent.com/-CH7KoupI7uI/URquu0FF__I/AAAAAAAAAbs/R7GDmI7v_G0/s160-c/Jelly%252520Fish%2525202.jpg",            "https://lh4.googleusercontent.com/-pwuuw6yhg8U/URquvPxR3FI/AAAAAAAAAbs/VNGk6f-tsGE/s160-c/Jelly%252520Fish%2525203.jpg",            "https://lh5.googleusercontent.com/-GoUQVw1fnFw/URquv6xbC0I/AAAAAAAAAbs/zEUVTQQ43Zc/s160-c/Kauai.jpg",            "https://lh6.googleusercontent.com/-8QdYYQEpYjw/URquwvdh88I/AAAAAAAAAbs/cktDy-ysfHo/s160-c/Kyoto%252520Sunset.jpg", };}

mainactivity

package com.photowall.activity;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.Menu;import android.view.MenuItem;import android.widget.GridView;import com.photowall.R;import com.photowall.adapter.PhotoWallAdapter;import com.photowall.utils.Images;public class MainActivity extends AppCompatActivity {    /** * 用于展示照片墙的GridView */    private GridView mPhotoWall;    /** * GridView的适配器 */    private PhotoWallAdapter adapter;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mPhotoWall = (GridView) findViewById(R.id.photo_wall);        adapter = new PhotoWallAdapter(this, 0, Images.imageThumbUrls, mPhotoWall);        mPhotoWall.setAdapter(adapter);    }    protected void onDestroy() {        super.onDestroy();        //退出程序时结束所有的下载任务        adapter.cancelAllTasks();    }    @Override    public boolean onCreateOptionsMenu(Menu menu) {        // Inflate the menu; this adds items to the action bar if it is present.        getMenuInflater().inflate(R.menu.menu_main, menu);        return true;    }    @Override    public boolean onOptionsItemSelected(MenuItem item) {        // Handle action bar item clicks here. The action bar will        // automatically handle clicks on the Home/Up button, so long        // as you specify a parent activity in AndroidManifest.xml.        int id = item.getItemId();        //noinspection SimplifiableIfStatement        if (id == R.id.action_settings) {            return true;        }        return super.onOptionsItemSelected(item);    }}

以上就是郭老师的程序,他给出的效果图是很棒的,然而我自己写了,运行只成功了两次,效果和老师给的效果一模一样。

后来不知道是怎么回事,就一直加载不上图片。

1.第一感觉图片被墙了,然而换成自己的图片也是不能成功,添加了几个国内的图片路径也是无法成功。是什么原因呢,也比较困惑,不知道错误在哪个地方。将con.setDoOutput(false)加上了也是没解决。

后来看了一下for循环

for(int i=firstVisibleItem;i<firstVisibleItem + visibleItemCount;i++)            {                String imageUrl = Images.imageThumbUrls[i];                Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);                //判断是否在缓存中,不在的话就去下载图片                if (bitmap == null) {                    BitmapWorkerTask task = new BitmapWorkerTask();                    taskCollection.add(task);                    task.execute(imageUrl);                } else {                    //原来imageview设置了tag                    ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);                    if (imageView != null && bitmap != null) {                        imageView.setImageBitmap(bitmap);                    }                }            }

大家都知道不能在主线程中使用进行耗时操作是这样解释的:
The BitmapFactory.decode* methods, discussed in the Load Large Bitmaps Efficiently lesson, should not be executed on the main UI thread if the source data is read from disk or a network location (or really any source other than memory). The time this data takes to load is unpredictable and depends on a variety of factors (speed of reading from disk or network, size of image, power of CPU, etc.). If one of these tasks blocks the UI thread, the system flags your application as non-responsive and the user has the option of closing it (see Designing for Responsiveness for more information).

This lesson walks you through processing bitmaps in a background thread using AsyncTask and shows you how to handle concurrency issues.

照片墙给了太多的异步task,都是挺耗资源的。如果虚拟机内存等等方面的因素限制了的话,很可能会出现阻塞ui更新或者是假死情况。。
用AsyncTask下载图片是好,又异步,下完后还可以回到ui线程来加载图片。但Android限制了能同时运行的AsyncTask的数量,这个可能有隐患。
当然要是用回线程,也有问题,一个是多个同时运行,这个得用线程池
第二个是要再回UI线程来设置图片

缓存用LruCache蛮不错的。但也可以考虑两层缓存:内存一个缓存,SD卡一个缓存(当然是要用Sd卡才行)。内存的缓存也可以建立两层,一个是强引用,一个是软引用。这样三层缓存能帮助不少

2.这个程序也是可能出现oom风险的,因为在downloadBitmap(String imageUrl) 方法中,直接进行BitmapFactory.decodeStream(con.getInputStream()); ,如果是很大的图片,肯定会OOM的!这是比较小的图片,所以不需要担心。
还有一个是gridview和listview机制问题,如果一个child view触发了任务,但它所对应的view尚未做好准备(循环机制)。导致的后果就是很多任务都在执行,但效率极低。
if each child view triggers an AsyncTask, there is no guarantee that when it completes, the associated view has not already been recycled for use in another child view. Furthermore, there is no guarantee that the order in which asynchronous tasks are started is the order that they complete.

3.加载失败的图片,并不没有设置标识,直有滚动后静止时才会重新加载,但造成的后果是可能一直加载不上。因为在线程中已经做了太多事情,虽然用到了异步线程。如上面的分析。

整理一下郭老师的程序思路:
初始化

瀑布流:

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

ImageLoader参考官方写法,主要是为了解决图片缓存的问题。
http://developer.android.com/training/displaying-bitmaps/index.html

MyScrollView是实现瀑布流照片墙的核心类,继承自ScrollView,这样允许用户可以通过滚动的方式来浏览更多的图片。这里提供了一个loadMoreImages()方法,是专门用于加载下一页的图片的,因此在onLayout()方法中我们要先调用一次这个方法,以初始化第一页的图片。然后在onTouch方法中每当监听到手指离开屏幕的事件,就会通过一个handler来对当前ScrollView的滚动状态进行判断,如果发现已经滚动到了最底部,就会再次调用loadMoreImages()方法去加载下一页的图片。

另外,为了保证照片墙上的图片都能够合适地被回收,这里还加入了一个可见性检查的方法,即checkVisibility()方法。这个方法的核心思想就是检查目前照片墙上的所有图片,判断出哪些是可见的,哪些是不可见。然后将那些不可见的图片都替换成一张空图,这样就可以保证程序始终不会占用过高的内存。当这些图片又重新变为可见的时候,只需要再从LruCache中将这些图片重新取出即可。如果某张图片已经从LruCache中被移除了,就会开启一个LoadImageTask,将这张图片重新加载到内存中。感觉这才是解决OOM的直接方法。

郭老师真的非常厉害,我再去看看他相关的其它文章。
http://blog.csdn.net/guolin_blog/article/details/9526203

http://blog.csdn.net/guolin_blog/article/details/10470797

http://blog.csdn.net/guolin_blog/article/details/9316683

更多相关文章

  1. django返回json的几种方法以及android调用
  2. Android面试题集锦(二)
  3. Android(安卓)P实现静默安装的方法示例(官方Demo)
  4. Android异步处理类AsyncTask
  5. Android(安卓)UI绘制流程之测量篇
  6. android对大图片压缩的方法
  7. android访问静态页面,出现405错误解决方法
  8. cocos2dx 玩转震动
  9. Android(安卓)开发

随机推荐

  1. Android异步处理二:使用AsyncTask异步更新
  2. Android(安卓)TextView 文字居中
  3. Android——SharedPreferences
  4. android中的数据库——学习
  5. Android(安卓)shell 系统命令
  6. android常用框架
  7. 【Android(安卓)Developer Blog】Android
  8. Android: Linear Layout and weight
  9. Android之MotionLayout(三),用 MotionLay
  10. ToggleButton