Android 高仿微信图片选择器(瀑布流)

前言

在很多很多的项目中,都有选择本地图片的功能,现在就带大家做一个仿微信的图片选择器

1.和微信相比,由于博主是平板,微信在博主的平板中的图片是很模糊的,而我们的这个比微信的清晰,但是代价基本就是内存的多消耗,但是现在的收集基本上这点内存还是有的,图片也是经过压缩的

2.和鸿洋封装的相比,有些人可能会说和大神的有可比性么?我可以很直白的说这个图片选择器就是参考鸿洋大神以前封装的图片选择器,并且进行代码的分层、逻辑的重新梳理、优化显示效果、去除很多难懂的代码,用浅显易懂的代码实现之!,并且图片的显示进行了适配,无论在大的平板还是小的手机上显示都不会觉得很过分.用RecyclerView取代了鸿洋大神使用的ListView,为以后改效果做下了很好的铺垫

但是鸿洋大神的选择器给了我一些很好的思路,在这里表示感谢!

先放上效果图:

Android 高仿微信图片选择器(瀑布流)

Android 高仿微信图片选择器(瀑布流)

Android 高仿微信图片选择器(瀑布流)

纵向的滑动

Android 高仿微信图片选择器(瀑布流)

上面的一些地方博主就不美化了,啥图片太大呀,文字太大之类的就留给亲们自行去更改吧,美化对博主来说是一件难得事情~~~~嗯

使用的是RecyclerView,所以你可以轻松的改成其他的样式,比如比较炫酷的瀑布流之类的,这就靠你自己去扩展了~~~

好了,下面开始正片~~~咳咳咳,是正文

梳理制作流程

1.提出问题

a)我们显示的图片在哪里?

b)他们在哪些文件夹中?

c)图片如何加载(其实就是如何加载缩略图,说白点就是如何压缩图)


2.针对问题,制定方案

问题a,b其实是一个问题,这么说呢?我们可以为我们的app和本地图片之间构建一个管理者LocalImageManager,当你想获取图片的路径的时候找他!当你想知道图片都在哪里文件夹中,找他!当你想获取某一个文件夹中的图片的时候,也找他.归根结底这个类就是要给对本地图片的一个管理者


问题c,我们需要定制一个自己的本地图片加载器,只要提供图片控件ImageView 和 图片的路径ImagePath,图片加载器就可以加载缩略图到控件中显示


写代码!

编写本地的图片管理者LocalImageManager和本地图片信息持有者LocalImageInfo

功能如图
Android 高仿微信图片选择器(瀑布流)
可以看到我们的LocalImageManager做了一个中间者,但是我们前面分析,要用到的图片信息有好几个
所以我们这里对信息进行一个封装,命名为LocalImageInfo
Android 高仿微信图片选择器(瀑布流)
所以我们每一次的对本地图片的请求操作,都可以带上这个对象,这个对象会给我们保存一些信息不会丢失

对应的代码为:

/**
 * Created by cxj on 2016/5/4.
 * 本地图片的一个信息,对图片的一个描述
 */
public class LocalImageInfo {

    /**
     * 信息的图片类型的数组
     * 图片的类型
     * {@link LocalImageManager#JPEG_MIME_TYPE}
     * {@link LocalImageManager#PNG_MIME_TYPE}
     * {@link LocalImageManager#JPG_MIME_TYPE}
     */
    private String[] mimeType;

    /**
     * 一个对象的图片类型是定死的,方便维护,对象一经创建
     * 这个类型就更改不了了
     *
     * @param mimeType 图片的类型
     *                 {@link LocalImageManager#JPEG_MIME_TYPE}
     *                 {@link LocalImageManager#PNG_MIME_TYPE}
     *                 {@link LocalImageManager#JPG_MIME_TYPE}
     */
    public LocalImageInfo(String[] mimeType) {
        //如果传进来是空的,直接就挂了
        this.mimeType = new String[mimeType.length];

        for (int i = 0; i < mimeType.length; i++) {
            this.mimeType[i] = mimeType[i];
        }

    }

    /**
     * 图片文件的文件夹路径
     */
    private Set<String> imageFolders = new HashSet<String>();

    /**
     * 所有图片文件的路径
     */
    private List<String> imageFiles = new ArrayList<String>();

    /**
     * 一个map集合,用于保存对应文件夹路径对应的图片路径的集合
     */
    private Map<String, List<String>> map = new HashMap<String, List<String>>();


    /**
     * 获取一个新的类型数组,和原来的不一样
     *
     * @return
     */
    public String[] getMimeType() {
        String[] arr = new String[this.mimeType.length];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = this.mimeType[i];
        }
        return arr;
    }

    public Set<String> getImageFolders() {
        return imageFolders;
    }

    public void setImageFolders(Set<String> imageFolders) {
        this.imageFolders = imageFolders;
    }

    public List<String> getImageFiles() {
        return imageFiles;
    }

    public void setImageFiles(List<String> imageFiles) {
        this.imageFiles = imageFiles;
    }

    public List<String> getImagesByFolderPath(String folderPath) {
        return map.get(folderPath);
    }

    public void setImagesByFolderPath(String folderPath, List<String> list) {
        map.put(folderPath, list);
    }

}
里面有维护mime_type类型的操作,这是防止有的人获取mime_type类型然后更改掉,这里的原则是一个LocalImageInfo对应一个信息,那图片的类型当然是创建了就不能更改啦!所以你可以看到getMimeType的时候我都是创建一个信息返回的

所以在我们的LocalImageManager中就可以编写代码啦,为用户替用对本地图片的便捷操作
/**
 * Created by cxj on 2016/5/4.
 * a manager of localImage
 * require permission:<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 * you can copy to your AndroidManifest.xml
 */
public class LocalImageManager {

    /**
     * png图片格式的类型
     * the mime_type of png
     */
    public static final String PNG_MIME_TYPE = "image/png";

    /**
     * png图片的后缀
     */
    public static final String PNG_SUFFIX = "png";

    /**
     * jpeg图片格式的类型
     * the mime_type of jpeg
     */
    public static final String JPEG_MIME_TYPE = "image/jpeg";

    /**
     * jpg图片格式的类型
     * the mime_type of jpg
     */
    public static final String JPG_MIME_TYPE = "image/jpeg";

    /**
     * jpeg图片的后缀
     */
    public static final String JPEG_SUFFIX = "jpeg";

    /**
     * jpg图片的后缀
     */
    public static final String JPG_SUFFIX = "jpg";

    /**
     * 上下文对象
     * the environment of app
     */
    private static Context context;

    /**
     * init
     *
     * @param context
     */
    public static void init(Context context) {
        if (LocalImageManager.context == null) {
            LocalImageManager.context = context;
        }
    }


    /**
     * 根据文件夹的路径,查询对应mime_type类型的图片路径的集合
     *
     * @param localImageInfo 可以为NULL,当为NULL的时候,不缓存
     * @param folderPath
     * @return
     */
    @Nullable
    public static List<String> queryImageByFolderPath(LocalImageInfo localImageInfo, String folderPath) {
        //图片的类型
        String[] mimeType = localImageInfo.getMimeType();
        //健壮性判断
        if (folderPath == null || "".equals(folderPath)
                || mimeType.length == 0) {
            return null;
        }
        //声明返回值
        List<String> images;

        //如果只是单纯的查询,一次性的,那么可以传入localImageInfo为空
        if (localImageInfo == null) {
            images = new ArrayList<String>();
        } else { //否则从缓存中拿出来看看
            images = localImageInfo.getImagesByFolderPath(folderPath);
        }
        //如果没有,那就自己去查询
        if (images == null) {
            //创建集合
            images = new ArrayList<String>();
            //创建文件对象爱那个
            File folder = new File(folderPath);
            //如果文件夹存在
            if (folder.exists() && folder.isDirectory()) {
                //获取所有的文件对象
                File[] files = folder.listFiles();
                //循环所有的文件对象
                for (int i = 0; i < files.length; i++) {
                    File file = files[i];
                    //如果是文件,而不是文件夹,并且文件的后缀匹配了
                    if (file.isFile() && isFileMatchMimeType(file, mimeType)) {
                        //添加到集合中
                        images.add(file.getPath());
                    }
                }
                if (localImageInfo != null) {
                    //集合保存起来,下次调用的时候就不会查询了,直接返回
                    localImageInfo.setImagesByFolderPath(folderPath, images);
                }
                return images;
            }
        } else {
            return images;
        }

        return null;

    }

    /**
     * 查询系统中存储的所有图片的路径和文件夹的路径
     * 其实就是
     * {@link LocalImageManager#queryImage(LocalImageInfo)}
     * 和
     * {@link LocalImageManager#queryAllFolders(LocalImageInfo)}
     * 的合体
     *
     * @param localImageInfo 本地图片的描述对象
     * @return
     */
    @Nullable
    public static LocalImageInfo queryImageWithFolder(LocalImageInfo localImageInfo) {
        String[] mimeType = localImageInfo.getMimeType();
        if (mimeType.length == 0) {
            return localImageInfo;
        }
        return queryAllFolders(queryImage(localImageInfo));
    }

    /**
     * 查询出所有图片的文件夹的路径,根据{@link LocalImageInfo#imageFiles}集合进行整理的
     * 结果放在{@link LocalImageInfo#imageFolders}
     *
     * @param localImageInfo 本地图片的一个描述信息
     * @return
     */
    private static LocalImageInfo queryAllFolders(LocalImageInfo localImageInfo) {
        //获取图片的文件夹
        List<String> imageFiles = localImageInfo.getImageFiles();
        //获取存放图片路径的文件夹集合
        Set<String> set = localImageInfo.getImageFolders();
        set.clear();
        int size = imageFiles.size();
        //循环所有的图片路径,找出所有的文件夹路径,不能重复
        for (int i = 0; i < size; i++) {
            File imageFile = new File(imageFiles.get(i));
            if (imageFile.exists()) {
                File parentFile = imageFile.getParentFile();
                if (parentFile.exists()) {
                    set.add(parentFile.getPath());
                }
            }
        }
        return localImageInfo;
    }

    /**
     * query by mime_type of image
     * 根据图片的类型进行查询,查询的是系统中存储的图片信息
     * 结果放在{@link LocalImageInfo#imageFiles}
     *
     * @param localImageInfo 本地图片的一个描述信息
     * @return
     */
    public static LocalImageInfo queryImage(LocalImageInfo localImageInfo) {

        String[] mimeType = localImageInfo.getMimeType();

        if (mimeType.length == 0) {
            return localImageInfo;
        }

        Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        ContentResolver mContentResolver = context
                .getContentResolver();

        //查询的条件
        StringBuffer selection = new StringBuffer();
        //利用循环生成条件
        for (int i = 0; i < mimeType.length; i++) {
            //图片的类型
            String mime = mimeType[i];
            if (i == mimeType.length - 1) { //如果是最后一个
                selection.append(MediaStore.Images.Media.MIME_TYPE + " = ?");
            } else {
                selection.append(MediaStore.Images.Media.MIME_TYPE + " = ? or ");
            }
        }

        //执行查询
        Cursor mCursor = mContentResolver.query(mImageUri, null,
                selection.toString(),
                mimeType,
                MediaStore.Images.Media.DATE_MODIFIED);

        List<String> imageFiles = localImageInfo.getImageFiles();
        imageFiles.clear();

        //循环结果集
        while (mCursor.moveToNext()) {
            // 获取图片的路径
            String imagePath = mCursor.getString(mCursor
                    .getColumnIndex(MediaStore.Images.Media.DATA));
            imageFiles.add(imagePath);
        }

        return localImageInfo;
    }

    /**
     * 文件的后缀是不是匹配mimeType类型
     *
     * @param file      不能为空 must not be NULL
     * @param mime_type 不能为空 must not be NULL
     * @return
     */
    private static boolean isFileMatchMimeType(File file, String... mime_type) {
        String SUFFIX = StringUtil.getLastContent(file.getName(), ".").toLowerCase();
        for (int i = 0; i < mime_type.length; i++) {
            switch (mime_type[i]) {
                case PNG_MIME_TYPE:
                    if (PNG_SUFFIX.equals(SUFFIX)) {
                        return true;
                    }
                    break;
                case JPEG_MIME_TYPE:
                    if (JPEG_SUFFIX.equals(SUFFIX) || JPG_SUFFIX.equals(SUFFIX)) {
                        return true;
                    }
                    break;
            }
        }
        return false;
    }

}
注释写的很清楚啦,这里不解释每一个方法的具体作用了,我们可以看到我们的每一个方法都是接受一个LocalImageInfo参数的,这和我们刚刚设计的是一致的,设计的时候怎么样,代码也应该是怎么样的

这里再次强调一点,博主为什么大费周章的写这几个类?
如此设计可以很好的降低代码之间的耦合性,这两个类你随便复制出去到其他项目中都是可以使用的,假如你不设计这两个类,你在Activity中直接写查询的代码,你的代码移植性和可读性已经大大的降低了,所以这里说明一下

LocalImageManager中几个重要的方法,这里罗列出来:

a) queryImage:用于查询Android系统中保存的图片信息
b) queryAllFolders 根据查询出来的图片信息,整理出有多少的文件夹
c) queryImageWithFolder     a和b方法的结合体
d) queryImageByFolderPath 根据指定的路径查询图片信息,如果传入图片信息持有者LocalImageInfo,就会把查询的结果缓存到对象中,反之不缓存

编写主界面代码

主界面xml文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.cxj.imageselect.ui.SelectLocalImageAct">

    <RelativeLayout
        android:id="@+id/rl_act_main_titlebar"
        android:paddingTop="8dp"
        android:paddingBottom="8dp"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:layout_width="match_parent"
        android:background="#000066"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/iv_act_main_back"
            android:layout_width="wrap_content"
            android:layout_centerVertical="true"
            android:src="@mipmap/arrow_left"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/tv_act_main_ok"
            android:layout_width="wrap_content"
            android:layout_centerVertical="true"
            android:layout_height="wrap_content"
            android:padding="2dp"
            android:layout_alignParentRight="true"
            android:textSize="22sp"
            android:background="#006800"
            android:textColor="#FFFFFF"
            android:text="我选好了"/>

    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_below="@+id/rl_act_main_titlebar"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </android.support.v7.widget.RecyclerView>

    <LinearLayout
        android:id="@+id/ll_act_main_info"
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:orientation="horizontal"
        android:background="#EE4d4d4d"
        android:padding="5dp"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_act_main_image_folder_name"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:text="image_folder"
            android:textSize="22sp"
            android:textColor="#FFFFFF"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/tv_act_main_image_number"
            android:layout_width="wrap_content"
            android:text="image_number"
            android:textColor="#FFFFFF"
            android:textSize="22sp"
            android:layout_height="wrap_content" />

    </LinearLayout>

</RelativeLayout>
Android 高仿微信图片选择器(瀑布流)
ui搭建的比较丑,讲究这看吧,你们自己可以更改的以后

Activity代码实现

/**
 * 选择本地图片的activity
 */
public class SelectLocalImageAct extends Activity implements View.OnClickListener, CommonRecyclerViewAdapter.OnRecyclerViewItemClickListener, Runnable {

    /**
     * 类的标识
     */
    public static final String TAG = "SelectLocalImageAct";

    /**
     * activity带回去的数据的标识
     */
    public static final String RETURN_DATA_FLAG = "data";

    /**
     * 结果码
     */
    public static final int RESULT_CODE = 666;


    private RecyclerView rv = null;

    /**
     * PopupWindow
     */
    private ListImageDirPopupWindow listImageDirPopupWindow;

    /**
     * 返回图标
     */
    private ImageView iv_back;

    /**
     * 确定
     */
    private TextView tv_ok;

    /**
     * 底部的控件
     */
    private LinearLayout ll_info = null;


    /**
     * 显示文件夹名称的控件
     */
    private TextView tv_folderName;


    /**
     * 显示文件夹中图片文件的个数
     */
    private TextView tv_imageNumber;

    /**
     * 适配器
     */
    private CommonRecyclerViewAdapter<String> adapter;

    /**
     * 本地图片的信息
     */
    private LocalImageInfo localImageInfo = new LocalImageInfo(new String[]{LocalImageManager.PNG_MIME_TYPE,
            LocalImageManager.JPEG_MIME_TYPE,
            LocalImageManager.JPG_MIME_TYPE});


    /**
     * 显示的数据u
     */
    private List<String> data = new ArrayList<String>();
    
    /**
     * 记录图片是不是被选中,利用下标进行关联
     */
    private List<Boolean> imageStates = new ArrayList<Boolean>();

    /**
     * 上下文
     */
    private Context context;


    private Handler h = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            MessageDataHolder m = (MessageDataHolder) msg.obj;

            //设置底部的信息
            tv_folderName.setText(m.folderName.length() > 12 ? m.folderName.substring(0, 12) + "..." : m.folderName);
            tv_imageNumber.setText(m.imageNum + "张");

            //初始化选中状态的记录集合
            imageStates.clear();
            for (int i = 0; i < localImageInfo.getImageFiles().size(); i++) {
                imageStates.add(false);
            }

            data.clear();
            data.addAll(localImageInfo.getImageFiles());

            //关闭弹出窗口
            listImageDirPopupWindow.dismiss();
            setBackAlpha(false);

            //通知数据改变
            adapter.notifyDataSetChanged();
            listImageDirPopupWindow.notifyDataSetChanged();

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        EBus.register(TAG, this);

        //初始化控件
        initView();

        //初始化事件
        initEvent();

        //线程池执行任务
        ThreadPool.getInstance().invoke(this);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        EBus.unRegister(TAG);
    }

    /**
     * 初始化监听事件
     */
    private void initEvent() {

        iv_back.setOnClickListener(this);
        tv_ok.setOnClickListener(this);

        //设置底部菜单的监听
        ll_info.setOnClickListener(this);

        //设置ReCyclerView的条目监听
        adapter.setOnRecyclerViewItemClickListener(this);
    }

    /**
     * 初始化控件
     */
    private void initView() {

        context = this;

        //寻找一些控件
        iv_back = (ImageView) findViewById(R.id.iv_act_main_back);
        tv_ok = (TextView) findViewById(R.id.tv_act_main_ok);

        ll_info = (LinearLayout) findViewById(R.id.ll_act_main_info);
        tv_folderName = (TextView) findViewById(R.id.tv_act_main_image_folder_name);
        tv_imageNumber = (TextView) findViewById(R.id.tv_act_main_image_number);

        rv = (RecyclerView) findViewById(R.id.rv);
        //创建适配器
        adapter = new ImageAdapter(context, data, imageStates);
        //设置适配器
        rv.setAdapter(adapter);

        rv.setLayoutManager(new GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false));

        //初始化弹出窗口
        initPopuWindow();

    }


    @Override
    public void onClick(View v) {
        int id = v.getId();
        switch (id) {
            case R.id.ll_act_main_info:
                if (listImageDirPopupWindow != null) {
                    listImageDirPopupWindow
                            .setAnimationStyle(R.style.anim_popup_dir);
                    listImageDirPopupWindow.showAsDropDown(ll_info, 0, 0);
                }
                // 设置背景颜色变暗
                setBackAlpha(true);
                break;
            case R.id.iv_act_main_back:
                finish();
                break;
            case R.id.tv_act_main_ok:
                Intent i = new Intent();
                i.putExtra(RETURN_DATA_FLAG, getSelectImages());
                setResult(RESULT_CODE, i);
                finish();
                break;
        }
    }

    @Override
    public void onItemClick(View v, int position) {
        imageStates.set(position, !imageStates.get(position));
        adapter.notifyDataSetChanged();
    }

    /**
     * 根据文件夹的路径,进行加载图片
     *
     * @param folderPath
     */
    public void onEventLoadImageByFolderPath(final String folderPath) {
        if ("System".equals(folderPath)) {
            //线程池执行任务
            ThreadPool.getInstance().invoke(this);
            return;
        }
        File folder = new File(folderPath);
        //文件夹存在并且是一个目录
        if (folder.exists() && folder.isDirectory()) {
            ThreadPool.getInstance().invoke(new Runnable() {
                @Override
                public void run() {
                    List<String> tmpData = LocalImageManager.queryImageByFolderPath(localImageInfo, folderPath);
                    localImageInfo.getImageFiles().clear();
                    localImageInfo.getImageFiles().addAll(tmpData);
                    //发送消息
                    h.sendMessage(MessageDataHolder.obtain(folderPath, localImageInfo.getImageFiles().size()));
                }
            });

        }
    }

    @Override
    public void run() {

        //初始化本地图片的管理者
        LocalImageManager.init(context);

        //获取本地系统图片的信息,并且整理文件夹
        LocalImageManager.
                queryImageWithFolder(localImageInfo);

        //发送消息
        h.sendMessage(MessageDataHolder.obtain("所有文件", localImageInfo.getImageFiles().size()));
    }

    /**
     * 初始化弹出的窗口
     */
    private void initPopuWindow() {
        //初始化弹出框
        View contentView = View.inflate(context, R.layout.list_dir, null);
        //创建要弹出的popupWindow
        listImageDirPopupWindow = new ListImageDirPopupWindow(contentView,
                ScreenUtils.getScreenWidth(context),
                ScreenUtils.getScreenHeight(context) * 2 / 3,
                true, localImageInfo);

        //消失的时候监听
        listImageDirPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
            @Override
            public void onDismiss() {
                setBackAlpha(false);
            }
        });
    }

    /**
     * 设置窗体的透明度,根据PopupWindow是否打开
     *
     * @param isOpen
     */
    private void setBackAlpha(boolean isOpen) {
        WindowManager.LayoutParams lp = getWindow().getAttributes();
        if (isOpen) {
            lp.alpha = .3f;
        } else {
            lp.alpha = 1.0f;
        }
        getWindow().setAttributes(lp);
    }

    /**
     * 获取被选中的图片的数组
     *
     * @return
     */
    private String[] getSelectImages() {
        List<String> tmp = new ArrayList<String>();
        for (int i = 0; i < imageStates.size(); i++) {
            if (imageStates.get(i)) {
                tmp.add(localImageInfo.getImageFiles().get(i));
            }
        }
        String[] arr = new String[tmp.size()];
        for (int i = 0; i < tmp.size(); i++) {
            arr[i] = tmp.get(i);
        }
        tmp = null;
        return arr;
    }

}

代码比较长,这里慢慢解释,一些简单的和一些初始化的代码就不细讲了,这里就讲一下加载数据的过程
Android 高仿微信图片选择器(瀑布流)
这个run方法是加载系统中保存的图片信息,并且方法中统计好了多少个文件夹,信息都在LocalImageInfo中.
调用的过程上面介绍LocalImageManager的时候已经解释过了
由于是子线程加载的,所以这里会发送一个消息给Handler,来更新UI
而这个任务是在oncreate方法中就被执行了
Android 高仿微信图片选择器(瀑布流)
这里的任务执行的工具类ThreadPool下面会介绍,现在只要知道能执行一个任务,所以程序一打开之后就是显示系统中的图片信息

这里有一个方法:
Android 高仿微信图片选择器(瀑布流)
此方法是点击PopupWindow中的可选文件夹之后被调用的,调用是类似于EventBus的功能的一个类实现的,最后详细说明
这里对鸿洋大神的进行了优化,添加了System选项,这个方法也就是根据点击的路径,加载对应路径的图片,然后显示,实现点击不用可选的文件夹实现切换
Android 高仿微信图片选择器(瀑布流)

选中效果的实现

其实这个功能非常好实现,看我的思路:
我们的图片信息是不是就是一个集合data(List<String>)
而我们建立另一个长度一样的集合imageStatus(List<Boolean>),元素是布尔型的,也就是一个元素对应记录一个图片的选中状态!
所以我们在adapter中加载图片的时候,只需要根据这个集合中对应的选中状态即可知道这张图片是什么状态,如果是true,表示选中状态,显示效果就是图片的右上角的勾勾的小图标会显示,否则就隐藏
还需要注意:
1.在图片信息集合数据加载出来之后,必须对记录选中状态的这个集合进行初始化,否则适配器adapter中会报空指针,并且图片选中状态混乱
Android 高仿微信图片选择器(瀑布流)
2.当点击图片的时候,让选中的状态反以下,何谓反一下?true->false,false->true.懂了吧.然后通知适配器数据改变,那么图片就会出现了选中的效果
ListView的点击事件中写如下代码:
Android 高仿微信图片选择器(瀑布流)

编写弹出窗口ListImageDirPopupWindow

弹出窗口其实里面的内容也是一个xml布局,所以这里先给出布局

弹出窗口的布局xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#ffffff" >

    <ListView
        android:id="@+id/lv_list_dir"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:divider="#EEE3D9"
        android:dividerHeight="1px" >
    </ListView>

</RelativeLayout>

非常简单,就是一个ListView,用来展示所有的可选文件夹,实现后的效果的图为
Android 高仿微信图片选择器(瀑布流)
既然有ListView了肯定有对应的条目Item啦
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="5dp" >

    <ImageView
        android:id="@+id/id_dir_item_image"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:background="@drawable/pic_dir"
        android:paddingBottom="17dp"
        android:paddingLeft="12dp"
        android:paddingRight="12dp"
        android:paddingTop="9dp"
        android:scaleType="fitXY"
        android:src="@mipmap/folder" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10dp"
        android:layout_toRightOf="@id/id_dir_item_image"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/id_dir_item_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="所有图片"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/id_dir_item_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="4723张"
            android:textColor="#444"
            android:textSize="12sp" />
    </LinearLayout>

</RelativeLayout>

一样是很简单的,预览图为
Android 高仿微信图片选择器(瀑布流)
看效果就知道布局很简单的

PopupWindow的实现代码

/**
 * Created by cxj on 2016/5/5.
 */
public class ListImageDirPopupWindow<T> extends PopupWindow implements View.OnTouchListener {

    /**
     * 布局文件的最外层View
     */
    protected View mContentView;

    /**
     * 上下文对象
     */
    protected Context context;

    /**
     * 本地图片的信息
     */
    private LocalImageInfo localImageInfo;


    /**
     * ListView
     */
    private ListView lv = null;

    /**
     * 适配器
     */
    private BaseAdapter adapter;

    /**
     * 显示的数据
     */
    private List<String> data;

    /**
     * 系统中的图片个数
     */
    private int systemImageNum = 0;

    public ListImageDirPopupWindow(View contentView, int width, int height,
                                   boolean focusable, LocalImageInfo localImageInfo) {
        super(contentView, width, height, focusable);

        this.localImageInfo = localImageInfo;


        data = ArrayUtil.setToList(localImageInfo.getImageFolders());
        data.add(0, "System");

        //根布局
        this.mContentView = contentView;
        //上下文
        context = contentView.getContext();

        //设置popupWindow个几个属性
        setBackgroundDrawable(new ColorDrawable(Color.parseColor("#00000000")));
        setTouchable(true);
        setOutsideTouchable(true);
        //触摸的拦截事件
        setTouchInterceptor(this);

        //初始化控件
        initView();

        initData();

        initEvent();

    }

    /**
     * 初始化事件
     */
    private void initEvent() {
        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                //发送纤细让Activity加载指定的文件夹中的图片
                EBus.postEvent(SelectLocalImageAct.TAG, "loadImageByFolderPath", data.get(position));
            }
        });
    }

    /**
     * 初始化数据
     */
    private void initData() {
        //创建适配器
        adapter = new CommonAdapter<String>(context, data, R.layout.list_dir_item) {
            @Override
            public void convert(CommonViewHolder h, String item, int position) {
                //设置目录名称
                h.setText(R.id.id_dir_item_name, item);
                if (position == 0) { //第一个条目不是目录,特殊考虑
                    h.setText(R.id.id_dir_item_count, systemImageNum + "张");
                } else {
                    List<String> list = LocalImageManager.queryImageByFolderPath(localImageInfo,
                            item);
                    h.setText(R.id.id_dir_item_count, list.size() + "张");
                }
            }
        };
        //设置适配器
        lv.setAdapter(adapter);
    }

    /**
     * 通知数据改变
     */
    public void notifyDataSetChanged() {
        List<String> list = ArrayUtil.setToList(localImageInfo.getImageFolders());
        if (systemImageNum == 0) {
            systemImageNum = localImageInfo.getImageFiles().size();
        }
        data.clear();
        data.add("System");
        data.addAll(list);
        list = null;
        adapter.notifyDataSetChanged();
    }

    /**
     * 查找控件封装
     *
     * @param id 控件的id
     * @return
     */
    public View findViewById(int id) {
        return mContentView.findViewById(id);
    }

    /**
     * 初始化控件
     */
    private void initView() {
        lv = (ListView) findViewById(R.id.lv_list_dir);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
            dismiss();
            return true;
        }
        return false;
    }
}


来掰扯掰扯!嗯,好像代码都很简单,其实就是属于弹出窗口的有关的显示的代码都在这里面写了,这是鸿洋大神设计的,我觉得分离的挺好,就只改了几个小地方就保留下来了
这里对外提供了一个通知改变的方法,因为这里的ListView使用的数据集合和整个项目中维护的那个LocalImageInfo中的文件夹集合不是同一个,所以当通知改变的时候,这个ListView使用的集合中的数据会更新,从而更新到UI上显示出来
在activity中集成这个PopupWindow就形成了完整的项目了

编写加载本地图片的压轴戏

1.如何压缩图片

为了抽取代码,所以这里对本地的图片针对性的写了一个图片压缩的工具类
/**
 * 有关图像的工具类
 * 功能1.加载本地图片,压缩的形式
 *
 * @author xiaojinzi
 */
public class ImageUtil {

    /**
     * 加载一个本地的图片
     *
     * @param localImagePath
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public static Bitmap decodeLocalImage(String localImagePath, int reqWidth, int reqHeight) {
        //获取大图的参数,包括大小
        BitmapFactory.Options options = getBitMapOptions(localImagePath);
        //获取一个合适的缩放比例
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight); // 计算inSampleSize
        //让BitmapFactory加载真的图片,等于true的时候只加载了图片的大小
        options.inJustDecodeBounds = false;
        Bitmap src = BitmapFactory.decodeFile(localImagePath, options); // 载入一个稍大的缩略图
        return createScaleBitmap(src, reqWidth, reqHeight); // 进一步得到目标大小的缩略图
    }

    /**
     * 计算inSampleSize,用于压缩图片
     *
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
//    private static int calculateInSampleSize(BitmapFactory.Options options,
//                                             int reqWidth, int reqHeight) {
//        // 源图片的宽度
//        int width = options.outWidth;
//        int height = options.outHeight;
//        int inSampleSize = 1;
//
//        if (width > reqWidth && height > reqHeight) {
//            // 计算出实际宽度和目标宽度的比率
//            int widthRatio = Math.round((float) width / (float) reqWidth);
//            int heightRatio = Math.round((float) width / (float) reqWidth);
//            inSampleSize = Math.max(widthRatio, heightRatio);
//        }
//        return inSampleSize;
//    }
    private static int calculateInSampleSize(BitmapFactory.Options options,
                                             int reqWidth, int reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

    /**
     * 如果是放大图片,filter决定是否平滑,如果是缩小图片,filter无影响
     *
     * @param src
     * @param dstWidth
     * @param dstHeight
     * @return
     */
    private static Bitmap createScaleBitmap(Bitmap src, int dstWidth,
                                            int dstHeight) {
        if (src == null) {
            return null;
        }
        Bitmap dst = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false);
        if (src != dst) { // 如果没有缩放,那么不回收
            src.recycle(); // 释放Bitmap的native像素数组
        }
        return dst;
    }

    /**
     * 获取本地图片的大小
     * require permission:
     * <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     *
     * @param localImagePath
     * @return
     */
    public static BitmapFactory.Options getBitMapOptions(String localImagePath) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        //表示不是真的加载图片的数据,只是计算了图片的大小
        options.inJustDecodeBounds = true;
        //计算出了图片的大小,没有真正的加载
        BitmapFactory.decodeFile(localImagePath, options);
        return options;
    }

    /**
     * 设置bitmap的宽和高
     *
     * @param b
     * @param width
     * @param height
     * @return
     */
    public static Bitmap setBitmap(Bitmap b, int width, int height) {
        return Bitmap.createScaledBitmap(b, width < 1 ? 1 : width, height < 1 ? 1 : height, false);
    }

    /**
     * 获取一个自适应的图片资源
     *
     * @param bitmap
     * @param context
     * @return
     */
    public static Bitmap getResizedBitmap(Bitmap bitmap, Context context) {
        int height = ScreenUtils.getScreenHeight(context);
        int width = ScreenUtils.getScreenWidth(context);

        if (height < 480 && width < 320) {
            return Bitmap.createScaledBitmap(bitmap, 32, 32, false);
        } else if (height < 800 && width < 480) {
            return Bitmap.createScaledBitmap(bitmap, 48, 48, false);
        } else if (height < 1024 && width < 600) {
            return Bitmap.createScaledBitmap(bitmap, 72, 72, false);
        } else {
            return Bitmap.createScaledBitmap(bitmap, 96, 96, false);
        }
    }

}
其中的各个方法都是参考网上搜索的代码然后加以修改完成的
那么现在我们的图片压缩功能就实现了,调用示例:
//获取本地的图片的压缩图
Bitmap bm = ImageUtil.decodeLocalImage(imageLocalPath, 100, 100);
表示把图片会适当的压缩成100*100的,但是并不是完全就是这个值,因为压缩的时候为了尽量避免失真的情况,压缩的比例都是2的次方,所以你这个大小和真的图片的大小的比值不一定是2的次方,所以最后的压缩图的大小是和你传入的大小不一致的,这里做一个说明!

编写图片加载器

那么就开工编写本地的图片加载器LocalImageLoader吧
/**
 * 本地的图片加载器
 */
public class LocalImageLoader {

    private static ThreadPool threadPool;

    /**
     * 自身
     */
    private static LocalImageLoader localImageLoader = null;

    /**
     * 图片缓存的核心类
     */
    private static LruCache<String, Bitmap> mLruCache;

    /**
     * 用于设置图片到控件上
     */
    private Handler h = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
            ImageView imageView = holder.imageView;
            Bitmap bm = holder.bitmap;
            String path = holder.path;
            if (imageView.getTag().toString().equals(path)) {
                imageView.setImageBitmap(bm);
            } else {
            }
        }
    };

    /**
     * 获取一个实例对象
     *
     * @return
     */
    public static synchronized LocalImageLoader getInstance() {
        if (localImageLoader == null) {
            localImageLoader = new LocalImageLoader();
            threadPool = ThreadPool.getInstance();
            // 获取应用程序最大可用内存
            int maxMemory = (int) Runtime.getRuntime().maxMemory();
            int cacheSize = maxMemory / 8;
            mLruCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }
        return localImageLoader;
    }

    public void loadImage(final String imageLocalPath, final ImageView imageView) {

        imageView.setTag(imageLocalPath);

        //从一级缓存中拿
        Bitmap bitmap = mLruCache.get(imageLocalPath);

        if (bitmap == null) {
            //让线程池去执行任务,会限制线程的数量
            threadPool.invoke(new Runnable() {
                @Override
                public void run() {
                    LayoutParams lp = imageView.getLayoutParams();
                    //获取本地的图片的压缩图
                    Bitmap bm = ImageUtil.decodeLocalImage(imageLocalPath, lp.width, lp.height);
                    if (bm != null) {
                        //添加到一级缓存
                        addBitmapToLruCache(imageLocalPath, bm);
                        ImgBeanHolder holder = new ImgBeanHolder();
                        holder.bitmap = mLruCache.get(imageLocalPath);
                        holder.imageView = imageView;
                        holder.path = imageLocalPath;
                        Message message = Message.obtain();
                        message.obj = holder;
                        h.sendMessage(message);
                    }

                }
            });
        } else {
            imageView.setImageBitmap(bitmap);
        }

    }


    /**
     * 往LruCache中添加一张图片
     *
     * @param key
     * @param bitmap
     */
    private void addBitmapToLruCache(String key, Bitmap bitmap) {
        if (mLruCache.get(key) == null) {
            if (bitmap != null)
                mLruCache.put(key, bitmap);
        }
    }


    /**
     * 几个信息的持有者,其实就是封装一下
     */
    private class ImgBeanHolder {
        Bitmap bitmap;
        ImageView imageView;
        String path;
    }


}
代码不长
1.加载的时候都是开启了子线程去加载的
2.并且使用一级缓存,效率更高
3.单例形式
4.控制了子线程的个数(下面介绍的ThreadPool)

到这里位置,整个项目算是完成了,下面是额外的一些介绍

几个工具类的介绍

ThreadPool介绍

是一个控制子线程个数的类似于线程池的概念,任务交给线程池ThreadPool来调度,会自动根据当前线程个数是不是超出了指定的个数,如果是的话,等待其他任务的完成,总的线程个数不超过maxThreadNumber个
/**
 * Created by cxj on 2016/5/5.
 * 线程池,可以执行任务,任务是以{@link Runnable}接口的形式
 * 详情请看{@link ThreadPool#invoke(Runnable)}
 */
public class ThreadPool implements Runnable {

    /**
     * 执行任务完毕停止
     */
    private final int stopped_flag = 0;

    /**
     * 无任务停止
     */
    private final int stoppedWithOutTash_flag = 1;

    //私有化构造函数
    private ThreadPool() {
    }

    private static ThreadPool threadPool;

    /**
     * 用于设置图片到控件上
     */
    private Handler h = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            int what = msg.what;
            switch (what) {
                case stopped_flag: //如果是停止了
                    currentThreadNum--;
                    startThread();
                    break;
                case stoppedWithOutTash_flag:
                    currentThreadNum--;
                    break;
            }
        }
    };

    public static ThreadPool getInstance() {
        if (threadPool == null) {
            threadPool = new ThreadPool();
        }
        return threadPool;
    }

    /**
     * 最大的线程数量
     */
    private int maxThreadNum = 3;

    public void setMaxThreadNum(int maxThreadNum) {
        if (maxThreadNum < 1) {
            maxThreadNum = 1;
        }
        this.maxThreadNum = maxThreadNum;
    }

    /**
     * 当前的线程
     */
    private int currentThreadNum = 0;

    /**
     * 只留的任务的集合
     */
    private Vector<Runnable> runnables = new Vector<Runnable>();


    /**
     * 执行一个任务
     *
     * @param runnable
     */
    public void invoke(Runnable runnable) {
        runnables.add(runnable);
        startThread();
    }

    /**
     * 启动线程
     */
    private void startThread() {
        //如果还没有达到最大的线程数量
        if (currentThreadNum < maxThreadNum) {
            currentThreadNum++;
            Thread t = new Thread(this);
            t.start();
        }
    }

    /**
     * 获取一个任务
     *
     * @return
     */
    @Nullable
    private synchronized Runnable getTask() {
        if (runnables.size() > 0) {
            return runnables.remove(0);
        }
        return null;
    }

    /**
     * 非UI线程执行
     */
    @Override
    public void run() {

        //获取一个任务
        Runnable task = getTask();
        if (task == null) { //如果没有任务,直接返回
            //发送停止的信息
            h.sendEmptyMessage(stoppedWithOutTash_flag);
            return;
        }

        //执行任务
        task.run();

        //发送停止的信息
        h.sendEmptyMessage(stopped_flag);

    }

}
这个明显是可以后续优化升级的,但是这里的情况已经适用了

EBus介绍

代码就不贴出来了,有兴趣的人可以研究一下,用法介绍一下:
//发送纤细让Activity加载指定的文件夹中的图片
EBus.postEvent(SelectLocalImageAct.TAG, "loadImageByFolderPath", data.get(position));
还记得我们的文件夹的选择是在哪里的么?
没错,实在PopupWindow中写的,也就是我们的ListImageDirPopupWindow
这里就是给我们的主Activity发送了一个消息
Android 高仿微信图片选择器(瀑布流)
然后在主Activity中的上图的方法就会被调用,类似于EventBus,不过这个是博主自己封装的,和EventBus一样可以在众多的地方排上用场
activity和fragment的通信,和Service、广播等等
需要接受到消息,你必须做两点:
1.在接受消息之前必须在EBus中注册自己
Android 高仿微信图片选择器(瀑布流)
2.方法的名称必须以"onEvent"开头,后面的部分作为消息传送中的方法名
原型为:public static void postEvent(String tagObjectFalg, final String methodName, final Object... o)
发送消息的时候可以指定对象,也就是tag
也可以不指定,说明只要方法匹配即可
方法不指定的情况下,只要参数列表匹配即可

源码下载


博客花了几天时间的准备,这篇写的真的累,如果你们觉得喜欢、有问题或者想调侃博主,欢迎留言!!!
最后感谢鸿洋大神给我提供的思路

Android 高仿微信图片选择器(瀑布流)

上一篇:Android 微信第三方登录


下一篇:实现QQ、微信、新浪微博和百度第三方登录(Android Studio)