Android 照片墙功能实现

效果
1.图片压缩:

package com.example.imageloader;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

import java.io.FileDescriptor;


/**
 * Created by 1013369768 on 2017/5/27.
 * 压缩图片工具类
 */


public class ImageResizer {

    public Bitmap decodeFormStream(FileDescriptor fd, int reqWidth, int reqHeight){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd,null,options);
        options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd,null,options);
    }

    /**
     * 计算采样率
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,int reqHeight){
        if(reqWidth == 0 || reqHeight == 0){
            return 1;
        }
        int inSampleSize = 1;
        int width = options.outWidth;
        int height = options.outHeight;
        if(width>reqWidth || height>reqHeight){
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

2.内存缓存和磁盘缓存的实现

    private ImageLoader(Context context){
        mContext = context.getApplicationContext();
        //获取当前进程最大可用内存,单位为KB
        int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024);
        //以当前进程最大可用内存的1/8作为内存缓存的总容量大小
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                //返回每个图片的大小,单位为KB
                return bitmap.getByteCount()/1024;
            }
        };

        File diskCacheDir = getDiskCacheDir(context,"bitmap");
        if(!diskCacheDir.exists()){
            diskCacheDir.mkdirs();
        }
        //判断磁盘剩余空间和磁盘缓存空间大小
        if(getUsableSpace(diskCacheDir) >DISK_CACHE_SIZE){
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

3.添加图片到内存缓存和磁盘缓存

    //添加到磁盘缓存
    private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight
    ) throws IOException {
        //通过检查当前线程的Looper是否和主线程的Looper相同
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("不能再主线程访问网络");
        }
        if(mDiskLruCache == null){
            return null;
        }
        String key = hashKeyForUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if(editor!=null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_I
            NDEX);
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFormDiskCache(url,reqWidth,reqHeight);
    }



----------
    //添加到内存缓存     
    private void addBitmapToMemoryCache(String key,Bitmap bitmap){
        if(getBitmapFormMemoryCache(key) == null){
            mMemoryCache.put(key, bitmap);
        }
    }

4.读取图片

    //从磁盘中获取图片
    private Bitmap loadBitmapFormDiskCache(String url,int reqWidth,int reqH
    eight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            Log.d("TAG","不允许在主线程加载图片");
        }
        if(mDiskLruCache == null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyForUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if(snapshot !=null){
            FileInputStream fileInputStream = (FileInputStream) snapshot.ge
            tInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeFormStream(fileDescriptor,reqWidth
            ,reqHeight);
            if(bitmap != null){
                addBitmapToMemoryCache(key,bitmap);
            }
        }
        return bitmap;
    }
----------
    //从内存中获取图片
    private Bitmap getBitmapFormMemoryCache(String key){
        return mMemoryCache.get(key);
    }

5.同步加载图片
同步加载接口需要在外部的子线程中调用,若在主线程中调用会抛出异常,通过loadBitmapFromHttp中进行检查,查看当前线程的Looper是否为主线程的Looper,判断当前线程是否为主线程。

    public Bitmap loadBitmap(String url,int reqWidth,int reqHeight){
        Bitmap bitmap = loadBitmapFormMemoryCache(url);
        Log.d("TAG","BITMAP:    "+bitmap);
        if(bitmap!=null){
            Log.d("TAG", "loadBitmapFromMemCache,url:" + url);
            return bitmap;
        }
        try {
            bitmap = loadBitmapFormDiskCache(url,reqWidth,reqHeight);
            if(bitmap !=null){
                Log.d("TAG", "loadBitmapFromDisk,url:" + url);
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url,reqWidth,reqHeight);
            Log.d("TAG", "loadBitmapFromHttp,url:" + url);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if(bitmap == null && !mIsDiskLruCacheCreated){
            Log.d("TAG", "encounter error, DiskLruCache is not created.");
            bitmap = downloadBitmapFormUrl(url);
        }
        return bitmap;
    }

5.异步加载
这里采用线程池加载图片,由于随着列表的滑动会产生大量线程,故不能使用普通线程加载

    public void bindBitmap(final String url, final ImageView imageView, fin
    al int reqWidth, final int reqHeight){
        // View中的setTag(int key, Object tag)表示给View添加额外数据,key值必须是唯一值,必须使用应用程序资源中声明的id,故需要在res/values中新建xml文件进行添加
        imageView.setTag(TAG_KEY_URI,url);
        Bitmap bitmap = null;
        bitmap = loadBitmapFormMemoryCache(url);
        if(bitmap!=null){
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
                if(bitmap!=null){
                    //把imageView、url、bitmap封装成一个LoaderBitmap对象
                    LoaderResult result = new LoaderResult(imageView,url,bi
                    tmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).
                    sendToTarget();
                }
            }
        };
        THTREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }



----------
    private static ThreadFactory sThreadFactory = new ThreadFactory() {
        // AtomicInteger可以在并发情况下达到原子化更新,避免使用了synchronized
        private AtomicInteger mCount = new AtomicInteger(1);
        @Override
        public Thread newThread(@NonNull Runnable r) {
            return new Thread(r,"ImageLoader#"+mCount.getAndIncrement());
        }
    };

    public static final Executor THTREAD_POOL_EXECUTOR = new ThreadPoolExec
    utor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS,new 
    LinkedBlockingQueue<Runnable>(),sThreadFactory);

6.Handler实现
采用主线程的Looper构造Handler对象,这样既可以在主线程更新UI,也可以在其他线程更新UI

    private Handler mMainHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            String url = (String) imageView.getTag(TAG_KEY_URI);
            if(url.equals(result.url)){
                imageView.setImageBitmap(result.bitmap);
            }else {
                Log.d("TAG", "set image bitmap,but url has changed, ignored
                !");
            }
        }
    };

Handeler实例化时:
① 更新UI,Handler要使用主线程中的Looper:
I、在主线程中可以使用Handler handler = new Handler()构造Handler对象

Handler h = new Handler(){
   public void handleMessage (Message msg){
      //在这里进行UI更新  
   }
}

II、如果在其他线程构造Handler对象,则需要:Handler handler = new Handler(Looper.getMainLooper())

    private Handler mMainHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
           //在这里处理UI更新
        }
    };

② 不更新UI,只处理消息:
I、 如果当前线程是主线程Handler handler = new Handler()

Handler h = new Handler(){
   public void handleMessage (Message msg){
      //在这里进行消息处理  
   }
}

II、如果当前线程不是主线程则使用Looper.prepare(); Handler handler = new Handler();Looper.loop()

class LooperThread extends Thread { 
     public Handler mHandler;  

      public void run() {  
          Looper.prepare();  
          mHandler = new Handler() {  
              public void handleMessage(Message msg) {  
                  //在这里处理消息
              }  
          };  

          Looper.loop();  
      } 
 }  

③ 注意:若是实例化的时候用Looper.getMainLooper()就表示放到UI线程去处理。
因为只有UI线程默认调用过:Loop.prepare();Loop.loop();故处理消息时不再需要调用。在其他线程需要手动调用这两个,否则会报错。

由于ListView和GridView存在View的复用机制,移出屏幕的View会进入到RecycleBin当中,而新进入屏幕的元素则会从RecycleBin中获取View控件。当有新的元素进入界面时就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片,注意网络操作都是比较耗时的,也就是说当我们快速滑动ListView的时候就很有可能出现这样一种情况,某一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。这种情况下会产生什么样的现象呢?根据GridView和ListView的工作原理,被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了乱序的情况。但是还没完,新进入屏幕的元素它也会发起一条网络请求来获取当前位置的图片,等到图片下载完的时候会设置到同样的ImageView上面,因此就会出现先显示一张图片,然后又变成了另外一张图片的情况。

解决方案:在异步请求网络前我们可以使用ImageView的setTag (int key, Object tag)为当前位置的ImageView绑定相关的数据,这里可以传入URL作为tag,然后再异步请求网络后使用getTag(int key)获取URL,若相同则加载图片。
由于GridView和ListView中的ImageView控件都是重用的,移出屏幕的控件很快会被进入屏幕的图片重新利用起来,那么getView()方法就会再次得到执行,而在getView()方法中会为这个ImageView控件设置新的Tag,这样老的Tag就会被覆盖掉,于是这时当老的网络请求完成后调用getTag(int key)获取URL,就只能得到老的URL了,而我们判断只有ImageView的URL相同的时候才会加载图片,这样图片乱序的问题也就不存在了。

照片墙功能的实现
适配器:

public class ImageAdapter extends BaseAdapter {
    private LayoutInflater mInflater;
    private List<String> mUrList;
    private Drawable mDefaultBitmapDrawable;
    private ImageLoader mImageLoader;
    private boolean mIsGridViewIdle;


    public ImageAdapter(List<String> urList, Context context,Boolean mIsGri
    dViewIdle){
        mImageLoader = ImageLoader.build(context);
        mUrList = urList;
        mInflater = LayoutInflater.from(context);
        mDefaultBitmapDrawable = ContextCompat.getDrawable(context,R.drawab
        le.image_default);
        this.mIsGridViewIdle = mIsGridViewIdle;
    }
    @Override
    public int getCount() {
        return mUrList.size();
    }

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

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder = null;
        if(convertView == null){
            convertView = mInflater.inflate(R.layout.item,parent,false);
            viewHolder = new ViewHolder();
            viewHolder.imageView = (ImageView)convertView.findViewById(R.id
            .image1);
            convertView.setTag(viewHolder);
        }else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        ImageView imageView = viewHolder.imageView;
        final String tag = (String)imageView.getTag();
        final String url = (String)getItem(position);
        if(!url.equals(tag)){
            imageView.setImageDrawable(mDefaultBitmapDrawable);
        }
        if(mIsGridViewIdle){
            imageView.setTag(url);
            //设置压缩后为100*100的像素,填充ImageView
            mImageLoader.bindBitmap(url,imageView,100,100);
        }
        return convertView;
    }

    private static class ViewHolder{
        public ImageView imageView;
    }
}

在getView中通过ImageLoader的bindBitmap方法异步加载图片,当用户刻意频繁
滑动就会瞬间产生大量的异步任务,这些异步任务会造成线程池的拥堵,并进行大量的UI更新操作,他们运行在主线程中,因此会造成一定的卡顿;可以考虑在滑动时停止加载图片,等滑动停止后再次加载。

    public void onScrollStateChanged(AbsListView view, int scrollState) {
       // 判断滑动事件是否结束,只触发一次  
        if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE){
            mIsGridViewIdle = true;
            mImageAdapter.notifyDataSetChanged();
        }else {
            mIsGridViewIdle = false;
        }
    }

demo下载

参考:

Android ListView异步加载图片乱序问题,原因分析及解决方案

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值