Android之ListView异步加载图片且仅显示可见子项中的图片

时间:2024-01-10 20:03:20

折腾了好多天,遇到 N 多让人崩溃无语的问题,不过今天终于有些收获了,这是实验的第一版,有些混乱,下一步进行改造细分,先把代码记录在这儿吧。

网上查了很多资料,发现都千篇一律,抄来抄去,很多细节和完整实例都没看到,只有自己一点点研究了,总体感觉 android 下面要显示个图片真不容易啊。

项目主要实现的功能:

  1. 异步加载图片
  2. 图片内存缓存、异步磁盘文件缓存
  3. 解决使用 viewHolder 后出现的图片错位问题
  4. 优化列表滚动性能,仅显示可见子项中的图片
  5. 无需固定图片显示高度,对高度进行缓存使列表滚动时不会因图片高度变化而闪动,使滚动体验更加流畅
  6. 图片动画展示效果,新加载的图片显示透明渐变动画

没有涉及到下拉加载和刷新数据,目前还没接触到这些,而且已发现自定义 ListView 中如果有添加 顶部和底部 的拉动加载更多数据提示的 view ,将会导致 ListView 的 child 数量和 position 混乱,所以只有先简单使用 ListView 来做个效果。

核心主要是三个文件:MainActivity.java,  ZAsyncImageLoader.java, DiaryListAdapter.java

下面贴代码:

MainActivity.java

package com.ai9475.meitian;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator; import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.util.JsonReader;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast; import com.ai9475.meitian.adapter.DiaryListAdapter;
import com.ai9475.util.ZAsyncImageLoader;
import com.ai9475.util.ZHttpRequest;
import com.ai9475.util.ZLog;
import com.ai9475.widget.PullToRefreshListView; import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.protocol.HTTP;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONStringer;
import org.w3c.dom.Text; public class MainActivity extends ActionBarActivity
{
private static final String TAG = "MainActivity"; private ListView mDiaryListView; private DiaryListAdapter mDiaryListAdapter; private ZAsyncImageLoader mAsyncImageLoader; private Handler handler = new Handler(); private int endId = 0; private boolean isScrolling = false; @Override
protected void onCreate(Bundle savedInstanceState)
{
Log.d("main activity", "start");
// 执行父级初始化方法
super.onCreate(savedInstanceState);
// 让 ActionBar 浮动在 Activity 上方进行半透明遮盖
//this.supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
// 解析视图数据
this.setContentView(R.layout.activity_main);
AppManager.getInstance().addActivity(this); this.mAsyncImageLoader = new ZAsyncImageLoader();
this.mAsyncImageLoader.setIsUseDiskCache(true);
this.mAsyncImageLoader.setCacheDir(AppConfig.IMAGE_CACHE_PATH); // 配置 ActionBar 相关
final ActionBar bar = this.getSupportActionBar();
// 标题
bar.setTitle("Bar");
// 返回按钮
//bar.setDisplayHomeAsUpEnabled(true);
// 应用徽标控制
//bar.setDisplayUseLogoEnabled(false);
// 应用图标控制
//bar.setDisplayShowHomeEnabled(true);
// 标题栏控制
//bar.setDisplayShowTitleEnabled(true);
// 设置 TABS 导航模式
bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
/*
bar.getHeight();
final ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
ViewTreeObserver scvto = scrollView.getViewTreeObserver();
if (scvto != null) {
scvto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
scrollView.setPadding(
scrollView.getPaddingLeft(),
bar.getHeight(),
scrollView.getPaddingRight(),
scrollView.getPaddingBottom()
);
return true;
}
});
}*/
/*Fragment fragmentA = new FragmentTab();
Fragment fragmentB = new FragmentTab();
Fragment fragmentC = new FragmentTab(); tabA.setTabListener(new MyTabsListener(fragmentA));
tabB.setTabListener(new MyTabsListener(fragmentB));
tabC.setTabListener(new MyTabsListener(fragmentC));*/ bar.addTab(bar.newTab().setText("ATab").setTabListener(new MyTabsListener()));
bar.addTab(bar.newTab().setText("BTab").setTabListener(new MyTabsListener()));
bar.addTab(bar.newTab().setText("CTab").setTabListener(new MyTabsListener())); /*//bar.setDisplayShowHomeEnabled(false);
//bar.setDisplayShowTitleEnabled(false);
// 顶部帧布局操作栏
final FrameLayout topActBar = (FrameLayout) findViewById(R.id.topActionBar);
// 底部帧布局操作栏
final FrameLayout bottomActBar = (FrameLayout) findViewById(R.id.bottomActionBar);
// 列表滚动视图
final ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
// 顶部操作栏绑定事件:同步设置滚动视图顶部内边距
topActBar
.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
scrollView.setPadding(
scrollView.getPaddingLeft(),
topActBar.getHeight(),
scrollView.getPaddingRight(),
scrollView.getPaddingBottom()
);
return true;
}
});
// 底部操作栏绑定事件:同步设置滚动视图底部内边距
bottomActBar
.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
scrollView.setPadding(
scrollView.getPaddingLeft(),
scrollView.getPaddingTop(),
scrollView.getPaddingRight(),
bottomActBar.getHeight()
);
return true;
}
}); */ //AppContext context = (AppContext) getApplicationContext();
//context.test();
/*
ZAsyncImageLoader loader = new ZAsyncImageLoader();
String url1 = "http://img.ai9475.com/data/attachment/images/meitian/c5/e4/59/c5e459f00dce21480c9941eefbb88f90_200.jpg";
String url2 = "http://img.ai9475.com/data/attachment/images/meitian/f9/29/ee/f929ee1dd6af7b805744b9fb3f4f99b5_200.jpg";
loader.loadDrawable(url1, new ZAsyncImageLoader.OnImageLoadListener() {
@Override
public void onLoaded(Drawable imageDrawable, String imageUrl) {
ImageView img = (ImageView) findViewById(R.id.showPic1);
img.setImageDrawable(imageDrawable);
}
});
loader.loadDrawable(url2, new ZAsyncImageLoader.OnImageLoadListener() {
@Override
public void onLoaded(Drawable imageDrawable, String imageUrl) {
ImageView img = (ImageView) findViewById(R.id.showPic2);
img.setImageDrawable(imageDrawable);
}
});*/
// 找到日记列表视图对象
this.mDiaryListView = (ListView) findViewById(R.id.diaryListCt);
new Thread(){
@Override
public void run() {
Runnable runnable = new Runnable() {
@Override
public void run() {
loadDiaryListData();
}
};
handler.post(runnable);
}
}.start();
} /**
* 日记列表初始化
*/
protected void initDiaryList(JSONArray diaryList)
{
Log.d("initDiaryList", "start");
// 列表单元与数据的适配器生成
this.mDiaryListAdapter = new DiaryListAdapter(this, this.mDiaryListView, this.mAsyncImageLoader, diaryList);
// 绑定列表数据单元适配器
Log.d("initDiaryList", "setAdapter");
this.mDiaryListView.setAdapter(this.mDiaryListAdapter);
Log.d("bindListViewEvents", "start");
// 绑定日记列表事件
this.bindListViewEvents();
Log.d("DiaryListAdapter", "end");
} static int j = 0;
/**
* 绑定日记列表事件
*/
public void bindListViewEvents()
{
// 列表滚动事件
this.mDiaryListView.setOnScrollListener(new AbsListView.OnScrollListener(){
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState)
{
switch (scrollState) {
case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
ZLog.i(TAG, "OnScrollListener : SCROLL_STATE_TOUCH_SCROLL");
mDiaryListAdapter.setIsSCrolling(true);
break;
case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
ZLog.i(TAG, "OnScrollListener : SCROLL_STATE_FLING");
mDiaryListAdapter.setIsSCrolling(true);
break;
case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
// 第一个可见 item 的 position
int first = mDiaryListView.getFirstVisiblePosition();
// 最后一个可见 item 的 position
int last = mDiaryListView.getLastVisiblePosition();
// 屏幕上可见 item 的总数
int onScreenCount = mDiaryListView.getChildCount();
int total = first + last;
ZLog.i(TAG, "OnScrollListener : SCROLL_STATE_IDLE => " + (j++) +", first: "+ first +", last: "+ last +", total: "+ total +", onScreenCount:"+ onScreenCount);
mDiaryListAdapter.setIsSCrolling(false);
mDiaryListAdapter.setPositionRange(first, last);
View child;
int position;
for (int i = 0; i < onScreenCount; i++) {
position = first + i;
if (mDiaryListAdapter.isInPrevPositionRange(position)) {
ZLog.i(TAG, "inPrevPositionRange position:"+ position);
continue;
}
// 获取可见 item 子项的视图容器对象
child = mDiaryListView.getChildAt(i);
ImageView picPhoto = (ImageView) child.findViewById(R.id.picPhoto);
ImageView avatar = (ImageView) child.findViewById(R.id.avatar);
try {
ZLog.i(TAG, "load image i:"+ first);
mDiaryListAdapter.loadImage(picPhoto, avatar, mDiaryListAdapter.getItem(position));
} catch (JSONException e) {
AppException.io(e);
}
}
break;
default:
break;
}
} @Override
public void onScroll(AbsListView absListView, int first, int last, int total) {
//mDiaryListAdapter.setPositionLimit(first, last);
//ZLog.i(TAG, "OnScrollListener : onScroll => " + (j++) +", first: "+ first +", last: "+ last +", total:"+ total);
}
}); // 列表单元点击事件
ZLog.i(TAG, "diaryListInit : setOnItemClickListener");
this.mDiaryListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
getSupportActionBar().setTitle("点击了: "+ i);
}
}); ZLog.i("DiaryListAdapter", "setOnRefreshListener");
// 当向下拉动刷新时触发列表更新事件
/*this.mDiaryListView.setOnRefreshListener(new PullToRefreshListView.OnRefreshListener() {
@Override
public void onRefresh() {
getSupportActionBar().setTitle("执行加载…");
loadDiaryListData();
mDiaryListView.onRefreshComplete();
}
});*/
} public void loadDiaryListData()
{
ZLog.i(TAG, "loadDiaryListData : start");
try {
ZHttpRequest httpRequset = new ZHttpRequest(new ZHttpRequest.OnHttpRequestListener() {
@Override
public void onRequest(ZHttpRequest request) {
ZLog.i(TAG, "request data : start");
} @Override
public void onSucceed(int statusCode, ZHttpRequest request) {
// 创建每行数据的集合
ZLog.i(TAG, "request onSucceed : start");
try {
String content = request.getInputStream();
if (content == null) {
Toast.makeText(getApplicationContext(), "数据请求失败", Toast.LENGTH_SHORT).show();
return;
}
JSONArray diaryList = new JSONArray(content);
/*if (asyncImageLoader.getMaxPosition() < 1) {
asyncImageLoader.setPositionLimit(0, diaryList.length());
}*/
endId = ((JSONObject) diaryList.opt(diaryList.length() - 1)).getInt("id");
initDiaryList(diaryList);
} catch (IOException e) {
Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
} catch (JSONException e) {
Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
} } @Override
public void onFailed(int statusCode, ZHttpRequest request) {
ZLog.i(TAG, "request onFailed : code"+ statusCode);
}
});
httpRequset.get("http://m.ai9475.com/?con=meitian_app&endId=" + this.endId);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
}
} protected class MyTabsListener implements ActionBar.TabListener
{
// private Fragment fragment; // public MyTabsListener(Fragment fragment)
// {
// this.fragment = fragment;
// } @Override
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft)
{
// ft.add(R.id.fragmentPlace, this.fragment, null);
} @Override
public void onTabReselected(ActionBar.Tab arg0, FragmentTransaction arg1) {
// TODO Auto-generated method stub } @Override
public void onTabUnselected(ActionBar.Tab arg0, FragmentTransaction arg1) {
// TODO Auto-generated method stub }
} /**
* 配置 ActionBar
*
* @param menu
* @return
*/
public boolean onCreateOptionsMenu(Menu menu)
{
this.getMenuInflater().inflate(R.menu.main, menu);
return super.onCreateOptionsMenu(menu);
} /*public void doClick(View view)
{
ZHttpRequest get = new ZHttpRequest();
get
.setCharset(HTTP.UTF_8)
.setConnectionTimeout(5000)
.setSoTimeout(5000);
get.setOnHttpRequestListener(new ZHttpRequest.OnHttpRequestListener() {
@Override
public void onRequest(ZHttpRequest request) throws Exception { } @Override
public String onSucceed(int statusCode, ZHttpRequest request) throws Exception {
return request.getInputStream();
} @Override
public String onFailed(int statusCode, ZHttpRequest request) throws Exception {
return "GET 请求失败:statusCode "+ statusCode;
}
}); ZHttpRequest post = new ZHttpRequest();
post
.setCharset(HTTP.UTF_8)
.setConnectionTimeout(5000)
.setSoTimeout(10000);
post.setOnHttpRequestListener(new ZHttpRequest.OnHttpRequestListener() {
private String CHARSET = HTTP.UTF_8;
private ContentType TEXT_PLAIN = ContentType.create("text/plain", Charset.forName(CHARSET)); @Override
public void onRequest(ZHttpRequest request) throws Exception {
// 设置发送请求的 header 信息
request.addHeader("cookie", "abc=123;456=爱就是幸福;");
// 配置要 POST 的数据
MultipartEntityBuilder builder = request.getMultipartEntityBuilder();
builder.addTextBody("p1", "abc");
builder.addTextBody("p2", "中文", TEXT_PLAIN);
builder.addTextBody("p3", "abc中文cba", TEXT_PLAIN);
if (picPath != null && ! "".equals(picPath)) {
builder.addTextBody("pic", picPath);
builder.addBinaryBody("file", new File(picPath));
}
request.buildPostEntity();
} @Override
public String onSucceed(int statusCode, ZHttpRequest request) throws Exception {
return request.getInputStream();
} @Override
public String onFailed(int statusCode, ZHttpRequest request) throws Exception {
return "POST 请求失败:statusCode "+ statusCode;
}
}); TextView textView = (TextView) findViewById(R.id.showContent);
String content = "初始内容";
try {
if (view.getId() == R.id.doGet) {
content = get.get("http://www.baidu.com");
content = "GET数据:isGet: " + (get.isGet() ? "yes" : "no") + " =>" + content;
} else {
content = post.post("http://192.168.1.6/test.php");
content = "POST数据:isPost" + (post.isPost() ? "yes" : "no") + " =>" + content;
} } catch (IOException e) {
content = "IO异常:" + e.getMessage();
} catch (Exception e) {
content = "异常:" + e.getMessage();
}
textView.setText(content);
} public void doPhoto(View view)
{
destoryBimap();
String state = Environment.getExternalStorageState();
if (state.equals(Environment.MEDIA_MOUNTED)) {
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
startActivityForResult(intent, 1);
} else {
Toast.makeText(MainActivity.this, "没有SD卡", Toast.LENGTH_LONG).show();
}
} @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
Uri uri = data.getData();
if (uri != null) {
this.photo = BitmapFactory.decodeFile(uri.getPath());
}
if (this.photo == null) {
Bundle bundle = data.getExtras();
if (bundle != null) {
this.photo = (Bitmap) bundle.get("data");
} else {
Toast.makeText(MainActivity.this, "拍照失败", Toast.LENGTH_LONG).show();
return;
}
} FileOutputStream fileOutputStream = null;
try {
// 获取 SD 卡根目录
String saveDir = Environment.getExternalStorageDirectory() + "/meitian_photos";
// 新建目录
File dir = new File(saveDir);
if (! dir.exists()) dir.mkdir();
// 生成文件名
SimpleDateFormat t = new SimpleDateFormat("yyyyMMddssSSS");
String filename = "MT" + (t.format(new Date())) + ".jpg";
// 新建文件
File file = new File(saveDir, filename);
// 打开文件输出流
fileOutputStream = new FileOutputStream(file);
// 生成图片文件
this.photo.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream);
// 相片的完整路径
this.picPath = file.getPath();
ImageView imageView = (ImageView) findViewById(R.id.showPhoto);
imageView.setImageBitmap(this.photo);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
} /**
* 销毁图片文件
*
private void destoryBimap()
{
if (photo != null && ! photo.isRecycled()) {
photo.recycle();
photo = null;
}
}*/
}

其中涉及到 scroll 滚动相关的事件,我一开始在这里折腾了好久,可以去看看我这篇文章:

Android 关于 OnScrollListener 事件顺序次数的简要分析

ZAsyncImageLoader.java

package com.ai9475.util;

import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message; import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* 异步多线程加载图片
*
* Created by ZHOUZ on 14-2-7.
*/
public class ZAsyncImageLoader
{
private static final String TAG = "ZAsyncImageLoader"; /**
* 线程池中的线程数量
*/
private int mThreadSize = 5; /**
* 是否使用 SD 卡缓存图片
*/
private boolean mIsUseDiskCache = false; /**
* SD 卡上缓存的图片有效期(单位:秒)
*/
private int mExpireTime = 86400; /**
* 图片缓存文件目录
*/
private String mCachePath = null; /**
* 同步缓存已加载过的图片,使用软引用优化内存
*/
private HashMap<String, SoftReference<Drawable>> mImageCaches = new HashMap<String, SoftReference<Drawable>>(); /**
* 使用线程池,根据 CPU 数量来动态决定可用线程数量
*/
private ExecutorService mExecutorService = null; /**
* 设置 SD 卡中的图片缓存有效时长(单位:秒)
*
* @param time
*/
public void setExpireTime(int time) {
this.mExpireTime = time;
} /**
* 设置线程数量
*
* @param size
*/
public void setThreadSize(int size) {
this.mThreadSize = size;
} /**
* 设置是否使用 SD 卡缓存图片
*
* @param isUse
*/
public void setIsUseDiskCache(Boolean isUse) {
this.mIsUseDiskCache = isUse;
} /**
* 设置缓存目录
*
* @param path
*/
public void setCacheDir(String path) {
this.mCachePath = path;
} /**
* 获取线程池管理器
*
* @return
*/
public ExecutorService getExecutorService() {
if (this.mExecutorService == null) {
if (this.mThreadSize < 1) {
this.mThreadSize = Runtime.getRuntime().availableProcessors() * 5;
}
this.mExecutorService = Executors.newFixedThreadPool(this.mThreadSize);
}
return this.mExecutorService;
} /**
* 加载图片的多线程控制
*
* @param imageUrl
* @param tag
* @param listener
*/
public Drawable loadDrawable(final String imageUrl, final String tag, final OnImageLoadListener listener)
{
// 是否已缓存过图片, 是则从缓存中直接获取, 若缓存中数据丢失则重新远程加载
if (this.mImageCaches.containsKey(imageUrl)) {
SoftReference<Drawable> softReference = this.mImageCaches.get(imageUrl);
if (softReference != null) {
Drawable drawable = softReference.get();
if (drawable != null) {
return drawable;
}
}
} // 异步多线程加载图片后的数据传递处理
final Handler handler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == 1) {
listener.onLoaded((Drawable) message.obj, imageUrl, tag);
} else {
listener.onFailed((IOException) message.obj, imageUrl, tag);
}
}
}; // 通过线程池来控制管理图片加载
this.getExecutorService().submit(new Runnable() {
@Override
public void run() {
Message msg;
try {
Drawable drawable = loadImageFromUrl(imageUrl);
mImageCaches.put(imageUrl, new SoftReference<Drawable>(drawable));
msg = handler.obtainMessage(1, drawable);
} catch (IOException e) {
msg = handler.obtainMessage(0, e);
}
handler.sendMessage(msg);
}
}); return null;
} /**
* 加载远程图片或本地图片缓存文件
*
* @param imageUrl
* @return
* @throws IOException
*/
public Drawable loadImageFromUrl(String imageUrl) throws IOException
{
// 检查 SD 卡是否可用并将图片缓存到 SD 卡上
if (mIsUseDiskCache && mCachePath != null)
{
File d = new File(mCachePath);
if (! d.exists()) {
d.mkdirs();
} final File f = new File(mCachePath + ZHelper.md5(imageUrl));
long time = (new Date()).getTime();
long expire = time - (mExpireTime * 1000L); // 文件存在且在有效期内则直接读取
if (f.exists() && f.lastModified() > expire) {
FileInputStream fis = new FileInputStream(f);
return Drawable.createFromStream(fis, "src");
} // 远程加载图片后写入到 SD 卡上
InputStream i = this.getImageInputStream(imageUrl);
if (i == null) {
return null;
} final Drawable drawable = Drawable.createFromStream(i, "src"); // 将图片异步写入到本地 SD 卡中缓存, 避免阻塞UI线程, 导致图片不能显示
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream i = ZFormat.drawable2InputStream(drawable);
DataInputStream in = new DataInputStream(i);
FileOutputStream out = new FileOutputStream(f);
byte[] buffer = new byte[1024];
int byteRead;
while ((byteRead = in.read(buffer)) != -1) {
out.write(buffer, 0, byteRead);
}
in.close();
out.close();
} catch (IOException e) {
ZLog.d("write image cache IOException", e.getMessage());
e.printStackTrace();
}
}
}).start(); return drawable;
}
// 只读取远程图片不缓存
else {
InputStream i = this.getImageInputStream(imageUrl);
return Drawable.createFromStream(i, "src");
}
} /**
* 远程加载图片数据
*
* @param imageUrl
* @return
* @throws IOException
*/
public InputStream getImageInputStream(String imageUrl) throws IOException
{
URL m = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) m.openConnection();
conn.setRequestMethod("GET");
conn.setUseCaches(false);
conn.setDoInput(true);
conn.setConnectTimeout(5000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
return conn.getInputStream();
} /**
* 加载图片的事件监听器
*/
public interface OnImageLoadListener {
/**
* 图片加载完成事件处理
*
* @param imageDrawable
* @param imageUrl
* @param tag
*/
public void onLoaded(Drawable imageDrawable, String imageUrl, String tag); /**
* 图片加载失败的事件处理
*
* @param e
* @param imageUrl
* @param tag
*/
public void onFailed(IOException e, String imageUrl, String tag);
} protected void finalize()
{
this.mExecutorService.shutdown();
}
}

DiaryListAdapter.java

package com.ai9475.meitian.adapter;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast; import com.ai9475.meitian.R;
import com.ai9475.util.ZAsyncImageLoader;
import com.ai9475.util.ZHelper;
import com.ai9475.util.ZLog;
import com.ai9475.util.ZUI; import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map; /**
* Created by ZHOUZ on 14-2-8.
*/
public class DiaryListAdapter extends BaseAdapter implements AbsListView.RecyclerListener
{
private static final String TAG = "DiaryListAdapter"; private Context mContext;
private ListView mDiaryListView;
public View mConvertView;
private ZAsyncImageLoader mAsyncImageLoader;
private JSONArray mDiaryDataList = null;
private boolean mIsScrolling = false;
private int mFirstPosition = 0;
private int mLastPosition = 0;
private int mPrevFirstPosition = 0;
private int mPrevLastPosition = 0;
private HashMap<String, Integer> mImagesHeight = new HashMap<String, Integer>(); public DiaryListAdapter(Context context, ListView listView, ZAsyncImageLoader imageLoader, JSONArray diaryList)
{
this.mContext = context;
this.mAsyncImageLoader = imageLoader;
this.mDiaryDataList = diaryList;
this.mDiaryListView = listView;
} public void setIsSCrolling(boolean flag)
{
this.mIsScrolling = flag;
} /**
* 当前列表加载到的日记总数
*
* @return
*/
public int getCount() {
return this.mDiaryDataList == null ? 0 : this.mDiaryDataList.length();
} /**
* 可见单元位置对比是否处在在上次滚动可是范围内
*
* @param position
* @return
*/
public boolean isInPrevPositionRange(int position) {
// 初始化时直接返回 false
if (this.mPrevLastPosition == 0) return false;
// 检测当前 item 的位置是否在上次滚动范围内, 是则表示该 item 正处于屏幕可见状态中无需重新加载
return (position >= this.mPrevFirstPosition && position <= this.mPrevLastPosition) ? true : false;
} /**
* 设置滚动后可见的起止项目序号
*
* @param first
* @param last
*/
public void setPositionRange(int first, int last) {
// 保存上次滚动后的可见位置
this.mPrevFirstPosition = this.mFirstPosition;
this.mPrevLastPosition = this.mLastPosition;
// 重置当前可见位置
this.mFirstPosition = first;
this.mLastPosition = last;
ZLog.i(TAG, "setPositionLimit prevFirst: "+ mPrevFirstPosition +", prevLast: "+ mPrevLastPosition +", first: "+ mFirstPosition +", last: "+ mLastPosition);
} /**
* 获取当前列表单元的日记id
*
* @param position
* @return
*/
public long getItemId(int position) {
int id = 0;
try {
id = this.getItem(position).getInt("id");
} catch (JSONException e) {
Toast.makeText(this.mContext, e.getMessage(), Toast.LENGTH_LONG);
}
return id;
} /**
* 获取一条数据
*
* @param position
* @return
*/
public JSONObject getItem(int position) {
return (JSONObject) this.mDiaryDataList.opt(position);
} /**
* 获取视图
*
* @param position
* @param convertView
* @param parent
* @return
*/
public View getView(int position, View convertView, ViewGroup parent)
{
ZLog.v(TAG, "getView i: " + position); final ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(this.mContext).inflate(R.layout.list_item_diary, null);
this.mConvertView = convertView;
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
} try {
// 获取当前列表单元的 JSON 日记数据
JSONObject item = this.getItem(position);
ZLog.i(TAG, "getView i: "+ position +", isScrolling: "+ mIsScrolling +", mFirstPosition: "+ mFirstPosition +", mLastPosition: "+ mLastPosition); /*if (! isScrolling && (position >= mFirstPosition && position <= mLastPosition)) {
ZLog.i(TAG, "getView i: "+ position +", show images");
this.loadImage(holder.picPhoto, holder.avatar, item);
} else {*/
ZLog.i(TAG, "getView i: "+ position +", can't show images");
// 初始化时自动加载
if (this.mLastPosition == 0) {
this.loadImage(holder.picPhoto, holder.avatar, item);
this.mPrevLastPosition = position;
} else {
this.setDefaultImage(holder.picPhoto, holder.avatar, item);
} /*holder.picPhoto.setScaleType(ImageView.ScaleType.CENTER);
holder.picPhoto.setImageResource(R.drawable.default_pic);
holder.avatar.setScaleType(ImageView.ScaleType.CENTER);
holder.avatar.setImageResource(R.drawable.default_avatar);
}*/
holder.nickname.setText(item.getString("nickname") +":"+ position);
holder.content.setText(item.getString("content"));
holder.calendarMonth.setText(ZHelper.dateFormat("MM月", item.getInt("calendarDate")));
holder.calendarDay.setText(ZHelper.dateFormat("dd", item.getInt("calendarDate")));
} catch (JSONException e) {
Toast.makeText(this.mContext, e.getMessage(), Toast.LENGTH_LONG);
} return convertView;
} public void setDefaultImage(ImageView picPhoto, ImageView avatar, JSONObject item) throws JSONException
{
int height = 0;
String picUrl = getPicUrl(item.getString("picUrl"));
if (mImagesHeight.containsKey(picUrl)) {
height = mImagesHeight.get(picUrl);
} int minHeight = ZUI.dp2px(this.mContext, 100);
if (height < minHeight) height = minHeight; picPhoto.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
picPhoto.setScaleType(ImageView.ScaleType.CENTER);
picPhoto.setImageResource(R.drawable.default_pic);
avatar.setScaleType(ImageView.ScaleType.CENTER);
avatar.setImageResource(R.drawable.default_avatar);
} public String getPicUrl(String pic)
{
return "http://img.ai9475.com/data/attachment/images/meitian/" + pic;
} public String getAvatarUrl(String avatar)
{
return "http://img.ai9475.com/data/attachment/images/avatar/" + avatar;
} /**
* 加载可见单元的图片
*
* @param picPhoto
* @param avatar
* @param item
* @throws JSONException
*/
public void loadImage(ImageView picPhoto, ImageView avatar, JSONObject item) throws JSONException
{
// 图片链接
String picUrl = getPicUrl(item.getString("picUrl"));
String avatarUrl = getAvatarUrl(item.getString("avatar")); // 记录异步加载的图片标签
String picTag = "pic"+ item.getInt("calendarDate") + item.getInt("id");
String avatarTag = "avatar"+ item.getInt("id"); picPhoto.setTag(picTag);
avatar.setTag(avatarTag); OnPicLoadListener mOnPicLoadListener = new OnPicLoadListener();
OnAvatarLoadListener mOnAvatarLoadListener = new OnAvatarLoadListener(); // 异步加载远程日记照片或缓存
Drawable picDrawable = this.mAsyncImageLoader.loadDrawable(picUrl, picTag, mOnPicLoadListener);
// 存在缓存则使用缓存中的图片资源或者使用默认占位图
mOnPicLoadListener.setDrawable(picPhoto, picUrl, picTag, picDrawable); // 异步加载远程用户头像或加载缓存
Drawable avatarDrawable = this.mAsyncImageLoader.loadDrawable(avatarUrl, avatarTag, mOnAvatarLoadListener);
// 存在缓存则使用缓存中的图片资源或者使用默认占位图
mOnAvatarLoadListener.setDrawable(avatar, avatarUrl, avatarTag, avatarDrawable);
} /**
* 当列表单元滚动到可是区域外时清除掉已记录的图片视图
*
* @param view
*/
@Override
public void onMovedToScrapHeap(View view) {
/*ViewHolder holder = (ViewHolder) view.getTag();
this.imageViews.remove(holder.avatar);
this.imageViews.remove(holder.picPhoto);*/
} private static class ViewHolder
{
public ImageView picPhoto;
public ImageView avatar;
public TextView nickname;
public TextView content;
public TextView calendarMonth;
public TextView calendarDay; public ViewHolder(View view)
{
this.picPhoto = (ImageView) view.findViewById(R.id.picPhoto);
this.avatar = (ImageView) view.findViewById(R.id.avatar);
this.nickname = (TextView) view.findViewById(R.id.nickname);
this.content = (TextView) view.findViewById(R.id.content);
this.calendarMonth = (TextView) view.findViewById(R.id.calendarMonth);
this.calendarDay = (TextView) view.findViewById(R.id.calendarDay);
}
} /**
* 头像图片加载事件监听
*/
private class OnAvatarLoadListener extends OnImageLoadListener
{
private int mImageSource = R.drawable.default_avatar; /**
* 设置图片
*
* @param view
* @param imageUrl
* @param tag
* @param drawable
*/
public void setDrawable(ImageView view, String imageUrl, String tag, Drawable drawable)
{
if (view == null) return;
if (drawable != null) {
view.setScaleType(ImageView.ScaleType.CENTER_CROP);
view.setImageDrawable(drawable);
} else {
view.setScaleType(ImageView.ScaleType.CENTER);
view.setImageResource(this.mImageSource);
}
}
} /**
* 日记照片加载事件监听
*/
private class OnPicLoadListener extends OnImageLoadListener
{
private int mImageSource = R.drawable.default_pic; /**
* 设置图片
*
* @param view
* @param imageUrl
* @param tag
* @param drawable
*/
public void setDrawable(ImageView view, String imageUrl, String tag, Drawable drawable)
{
if (view == null) return;
int height = 0;
if (mImagesHeight.containsKey(imageUrl)) {
height = mImagesHeight.get(imageUrl);
}
if (drawable != null) {
// 定义图片的最佳高度
if (height == 0) {
int minHeight = ZUI.dp2px(mContext, 100);
int maxHeight = ZUI.dp2px(mContext, 300);
height = (int) ((float) view.getWidth() / drawable.getMinimumWidth() * drawable.getMinimumHeight());
if (height > maxHeight) {
height = maxHeight;
} else if (height < minHeight) {
height = minHeight;
}
mImagesHeight.put(imageUrl, height);
}
// 现将图片完全透明
drawable.setAlpha(0);
view.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
view.setScaleType(ImageView.ScaleType.CENTER_CROP);
view.setImageDrawable(drawable);
// 添加透明渐变动画显示图片
AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
alphaAnim.setDuration(1000);
view.setAnimation(alphaAnim);
} else {
int minHeight = ZUI.dp2px(mContext, 100);
height = height < minHeight ? minHeight : height;
view.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
view.setScaleType(ImageView.ScaleType.CENTER);
view.setImageResource(mImageSource);
}
}
} /**
* 图片的加载监听事件
*/
abstract private class OnImageLoadListener implements ZAsyncImageLoader.OnImageLoadListener
{
/**
* 实现图片显示的抽象方法
*
* @param view
* @param tag
* @param drawable
*/
abstract public void setDrawable(ImageView view, String imageUrl, String tag, Drawable drawable); @Override
public void onLoaded(Drawable drawable, String imageUrl, String tag) {
ImageView view = (ImageView) mDiaryListView.findViewWithTag(tag == null ? imageUrl : tag);
this.setDrawable(view, imageUrl, tag, drawable);
} @Override
public void onFailed(IOException e, String imageUrl, String tag) {
//Toast.makeText(mContext, e.toString(), Toast.LENGTH_SHORT).show();
}
}
}

代码相关的一些类方法,以及涉及到的其他方面问题的相关博文:

android 一些数据转换方法

Android实现图片宽度100%ImageView宽度且高度按比例自动伸缩

Android之ListView异步加载图片且仅显示可见子项中的图片

另外提醒下:

如果遇到 position 值对应不上,可能是你使用了自定义的 ListView 中又添加拉动加载更多的子项,导致ListView中的 child 数量和 getView 中传递的 position 对应不上,我在这里折腾了好久才偶然发现这个问题。

项目源码:http://yunpan.cn/QpzhBEWCw3gDH

项目中需要修改服务端的地址,我在压缩包中附有一些 服务端发送的 JSON 数据

还可以加入 Android 文件共享群,这里全是 Android 和  JAVA 学习资料和教程:http://qun.yunpan.360.cn/38063538