Android 超高仿微信图片选择器 图片该这么加载

jopen 8年前

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39943731,本文出自:【张鸿洋的博客】

1、概述

关于手机图片加载器,在当今像素随随便便破千万的时代,一张图片占据的内存都相当可观,作为高大尚程序猿的我们,有必要掌握图片的压缩,缓存等处理,以到达纵使你有万张照片,纵使你的像素再高,我们也能正确的显示所有的图片。当然了,单纯显示图片没撒意思,我们决定高仿一下微信的图片选择器,在此,感谢微信!本篇博客将基于以下两篇博客:

Android 快速开发系列 打造万能的ListView GridView 适配器  将使用我们打造的CommonAdapter作为我们例子中GridView以及ListView的适配器

Android Handler 异步消息处理机制的妙用 创建强大的图片加载类 将使用我们自己写的ImageLoader作为我们的图片加载的核心类

如果你没看过也没关系,等看完本篇博客,可以结合以上两篇再进行充分理解一下。

好了,首先贴一下效果图:

动态图实在是录不出来,大家自己打开微信点击发表图片,或者聊天窗口发送图片,大致和微信的效果一样~

简单描述一下:

1、默认显示图片最多的文件夹图片,以及底部显示图片总数量;如上图1;

2、点击底部,弹出popupWindow,popupWindow包含所有含有图片的文件夹,以及显示每个文件夹中图片数量;如上图2;注:此时Activity变暗

3、选择任何文件夹,进入该文件夹图片显示,可以点击选择图片,当然了,点击已选择的图片则会取消选择;如上图3;注:选中图片变暗

当然了,最重要的效果一定流畅,不能动不动OOM~~

本人测试手机小米2s,图片6802张,未出现OOM异常,效果也是非常流畅,堪比图库~

不过存在bug在所难免,大家可以留言说下自己发现的bug;文末会提供源码下载。

好了,下面就可以代码的征程了~

2、图片的列表页

首先对手机中图片进行扫描,拿到图片数量最多的,直接显示在GridView上;并且扫描结束,得到一个所有包含图片的文件夹信息的List;

对于文件夹信息,我们单独创建了一个Bean:

package com.zhy.bean;    public class ImageFloder  {   /**    * 图片的文件夹路径    */   private String dir;     /**    * 第一张图片的路径    */   private String firstImagePath;     /**    * 文件夹的名称    */   private String name;     /**    * 图片的数量    */   private int count;     public String getDir()   {    return dir;   }     public void setDir(String dir)   {    this.dir = dir;    int lastIndexOf = this.dir.lastIndexOf("/");    this.name = this.dir.substring(lastIndexOf);   }     public String getFirstImagePath()   {    return firstImagePath;   }     public void setFirstImagePath(String firstImagePath)   {    this.firstImagePath = firstImagePath;   }     public String getName()   {    return name;   }   public int getCount()   {    return count;   }     public void setCount(int count)   {    this.count = count;   }         }

用来存储当前文件夹的路径,当前文件夹包含多少张图片,以及第一张图片路径用于做文件夹的图标;注:文件夹的名称,我们在set文件夹的路径的时候,自动提取,仔细看下setDir这个方法。

接下来就是扫描手机图片的代码了:

@Override   protected void onCreate(Bundle savedInstanceState)   {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);      DisplayMetrics outMetrics = new DisplayMetrics();    getWindowManager().getDefaultDisplay().getMetrics(outMetrics);    mScreenHeight = outMetrics.heightPixels;      initView();    getImages();    initEvent();     }          /**    * 利用ContentProvider扫描手机中的图片,此方法在运行在子线程中 完成图片的扫描,最终获得jpg最多的那个文件夹    */   private void getImages()   {    if (!Environment.getExternalStorageState().equals(      Environment.MEDIA_MOUNTED))    {     Toast.makeText(this, "暂无外部存储", Toast.LENGTH_SHORT).show();     return;    }    // 显示进度条    mProgressDialog = ProgressDialog.show(this, null, "正在加载...");      new Thread(new Runnable()    {     @Override     public void run()     {        String firstImage = null;        Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;      ContentResolver mContentResolver = MainActivity.this        .getContentResolver();        // 只查询jpeg和png的图片      Cursor mCursor = mContentResolver.query(mImageUri, null,        MediaStore.Images.Media.MIME_TYPE + "=? or "          + MediaStore.Images.Media.MIME_TYPE + "=?",        new String[] { "image/jpeg", "image/png" },        MediaStore.Images.Media.DATE_MODIFIED);        Log.e("TAG", mCursor.getCount() + "");      while (mCursor.moveToNext())      {       // 获取图片的路径       String path = mCursor.getString(mCursor         .getColumnIndex(MediaStore.Images.Media.DATA));         Log.e("TAG", path);       // 拿到第一张图片的路径       if (firstImage == null)        firstImage = path;       // 获取该图片的父路径名       File parentFile = new File(path).getParentFile();       if (parentFile == null)        continue;       String dirPath = parentFile.getAbsolutePath();       ImageFloder imageFloder = null;       // 利用一个HashSet防止多次扫描同一个文件夹(不加这个判断,图片多起来还是相当恐怖的~~)       if (mDirPaths.contains(dirPath))       {        continue;       } else       {        mDirPaths.add(dirPath);        // 初始化imageFloder        imageFloder = new ImageFloder();        imageFloder.setDir(dirPath);        imageFloder.setFirstImagePath(path);       }         int picSize = parentFile.list(new FilenameFilter()       {        @Override        public boolean accept(File dir, String filename)        {         if (filename.endsWith(".jpg")           || filename.endsWith(".png")           || filename.endsWith(".jpeg"))          return true;         return false;        }       }).length;       totalCount += picSize;         imageFloder.setCount(picSize);       mImageFloders.add(imageFloder);         if (picSize > mPicsSize)       {        mPicsSize = picSize;        mImgDir = parentFile;       }      }      mCursor.close();        // 扫描完成,辅助的HashSet也就可以释放内存了      mDirPaths = null;        // 通知Handler扫描图片完成      mHandler.sendEmptyMessage(0x110);       }    }).start();     }

ps:运行出现空指针的话,在81行的位置添加判断,if(parentFile.list()==null)continue , 切记~~~有些图片比较诡异~~; 

initView就不看了,都是些findViewById;

getImages主要就是扫描图片的代码,我们开启了一个Thread进行扫描,扫描完成以后,我们得到了图片最多文件夹路径(mImgDir),手机中图片数量(totalCount);以及所有包含图片文件夹信息(mImageFloders)

然后我们通过handler发送消息,在handleMessage里面:

1、创建GridView的适配器,为我们的GridView设置适配器,显示图片;

2、有了mImageFloders,就可以创建我们的popupWindow了

看一眼我们的Handler

private Handler mHandler = new Handler()   {    public void handleMessage(android.os.Message msg)    {     mProgressDialog.dismiss();     //为View绑定数据     data2View();     //初始化展示文件夹的popupWindw     initListDirPopupWindw();    }   };

可以看到分别干了上述的两件事:

/**    * 为View绑定数据    */   private void data2View()   {    if (mImgDir == null)    {     Toast.makeText(getApplicationContext(), "擦,一张图片没扫描到",       Toast.LENGTH_SHORT).show();     return;    }      mImgs = Arrays.asList(mImgDir.list());    /**     * 可以看到文件夹的路径和图片的路径分开保存,极大的减少了内存的消耗;     */    mAdapter = new MyAdapter(getApplicationContext(), mImgs,      R.layout.grid_item, mImgDir.getAbsolutePath());    mGirdView.setAdapter(mAdapter);    mImageCount.setText(totalCount + "张");   };

data2View就是我们当前Activity上所有的View设置数据了。

看到这里还用到了一个Adapter,我们GridView的:

package com.zhy.imageloader;    import java.util.LinkedList;  import java.util.List;    import android.content.Context;  import android.graphics.Color;  import android.view.View;  import android.view.View.OnClickListener;  import android.widget.ImageView;    import com.zhy.utils.CommonAdapter;    public class MyAdapter extends CommonAdapter<String>  {     /**    * 用户选择的图片,存储为图片的完整路径    */   public static List<String> mSelectedImage = new LinkedList<String>();     /**    * 文件夹路径    */   private String mDirPath;     public MyAdapter(Context context, List<String> mDatas, int itemLayoutId,     String dirPath)   {    super(context, mDatas, itemLayoutId);    this.mDirPath = dirPath;   }     @Override   public void convert(final com.zhy.utils.ViewHolder helper, final String item)   {    // 设置no_pic    helper.setImageResource(R.id.id_item_image, R.drawable.pictures_no);    // 设置no_selected    helper.setImageResource(R.id.id_item_select,      R.drawable.picture_unselected);    // 设置图片    helper.setImageByUrl(R.id.id_item_image, mDirPath + "/" + item);      final ImageView mImageView = helper.getView(R.id.id_item_image);    final ImageView mSelect = helper.getView(R.id.id_item_select);      mImageView.setColorFilter(null);    // 设置ImageView的点击事件    mImageView.setOnClickListener(new OnClickListener()    {     // 选择,则将图片变暗,反之则反之     @Override     public void onClick(View v)     {        // 已经选择过该图片      if (mSelectedImage.contains(mDirPath + "/" + item))      {       mSelectedImage.remove(mDirPath + "/" + item);       mSelect.setImageResource(R.drawable.picture_unselected);       mImageView.setColorFilter(null);      } else      // 未选择该图片      {       mSelectedImage.add(mDirPath + "/" + item);       mSelect.setImageResource(R.drawable.pictures_selected);       mImageView.setColorFilter(Color.parseColor("#77000000"));      }       }    });      /**     * 已经选择过的图片,显示出选择过的效果     */    if (mSelectedImage.contains(mDirPath + "/" + item))    {     mSelect.setImageResource(R.drawable.pictures_selected);     mImageView.setColorFilter(Color.parseColor("#77000000"));    }     }  }

可以看到我们GridView的Adapter继承了我们的CommonAdapter,如果不知道CommonAdapter为何物,可以去看看万能适配器那篇博文;

我们现在只需要实现convert方法:

在convert中,我们设置图片,设置事件等,对于图片的变暗,我们使用的是ImageView的setColorFilter ;根据Url加载图片的操作封装在helper.setImageByUrl(view,url)中,内部使用的是我们自己定义的ImageLoader,包括错乱处理都已经封装了,图片策略我们使用的是LIFO后进先出;不清楚的可以看文章一开始说明的那两篇博文,对于CommonAdapter以及ImageLoader都有从无到有的详细打造过程;

到此我们的第一个Activity的所有的任务就完成了~~~

 

3、展现文件夹的PopupWindow

现在我们要实现,点击底部的布局弹出我们的文件夹选择框,并且我们弹出框后面的Activity要变暗;

不急着贴代码,我们先考虑下PopupWindow怎么用最好,我们的PopupWindow需要设置布局文件,需要初始化View,需要初始化事件,还需要和Activity交互~~

那么肯定的,我们使用独立的类,这个类和Activity很相似,在里面initView(),initEvent()之类的。

我们创建了一个popupWindow使用的超类:

package com.zhy.utils;    import java.util.List;    import android.content.Context;  import android.graphics.drawable.BitmapDrawable;  import android.view.MotionEvent;  import android.view.View;  import android.view.View.OnTouchListener;  import android.widget.PopupWindow;    public abstract class BasePopupWindowForListView<T> extends PopupWindow  {   /**    * 布局文件的最外层View    */   protected View mContentView;   protected Context context;   /**    * ListView的数据集    */   protected List<T> mDatas;     public BasePopupWindowForListView(View contentView, int width, int height,     boolean focusable)   {    this(contentView, width, height, focusable, null);   }     public BasePopupWindowForListView(View contentView, int width, int height,     boolean focusable, List<T> mDatas)   {    this(contentView, width, height, focusable, mDatas, new Object[0]);     }     public BasePopupWindowForListView(View contentView, int width, int height,     boolean focusable, List<T> mDatas, Object... params)   {    super(contentView, width, height, focusable);    this.mContentView = contentView;    context = contentView.getContext();    if (mDatas != null)     this.mDatas = mDatas;      if (params != null && params.length > 0)    {     beforeInitWeNeedSomeParams(params);    }      setBackgroundDrawable(new BitmapDrawable());    setTouchable(true);    setOutsideTouchable(true);    setTouchInterceptor(new OnTouchListener()    {     @Override     public boolean onTouch(View v, MotionEvent event)     {      if (event.getAction() == MotionEvent.ACTION_OUTSIDE)      {       dismiss();       return true;      }      return false;     }    });    initViews();    initEvents();    init();   }     protected abstract void beforeInitWeNeedSomeParams(Object... params);     public abstract void initViews();     public abstract void initEvents();     public abstract void init();     public View findViewById(int id)   {    return mContentView.findViewById(id);   }     protected static int dpToPx(Context context, int dp)   {    return (int) (context.getResources().getDisplayMetrics().density * dp + 0.5f);   }    }

也就是封装了一下popupWindow常用的一些设置,然后使用了类似模版方法模式,约束子类,必须实现initView,initEvent,init等方法

package com.zhy.imageloader;    import java.util.List;    import android.view.View;  import android.widget.AdapterView;  import android.widget.AdapterView.OnItemClickListener;  import android.widget.ListView;    import com.zhy.bean.ImageFloder;  import com.zhy.utils.BasePopupWindowForListView;  import com.zhy.utils.CommonAdapter;  import com.zhy.utils.ViewHolder;    public class ListImageDirPopupWindow extends BasePopupWindowForListView<ImageFloder>  {   private ListView mListDir;     public ListImageDirPopupWindow(int width, int height,     List<ImageFloder> datas, View convertView)   {    super(convertView, width, height, true, datas);   }     @Override   public void initViews()   {    mListDir = (ListView) findViewById(R.id.id_list_dir);    mListDir.setAdapter(new CommonAdapter<ImageFloder>(context, mDatas,      R.layout.list_dir_item)    {     @Override     public void convert(ViewHolder helper, ImageFloder item)     {      helper.setText(R.id.id_dir_item_name, item.getName());      helper.setImageByUrl(R.id.id_dir_item_image,        item.getFirstImagePath());      helper.setText(R.id.id_dir_item_count, item.getCount() + "张");     }    });   }     public interface OnImageDirSelected   {    void selected(ImageFloder floder);   }     private OnImageDirSelected mImageDirSelected;     public void setOnImageDirSelected(OnImageDirSelected mImageDirSelected)   {    this.mImageDirSelected = mImageDirSelected;   }     @Override   public void initEvents()   {    mListDir.setOnItemClickListener(new OnItemClickListener()    {     @Override     public void onItemClick(AdapterView<?> parent, View view,       int position, long id)     {        if (mImageDirSelected != null)      {       mImageDirSelected.selected(mDatas.get(position));      }     }    });   }     @Override   public void init()   {    // TODO Auto-generated method stub     }     @Override   protected void beforeInitWeNeedSomeParams(Object... params)   {    // TODO Auto-generated method stub   }    }
好了,现在就是我们正在的popupWindow咯,布局文件夹主要是个ListView,所以在initView里面,我们得设置它的适配器;当然了,这里的适配器依然用我们的CommonAdapter,几行代码搞定

然后我们需要和Activity交互,当我们点击某个文件夹的时候,外层的Activity需要改变它GridView的数据源,展示我们点击文件夹的图片;

关于交互,我们从Activity的角度去看弹出框,Activity想知道什么,只想知道选择了别的文件夹来告诉我,所以我们创建一个接口OnImageDirSelected,对Activity设置回调;

这里还可以这么写:就是把popupWindow的ListView公布出去,然后在Activity里面使用popupWindow.getListView(),setOnItemClickListener,这么做,个人觉得不好,耦合度太高,客户简单改下需求“这个文件夹展示,给我们换了,换成GridView”,呵呵,此时,你需要到处去修改Activity里面的代码,因为你Activity里面竟然还有个popupWindow.getListView。

好了,扯多了,初始化事件的代码:

@Override   public void initEvents()   {    mListDir.setOnItemClickListener(new OnItemClickListener()    {     @Override     public void onItemClick(AdapterView<?> parent, View view,       int position, long id)     {        if (mImageDirSelected != null)      {       mImageDirSelected.selected(mDatas.get(position));      }     }    });   }

如果有人设置了回调,我们就调用;

到此,整个popupWindow就出炉了,接下来就看啥时候让它展示了;

4、选择不同的文件夹

上面说道,当扫描图片完成,拿到包含图片的文件夹信息列表;这个列表就是我们popupWindow所需的数据,所以我们的popupWindow的初始化在handleMessage(上面贴了handler的代码)里面:

在handleMessage里面调用initListDirPopupWindw

/**    * 初始化展示文件夹的popupWindw    */   private void initListDirPopupWindw()   {    mListImageDirPopupWindow = new ListImageDirPopupWindow(      LayoutParams.MATCH_PARENT, (int) (mScreenHeight * 0.7),      mImageFloders, LayoutInflater.from(getApplicationContext())        .inflate(R.layout.list_dir, null));      mListImageDirPopupWindow.setOnDismissListener(new OnDismissListener()    {       @Override     public void onDismiss()     {      // 设置背景颜色变暗      WindowManager.LayoutParams lp = getWindow().getAttributes();      lp.alpha = 1.0f;      getWindow().setAttributes(lp);     }    });    // 设置选择文件夹的回调    mListImageDirPopupWindow.setOnImageDirSelected(this);   }
我们初始化我们的popupWindow,设置了关闭对话框的回调,已经设置了选择不同文件夹的回调;
这里仅仅是初始化,下面看我们合适将其弹出的,其实整个Activity也就一个事件,点击弹出该对话框,所以看Activity的initEvents方法:

private void initEvent()   {    /**     * 为底部的布局设置点击事件,弹出popupWindow     */    mBottomLy.setOnClickListener(new OnClickListener()    {     @Override     public void onClick(View v)     {      mListImageDirPopupWindow        .setAnimationStyle(R.style.anim_popup_dir);      mListImageDirPopupWindow.showAsDropDown(mBottomLy, 0, 0);        // 设置背景颜色变暗      WindowManager.LayoutParams lp = getWindow().getAttributes();      lp.alpha = .3f;      getWindow().setAttributes(lp);     }    });   }

可以看到,我们为底部布局设置点击事件;设置popupWindow的弹出与消失的动画;已经让Activity背景变暗变亮,通过改变Window alpha实现的。变亮在弹出框消息的监听里面

动画的文件就不贴了,大家自己看源码;

popupWindow弹出了,用户此时可以选择不同的文件夹,那么现在该看选择后的回调的代码了:

我们的Activity实现了该接口,直接看实现的方法:

@Override   public void selected(ImageFloder floder)   {      mImgDir = new File(floder.getDir());    mImgs = Arrays.asList(mImgDir.list(new FilenameFilter()    {     @Override     public boolean accept(File dir, String filename)     {      if (filename.endsWith(".jpg") || filename.endsWith(".png")        || filename.endsWith(".jpeg"))       return true;      return false;     }    }));    /**     * 可以看到文件夹的路径和图片的路径分开保存,极大的减少了内存的消耗;     */    mAdapter = new MyAdapter(getApplicationContext(), mImgs,      R.layout.grid_item, mImgDir.getAbsolutePath());    mGirdView.setAdapter(mAdapter);    // mAdapter.notifyDataSetChanged();    mImageCount.setText(floder.getCount() + "张");    mChooseDir.setText(floder.getName());    mListImageDirPopupWindow.dismiss();     }

我们改变了GridView的适配器,以及底部的控件上的文件夹名称,文件数量等等;

好了,到此结束;整篇由于篇幅原因没有贴任何布局文件,大家自己通过源码查看;

在此希望大家可以通过该案例,能够去其糟粕,取其精华,学习其中值得借鉴的代码风格,不要真的当作一个例子去学习~~

 

 

源码点击下载  

ps:请真机测试,反正我的模拟器扫描不到图片~

ps:运行出现空指针的话,在getImages中添加判断,if(parentFile.list()==null)continue , 切记~~~具体位置,上面有说; 

 

 

 

 

---------------------------------------------------------------------------------------------------------

建了一个QQ群,方便大家交流。群号:55032675


 

----------------------------------------------------------------------------------------------------------

博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):

1、高仿微信5.2.1主界面及消息提醒

2、高仿QQ5.0侧滑


 

 

 

 

 


 

 

 

 

 

 

 

 

 

来自: http://blog.csdn.net//lmj623565791/article/details/39943731