昨天参加一个面试,面试官让当场写一个类似于新闻列表的页面,文本数据和图片都从网络上获取,想起我还没写过ListView异步加载图片并实现图文混排效果的文章,so,今天就来写一下,介绍一下经验。
ListView加载文本数据都是很简单的,即使是异步获取文本数据。但是异步加载图片就稍微有一点麻烦,既要获得一个比较好的用户体验,还要防止出现图片错位等各种不良BUG,其实要考虑的东西还是挺多的。好了,我们先来看一下我们今天要实现的一个效果图:
看起来似乎并不难,确实,我们今天的核心问题只有一个,就是怎么异步加载图片,并且没有违和感。
好了,废话不多说,先来看主布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.listview.MainActivity" > <ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="match_parent" > </ListView> </RelativeLayout>
主布局中就一个listview,看看listview的item布局文件:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="100dp" > <ImageView android:id="@+id/iv" android:layout_width="80dp" android:layout_height="90dp" android:layout_centerVertical="true" android:padding="5dp" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_toRightOf="@id/iv" android:gravity="center_vertical" android:text="人社部:养老转移已有初稿" android:textSize="14sp" android:textStyle="bold" /> <TextView android:id="@+id/summary" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/title" android:layout_marginTop="8dp" android:layout_toRightOf="@id/iv" android:text="人社部:养老转移已有初稿" android:textSize="12sp" /> </RelativeLayout>
这个布局和我们上图描述的一样,左边一个ImageView,右边是两个TextView,这些都不难,我们看看MainActivity:
public class MainActivity extends Activity { private ListView lv; private List<News> list; private String HTTPURL = "http://litchiapi.jstv.com/api/GetFeeds?column=3&PageSize=20&pageIndex=1&val=100511D3BE5301280E0992C73A9DEC41"; private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case 0: MyAdaper adapter = new MyAdaper(list); lv.setAdapter(adapter); break; default: break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv = (ListView) this.findViewById(R.id.lv); initData(); } private void initData() { list = new ArrayList<News>(); OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(HTTPURL).build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onResponse(Response response) throws IOException { try { JSONObject jo1 = new JSONObject(response.body().string()); JSONObject jo2 = jo1.getJSONObject("paramz"); JSONArray ja = jo2.getJSONArray("feeds"); News news = null; for (int i = 0; i < ja.length(); i++) { JSONObject data = ja.getJSONObject(i).getJSONObject( "data"); String imageUrl = "http://litchiapi.jstv.com" + data.getString("cover"); String title = data.getString("subject"); String summary = data.getString("summary"); news = new News(imageUrl, title, summary); list.add(news); } } catch (JSONException e) { e.printStackTrace(); } mHandler.obtainMessage(0).sendToTarget(); } @Override public void onFailure(Request arg0, IOException arg1) { } }); } }
在onCreate方法中,我们先拿到一个ListView的实例,然后就是初始化数据,这里初始化数据我们使用了OKHttp,关于OKHttp的使用可以查看我之前的文章OKHttp的简单使用,我们拿到一串json数据,至于json里边的结构是怎么样的,我就不多说了,大家可以直接在浏览器中打开上面的地址,这样就能看到json数据了,我们把我们需要的数据封装成一个JavaBean,其中ImageView我们先存储一个url地址,然后在Adapter中通过这个url地址异步加载图片。json解析就不多说了,我们瞅一眼这个Bean:
public class News { private String imageUrl; private String title; private String summary; public String getImageUrl() { return imageUrl; } public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } public News(String imageUrl, String title, String summary) { this.imageUrl = imageUrl; this.title = title; this.summary = summary; } public News() { } }
好了,到这里,所有的东西都还是基本用法,下面我们先不急着看Adapter,先来看看Google给我们提供的一个缓存机制,在android-support-v4.jar包中,Google提供了这样一个类LruCache,这个LruCache的使用和Java中的Map用法差不多,甚至你就可以把它当作Map来使用,不同的是LruCache中的Value可以是一张图片。如果我们缓存的图片太多,超出了我们设置的缓存大小,那么系统会自动移除我们在最近使用比较少的图片。好了,我们来看看LruCache的定义:
private LruCache<String, BitmapDrawable> mImageCache;
每个图片的缓存的key我们就使用该图片的url(这个是唯一的),value就是一张我们要缓存的图片,在实例化LruCache的时候,我们需要传入一个参数,表明我们可以使用的最大缓存,这个缓存参数我们传入可用缓存的1/8,同时我们需要重写sizeOf方法,查看源码我们可以知道,如果不重写sizeOf方法,它默认返回的是图片的数量,但是我们实际上是需要计算图片大小来判断当前已经使用的缓存是否已经超出界限,所以我们这里重写sizeOf方法,返回每张图片的大小。代码如下:
int maxCache = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxCache / 8; mImageCache = new LruCache<String, BitmapDrawable>(cacheSize) { @Override protected int sizeOf(String key, BitmapDrawable value) { return value.getBitmap().getByteCount(); } };
从LruCache中读取一张图片的方式和从Map中取值是一样的:
mImageCache.get(key)
向LruCache中存储一张图片:
mImageCache.put(key, bitmapDrawable);
关于LruCache的基本用法就说这些,这已经够我们后面使用了,现在我就大概说说我们的一个思路,当我们要给ImageView设置图片的时候,就先在本地缓存中查看是否有该图片,有的话,直接从本地读取,没有的话就从网络请求,同时,在从网络请求图片的时候,为了防止发生图片错位的情况,我们要给每一个item的每一个ImageView设置一个tag,这个tag就使用该ImageView要加载的图片的url(这样就可以确保每一个ImageView唯一),在给ImageView设置图片的时候我们就可以通过这个tag找到我们需要的ImageView,这样可以有效避免图片错位的问题。好了,看代码:
public class MyAdaper extends BaseAdapter { private List<News> list; private ListView listview; private LruCache<String, BitmapDrawable> mImageCache; public MyAdaper(List<News> list) { super(); this.list = list; int maxCache = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxCache / 8; mImageCache = new LruCache<String, BitmapDrawable>(cacheSize) { @Override protected int sizeOf(String key, BitmapDrawable value) { return value.getBitmap().getByteCount(); } }; } @Override public int getCount() { return list.size(); } @Override public Object getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (listview == null) { listview = (ListView) parent; } ViewHolder holder = null; if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()).inflate( R.layout.listview_item, null); holder = new ViewHolder(); holder.iv = (ImageView) convertView.findViewById(R.id.iv); holder.title = (TextView) convertView.findViewById(R.id.title); holder.summary = (TextView) convertView.findViewById(R.id.summary); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } News news = list.get(position); holder.title.setText(news.getTitle()); holder.summary.setText(news.getSummary()); holder.iv.setTag(news.getImageUrl()); // 如果本地已有缓存,就从本地读取,否则从网络请求数据 if (mImageCache.get(news.getImageUrl()) != null) { holder.iv.setImageDrawable(mImageCache.get(news.getImageUrl())); } else { ImageTask it = new ImageTask(); it.execute(news.getImageUrl()); } return convertView; } class ViewHolder { ImageView iv; TextView title, summary; } class ImageTask extends AsyncTask<String, Void, BitmapDrawable> { private String imageUrl; @Override protected BitmapDrawable doInBackground(String... params) { imageUrl = params[0]; Bitmap bitmap = downloadImage(); BitmapDrawable db = new BitmapDrawable(listview.getResources(), bitmap); // 如果本地还没缓存该图片,就缓存 if (mImageCache.get(imageUrl) == null) { mImageCache.put(imageUrl, db); } return db; } @Override protected void onPostExecute(BitmapDrawable result) { // 通过Tag找到我们需要的ImageView,如果该ImageView所在的item已被移出页面,就会直接返回null ImageView iv = (ImageView) listview.findViewWithTag(imageUrl); if (iv != null && result != null) { iv.setImageDrawable(result); } } /** * 根据url从网络上下载图片 * * @return */ private Bitmap downloadImage() { HttpURLConnection con = null; Bitmap bitmap = null; try { URL url = new URL(imageUrl); con = (HttpURLConnection) url.openConnection(); con.setConnectTimeout(5 * 1000); con.setReadTimeout(10 * 1000); bitmap = BitmapFactory.decodeStream(con.getInputStream()); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (con != null) { con.disconnect(); } } return bitmap; } } }好了,listview图文混排就说到这里,有问题欢迎留言讨论。