您的位置:首页 > 其它

实现一个最简单图片列表所引发的问题

2015-07-21 13:59 597 查看
前一阵看了些Universal-Image-Loager的源码。我觉得看源码很累的一个原因就是除了看怎么实现,就是去揣测为什么这么实现。这个揣测的过程很容易走马观花,看到后面似懂非懂。

人懒到一个地步一句话来说是能躺着就绝对不坐着,能坐着就绝对不蹲着,能蹲着就绝对不站着。有时候看源码也是,能看懂就不会想着去debug,debug能看明白的就懒得去动手写写。
看和写的感受是不一样的。看的是结果,写的是过程。

第三方库的使用让开发变得很方便,大量图片请求的实现,大多数不再是说实现的核心,而是直接说使用什么第三方。即便如此也没关系,只要知道别人是怎么实现的就好了,这很重要。Universal-Image-Loader是一个强大的图片加载开源框架,应该都被说烂用烂了吧。这篇主要是为了去发现问题,从0开始。

看代码的时候很多为什么,为什么要这么写,怎么就想到会有这些问题,怎么就想到用这种方法去解决。想找原因还是从最简单的实现一个图片列表开始找吧。不用任何框架,也不考虑什么缓存,单纯的去写一个网络请求显示图片列表这样一个功能。

新建一个PhotoListActivity,这个类只显示一个listview:

package com.aliao.learninguil.activity;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;

import com.aliao.learninguil.Constants;
import com.aliao.learninguil.R;
import com.aliao.learninguil.adapter.PhotoListAdapter;
import com.aliao.learninguil.entity.ImageInfo;

import java.util.ArrayList;
import java.util.List;

/**
* Created by ALiao on 2015/7/13.
*/
public class PhotoListActivity extends AppCompatActivity {

private ListView mListView;
private PhotoListAdapter mAdapter;
private List<ImageInfo> mImageInfos = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photolist);
for (int i = 0; i< Constants.IMAGES.length; i++){
ImageInfo imageInfo = new ImageInfo();
imageInfo.setUrl(Constants.IMAGES[i]);
imageInfo.setName("item-" + i);
mImageInfos.add(imageInfo);
}
mListView = (ListView) findViewById(R.id.photoList);
mAdapter = new PhotoListAdapter(mImageInfos, mListView);
mListView.setAdapter(mAdapter);
}
}
listview的item的布局是左边显示一个ImageVeiw,右边显示一个textview:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<TextView
android:id="@+id/tv_imagename"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</LinearLayout>


要想在listview的每个item中显示一张网络请求的图片,那么在PhotoListAdapter中要启动线程进行网络请求。

package com.aliao.learninguil.adapter;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.aliao.learninguil.R;
import com.aliao.learninguil.entity.ImageInfo;
import com.aliao.learninguil.utils.L;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

public class PhotoListAdapter extends BaseAdapter {

private List<ImageInfo> imageInfos;
private ListView mListView;

public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) {
this.imageInfos = imageInfos;
mListView = listView;
}

@Override
public int getCount() {
return imageInfos.size();
}

@Override
public Object getItem(int position) {
return imageInfos.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

ViewHolder holder;
if (convertView == null){
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false);
holder = new ViewHolder();
holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img);
holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename);
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}

ImageInfo imageInfo = imageInfos.get(position);
holder.imgName.setText(imageInfo.getName());
holder.imgView.setTag(imageInfo.getUrl());
loadAndSetImage(imageInfo.getUrl());

return convertView;
}

class ViewHolder{
ImageView imgView;
TextView imgName;
}

private void loadAndSetImage(String url) {
new LoadImageAsyncTask().execute(url);
}

class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{

private String mImageUrl;

@Override
protected Bitmap doInBackground(String... params) {

mImageUrl = params[0];

Bitmap bitmap = loadBitmap(mImageUrl);

return bitmap;
}

@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
if (imageView != null){
imageView.setImageBitmap(bitmap);
}
}
}

private Bitmap loadBitmap(String imageUrl) {
HttpURLConnection connection = null;
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (connection != null){
connection.disconnect();
}
}
return bitmap;
}

}
这是实现一个图片列表最基本的代码了。实现的效果图如下:



看起来显示的效果还不错,但是当我们上下滑动的时候明显会感受到不够流畅。

回到PhotoListAdapter类,每调用一次getView方法,就会去启动线程进行网络请求。我们期望的效果是当页面显示5个item的时候,就进行5次网络请求,这是最理想的状态。但是实际上getView调用的次数比预想的要多得多,也就意味着会伴随多余的网络请求。有哪些情况会导致多余的网络请求呢(这里的多余是表示,除了当前屏幕显示的图片以外,进行了额外的其他图片的网络请求)

情况一:没有设置item的高度

listview item的布局文件中看到,并没有去设置ImageView的高度,item的高度是自动扩展的:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<TextView
android:id="@+id/tv_imagename"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</LinearLayout>
所以,当进入图片列表页时,由于还未加载出图片,默认是textview的高度,一屏能够显示了27个item,即调用了27次getView,也就是开启线程进行网络请求的操作就要执行27次。但随着前面图片陆续加载完毕,一屏最终显示5个item,但是剩余的22次网络请求还在继续。通常我们希望一屏显示多少张图片就去请求多少张,当要看更多的图片的时候再去请求。而这22次网络请求既然都执行了,是不是往下滑动的时候他就可以直接显示出已加载完的图片?答案是否定的,因为除了当前屏幕显示的item,其他的item都被回收了,通过findViewWithTag(imageUrl)已经找不到对应的imageView(为null)。即使图片已经请求成功,但是由于当前屏幕没有对应的imageview,也无法设置图片。

@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
if (imageView != null){
imageView.setImageBitmap(bitmap);
}
}
所以继续向下滑的时候,还是会重复请求先前已经请求过的图片。





如果提前已知item的显示高度,例如设置ImageView的高度为100dp或者设置默认图片,那么一屏能够显示的item数量是确定的,就可以做到一屏显示多少个item,进行相应数量的网络请求。

情况二:如果想看第三屏的图片列表,滑过去的前两屏的图片已经进行了网络请求,但并没必要

对于我们不想查看而快速滑过的图片是没有必要浪费资源去加载的。但是由于把加载图片的操作放在了getView()中,只要滑动屏幕都会调用getView(),我们希望当listview滑动停止时再去加载。listview的滚动监听事件的回调函数可以监听到滚动的状态,onScrollStateChanged(AbsListView view, int scrollState)中的scrollState有三种表示listvuew的滚动状态,分别是:

SCROLL_STATE_IDLE( = 0 )  :停止滚动

SCROLL_STATE_TOUCH_SCROLL( = 1 )  :正在滚动

SCROLL_STATE_FLING( = 2 )  :手指做了抛的动作

当scrollState的状态为SCROLL_STATE_IDLE的时候,去下载图片。图片下载的时机确定了,那么当滑动停止时当前屏幕显示的可视图片的地址该怎么获取。imageInfos对象列表存放了所有图片信息,知道了当前屏幕可视图片的position位置,也就可以通过索引获取到图片地址了。另一个滚动监听的回调onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)(滚动时一直回调)帮助我们获取到当前屏幕图片的位置信息。参数中

firstVisibleItem  :当前屏幕第一张可见item的下标(从0开始)

visibleItemCount  :当前屏幕所有可见item的总数 

totalItemCount  :列表项总数

具体的代码实现如下:

package com.aliao.learninguil.adapter;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.aliao.learninguil.R;
import com.aliao.learninguil.entity.ImageInfo;
import com.aliao.learninguil.utils.L;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class PhotoListAdapter extends BaseAdapter implements AbsListView.OnScrollListener{

private List<ImageInfo> imageInfos;
private ListView mListView;
private int mFirstVisibleItem;
private int mVisibleItemCount;
private boolean mFirstEnter;
private Set<LoadImageAsyncTask> taskCollection;

public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) {
this.imageInfos = imageInfos;
mListView = listView;
mListView.setOnScrollListener(this);
mFirstEnter = true;
taskCollection = new HashSet<>();
}

@Override
public int getCount() {
return imageInfos.size();
}

@Override
public Object getItem(int position) {
return imageInfos.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

ViewHolder holder;
if (convertView == null){
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false);
holder = new ViewHolder();
holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img);
holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename);
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}

ImageInfo imageInfo = imageInfos.get(position);
holder.imgName.setText(imageInfo.getName());
holder.imgView.setTag(imageInfo.getUrl());
//        L.d("position = "+position+", url = "+imageInfo.getUrl());
//        loadAndSetImage(imageInfo.getUrl());

return convertView;
}

class ViewHolder{
ImageView imgView;
TextView imgName;
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
/**
* scrollState = SCROLL_STATE_IDLE( = 0 )停止混动
* scrollState = SCROLL_STATE_TOUCH_SCROLL( = 1 )正在滚动
* scrollState = SCROLL_STATE_FLING( = 2 ) 手指做了抛的动作
*/
L.d("-------》onScrollStateChanged scrollState = "+scrollState);
if (scrollState == SCROLL_STATE_IDLE){
loadAndSetImage(mFirstVisibleItem, mVisibleItemCount);
}else {
//当listview再次滑动时取消所有正在下载的任务
//            cancelAllTasks();
}
}

public void cancelAllTasks() {
if (taskCollection != null){
for (LoadImageAsyncTask task : taskCollection){
task.cancel(false);
}
}
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
L.d("onScroll firstVisibleItem = "+firstVisibleItem+", visibleItemCount = "+visibleItemCount+", totalItemCount = "+totalItemCount);
mFirstVisibleItem = firstVisibleItem;//第一张可见图片的下标
mVisibleItemCount = visibleItemCount;//一屏可见图片的总数

//首次进入程序时,onScrollStateChanged方法并不会被调用,所以在这里首次进入程序时启动下载任务
if (mFirstEnter && visibleItemCount > 0){
loadAndSetImage(mFirstVisibleItem, mVisibleItemCount);
mFirstEnter = false;
}

}

private void loadAndSetImage(int firstVisibleItem, int visibleItemCount) {
for (int i = firstVisibleItem; i< firstVisibleItem + visibleItemCount; i++){
ImageInfo imageInfo = imageInfos.get(i);
L.d("position = "+i+", url = "+imageInfo.getUrl());
LoadImageAsyncTask task = new LoadImageAsyncTask();
task.execute(imageInfo.getUrl());
taskCollection.add(task);
}
}

class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{

private String mImageUrl;

@Override
protected Bitmap doInBackground(String... params) {

mImageUrl = params[0];

Bitmap bitmap = loadBitmap(mImageUrl);

return bitmap;
}

@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
if (imageView != null){
imageView.setImageBitmap(bitmap);
}
}
}

private Bitmap loadBitmap(String imageUrl) {
HttpURLConnection connection = null;
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
L.d("------------------------------------loadBitmap ");
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (connection != null){
connection.disconnect();
}
}
return bitmap;
}

}
除了当listview再次滑动的时候,取消所有正在下载的任务,当Activity销毁时,在onDestroy方法中可以调用PhotoListAdapter中的cancelAllTasks()来取消所有还未完成的下载任务。

到目前为止通过确定list item的高度以及对启动图片下载任务的时机的修改,做到了只下载所要查看到的图片,避免多余的网络请求,减少了网络请求的负荷和节省了手机流量。

解决了上面的问题后,代码又完善健壮了一步,想想接下来还会有什么问题。

参考:

使用内存缓存实现图片墙
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: