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


    折腾了好多天,遇到 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宽度且高度按比例自动伸缩




    另外提醒下:

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


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

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


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

  • 相关阅读:
    git上传本地项目
    第十一章 持有对象
    java 闭包与回调
    类名.class 类名.this 详解
    匿名内部类
    第十章 内部类
    Java简单调用Zookeeper服务
    Linux下ZooKeeper集群安装
    Linux自动化安装JDK
    linux下初步实现Keepalived+Nginx高可用
  • 原文地址:https://www.cnblogs.com/zhouzme/p/5758504.html
Copyright © 2020-2023  润新知