Android之ListView异步加载图片且仅显示可见子项中的图片
折腾了好多天,遇到 N 多让人崩溃无语的问题,不过今天终于有些收获了,这是实验的第一版,有些混乱,下一步进行改造细分,先把代码记录在这儿吧。
网上查了很多资料,发现都千篇一律,抄来抄去,很多细节和完整实例都没看到,只有自己一点点研究了,总体感觉 android 下面要显示个图片真不容易啊。
项目主要实现的功能:
- 异步加载图片
- 图片内存缓存、异步磁盘文件缓存
- 解决使用 viewHolder 后出现的图片错位问题
- 优化列表滚动性能,仅显示可见子项中的图片
- 无需固定图片显示高度,对高度进行缓存使列表滚动时不会因图片高度变化而闪动,使滚动体验更加流畅
- 图片动画展示效果,新加载的图片显示透明渐变动画
没有涉及到下拉加载和刷新数据,目前还没接触到这些,而且已发现自定义 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; } }*/ }
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实现图片宽度100%ImageView宽度且高度按比例自动伸缩
另外提醒下:
如果遇到 position 值对应不上,可能是你使用了自定义的 ListView 中又添加拉动加载更多的子项,导致ListView中的 child 数量和 getView 中传递的 position 对应不上,我在这里折腾了好久才偶然发现这个问题。
项目源码:http://yunpan.cn/QpzhBEWCw3gDH
项目中需要修改服务端的地址,我在压缩包中附有一些 服务端发送的 JSON 数据
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。