美文网首页
Android仿微信图片选择器(二)

Android仿微信图片选择器(二)

作者: 逸先森 | 来源:发表于2017-05-12 00:04 被阅读0次

    接上一篇:Android仿微信图片选择器(一)

    上一篇介绍了发表界面的编写及数据的处理,这一篇主要介绍图片选择界面的编写。

    老规矩,先上效果图:

    选择图片界面

    一、基础条件

    1. 实体类设计
    public class PhotoFolder {
    
        private String dir;
    
        private String firstPhotoPath;
    
        private String name;
    
        private int count;
    
        public String getDir() {
            return dir;
        }
    
        public void setDir(String dir) {
            this.dir = dir;
            int lastIndexOf = this.dir.lastIndexOf(File.separator);
            this.name = this.dir.substring(lastIndexOf + 1);
        }
    
        public String getFirstPhotoPath() {
            return firstPhotoPath;
        }
    
        public void setFirstPhotoPath(String firstPhotoPath) {
            this.firstPhotoPath = firstPhotoPath;
        }
    
        public String getName() {
            return name;
        }
    
        public int getCount() {
            return count;
        }
    
        public void setCount(int count) {
            this.count = count;
        }
    
    }
    
    2. 工具类
    public class PhotoUtils {
    
        public static List<PhotoFolder> getPhotoes(Context context) {
            Uri photoUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            ContentResolver contentResolver = context.getContentResolver();
            Cursor cursor = contentResolver.query(photoUri, null,
                    MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?",
                    new String[]{"image/jpeg", "image/png"},
                    MediaStore.Images.Media.DATE_MODIFIED);
            String firstImage = null;
            List<PhotoFolder> photoFolders = null;
            HashSet<String> dirPathSet = new HashSet<>();  // 辅助工具
            if (cursor != null) {
                photoFolders = new ArrayList<>();
                while (cursor.moveToNext()) {
                    String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
                    if (firstImage == null) {
                        firstImage = path;
                    }
                    File parentFile = new File(path).getParentFile();
                    if (parentFile == null) {
                        continue;
                    }
                    String dirPath = parentFile.getAbsolutePath();
                    PhotoFolder photoFolder = null;
                    if (dirPathSet.contains(dirPath)) {
                        continue;
                    } else {
                        dirPathSet.add(dirPath);
                        photoFolder = new PhotoFolder();
                        photoFolder.setDir(dirPath);
                        photoFolder.setFirstPhotoPath(path);
                    }
                    if (parentFile.list() == null) {
                        continue;
                    }
                    int photoSize = parentFile.list(new FilenameFilter() {
                        @Override
                        public boolean accept(File file, String fileName) {
                            return fileName.endsWith(".jpg") || fileName.endsWith(".png") || fileName.endsWith(".jpeg");
                        }
                    }).length;
                    photoFolder.setCount(photoSize);
                    photoFolders.add(photoFolder);
                }
                Log.i("PhotoUtils", "photoFolders.size() = " + photoFolders.size());
                cursor.close();
                dirPathSet = null;
            }
            return photoFolders;
        }
    
    }
    

    二、界面设计

    1. 主界面
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/background_dark"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
    
        <include layout="@layout/layout_toolbar" />
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/id_image_grid_view"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
    
        <RelativeLayout
            android:id="@+id/id_bottom_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#515151">
    
            <Button
                android:id="@+id/id_photo_spinner"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="?android:selectableItemBackground"
                android:ellipsize="end"
                android:maxLines="1"
                android:text="所有图片"
                android:textColor="#E0E0E0" />
    
            <Button
                android:id="@+id/id_photo_preview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:background="?android:selectableItemBackground"
                android:text="预览"
                android:textColor="#E0E0E0" />
    
        </RelativeLayout>
    </LinearLayout>
    

    RecyclerView的作用是显示当前选择的文件夹的图片,其中一个按钮的作用是弹出选择文件夹的窗口,一个是预览的按钮。

    先看RecyclerView的item布局,包含一个ImageView和CheckBox。

    <?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="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="4dp">
    
        <ImageView
            android:id="@+id/id_pick_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            tools:src="@drawable/ic_profile" />
    
        <CheckBox
            android:id="@+id/id_select_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true" />
    
    </RelativeLayout>
    

    为RecyclerView编写Adapter,此处有一个坑是ViewHolder的复用机制会导致CheckBox乱序,通常的解决方法是使用一个HashMap来保存CheckBox的选中状态,在使用HashMap<Integer,Boolean>的时候,AS提示使用SparseBooleanArray会有更好的效率,有兴趣的同学可以去百度一下原理,这里就不解释了。但是,结合当前项目的需求,我可以通过点击按钮切换文件夹路径显示不同文件夹的图片,这时复用的机制再次成为一个坑。幸好机智如我,最后通过使用一个HashMap<String,SparseBooleanArray>,为每一个路径创建一个SparseBooleanArray来保存对应路径的CheckBox的选中情况解决了乱序和复用的问题。

    以下是adapter的代码:

    public class PhotoPickAdapter extends AbsRecyclerAdapter<String> {
    
        private Object tag;
        private int mImageWidth;
        private OnItemSelectedListener onItemSelectedListener;
    
        private HashMap<String, SparseBooleanArray> mFolderSelectedMap = new HashMap<>();
        private String mCurrentFolder;
        private SparseBooleanArray mSelectedMap;
    
        public PhotoPickAdapter(Context context, String currentFolder, List<String> list) {
            super(context, list);
            DisplayMetrics metrics = context.getResources().getDisplayMetrics();
            mImageWidth = metrics.widthPixels / 3;
            mCurrentFolder = currentFolder;
            mSelectedMap = new SparseBooleanArray();
            initArray(mSelectedMap, list);
            mFolderSelectedMap.put(mCurrentFolder, mSelectedMap);
        }
    
        @Override
        protected AbsViewHolder createHolder(ViewGroup parent, int viewType) {
            return new ItemViewHolder(mInflater.inflate(R.layout.layout_pick_image_item, parent, false));
        }
    
        @Override
        protected void showViewHolder(AbsViewHolder holder, final int position) {
            mSelectedMap = mFolderSelectedMap.get(mCurrentFolder);
            final ItemViewHolder viewHolder = (ItemViewHolder) holder;
            Picasso.with(mContext)
                    .load(new File(mData.get(position)))
                    .placeholder(R.drawable.ic_place_holder)
                    .error(R.drawable.ic_load_error)
                    .config(Bitmap.Config.RGB_565)
                    .resize(mImageWidth, mImageWidth)
                    .centerCrop()
                    .tag(tag = mData.get(position))
                    .into(viewHolder.image);
            viewHolder.select.setOnCheckedChangeListener(null);
            viewHolder.select.setChecked(mSelectedMap.get(position));
            viewHolder.select.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                    mSelectedMap.put(position, b);
                    if (b) {
                        if (onItemSelectedListener != null) {
                            onItemSelectedListener.onChecked(compoundButton, mData.get(position));
                        }
                    } else {
                        if (onItemSelectedListener != null) {
                            onItemSelectedListener.onRemoved(mData.get(position));
                        }
                    }
                }
            });
        }
    
        public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
            this.onItemSelectedListener = onItemSelectedListener;
        }
    
        public interface OnItemSelectedListener {
    
            void onChecked(CompoundButton compoundButton, String image);
    
            void onRemoved(String image);
    
        }
    
        public Object getTag() {
            return tag;
        }
    
        public void setCurrentFolder(String folder, List<String> data) {
            LogUtils.e("PickAdapter", "current folder" + folder);
            if (!mFolderSelectedMap.containsKey(folder)) {
                SparseBooleanArray array = new SparseBooleanArray();
                initArray(array, data);
                mFolderSelectedMap.put(folder, array);
            }
            mCurrentFolder = folder;
            mSelectedMap = mFolderSelectedMap.get(mCurrentFolder);
            mData.clear();
            mData.addAll(data);
            notifyDataSetChanged();
        }
    
        private void initArray(SparseBooleanArray array, List<String> data) {
            for (int i = 0; i < data.size(); i++) {
                array.put(i, false);
            }
        }
    
        private static class ItemViewHolder extends AbsViewHolder {
    
            ImageView image;
            CheckBox select;
    
            ItemViewHolder(View itemView) {
                super(itemView);
                image = (ImageView) itemView.findViewById(R.id.id_pick_image);
                select = (CheckBox) itemView.findViewById(R.id.id_select_image);
            }
        }
    
    }7
    

    其中,OnItemSelectedListener的作用是为了把CheckBox的选中事件监听回调到Activity中,让Activity去处理相应的数据和逻辑。setCurrentFolder()是一个关键的方法,通过该方法可以为当前路径创建一个SparseBooleanArray来保存CheckBox的选中状态。adapter中的tag的作用是在RecyclerView滚动的时候可以通过tag来控制是否暂停加载图片,加快响应速度。

    2. 弹出窗口设计

    先看弹出窗口的效果图:

    文件夹路径选择界面

    该效果通过一个PopupWindow实现,该PopupWindow布局仅包括一个RecyclerView。实现代码如下:

    public class PhotoSpinnerWindow extends PopupWindow {
    
        public PhotoSpinnerWindow(Context context, final List<PhotoFolder> list, final OnItemSelectedListener listener) {
            LayoutInflater inflater = LayoutInflater.from(context);
            RecyclerView view = new RecyclerView(context);
            view.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            view.setLayoutManager(new LinearLayoutManager(context));
            PhotoFolderAdapter adapter = new PhotoFolderAdapter(context, list);
            view.setAdapter(adapter);
            adapter.setOnItemClickListener(new AbsRecyclerAdapter.DefaultItemClickListener() {
    
                @Override
                public void onClick(View view, int position) {
                    String dir = list.get(position).getDir();
                    String name = list.get(position).getName();
                    File file = new File(dir);
                    if (file.list() != null) {
                        List<String> images = new ArrayList<>();
                        for (String path : file.list()) {
                            images.add(list.get(position).getDir() + File.separator + path);
                        }
                        if (listener != null) {
                            listener.onSelected(view, dir, name, images);
                        }
                    }
                }
            });
            this.setContentView(view);
            this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
            this.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
            this.setFocusable(true);
            this.setOutsideTouchable(true);
            ColorDrawable bd = new ColorDrawable(0xb0000000);
            this.setBackgroundDrawable(bd);
            this.setAnimationStyle(R.style.bottom_popup_anim);
        }
    
        public interface OnItemSelectedListener {
            void onSelected(View view, String dir, String name, List<String> images);
        }
    
    }
    

    在该PopupWindow中有一个OnItemSelectedListener,主要作用是将选中的路径下的图片的路径列表回调到Activity进行处理。PhotoFolderAdapter是该RecyclerView的适配器,具体实现如下:

    public class PhotoFolderAdapter extends AbsRecyclerAdapter<PhotoFolder> {
    
        public PhotoFolderAdapter(Context context, List<PhotoFolder> list) {
            super(context, list);
        }
    
        @Override
        protected AbsViewHolder createHolder(ViewGroup parent, int viewType) {
            return new ItemViewHolder(mInflater.inflate(R.layout.layout_photo_spinner_item, parent, false));
        }
    
        @Override
        protected void showViewHolder(AbsViewHolder holder, int position) {
            ItemViewHolder viewHolder = (ItemViewHolder) holder;
            viewHolder.dir.setText(mData.get(position).getName());
            viewHolder.count.setText(mData.get(position).getCount() + "张");
            Picasso.with(mContext)
                    .load(new File(mData.get(position).getFirstPhotoPath()))
                    .placeholder(R.drawable.ic_place_holder)
                    .error(R.drawable.ic_load_error)
                    .config(Bitmap.Config.RGB_565)
                    .into(viewHolder.image);
        }
    
        private static class ItemViewHolder extends AbsViewHolder {
    
            ImageView image;
            TextView dir;
            TextView count;
    
            ItemViewHolder(View itemView) {
                super(itemView);
                image = (ImageView) itemView.findViewById(R.id.id_spinner_image);
                dir = (TextView) itemView.findViewById(R.id.id_spinner_dir);
                count = (TextView) itemView.findViewById(R.id.id_spinner_count);
            }
        }
    }
    

    对应的布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/white_item"
        android:orientation="horizontal"
        android:padding="@dimen/activity_margin">
    
        <ImageView
            android:id="@+id/id_spinner_image"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:scaleType="centerCrop"
            tools:src="@drawable/ic_profile" />
    
        <TextView
            android:id="@+id/id_spinner_dir"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_weight="1"
            android:textColor="#2c2c2c"
            android:textSize="18sp"
            tools:text="WeiXin" />
    
        <TextView
            android:id="@+id/id_spinner_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="16sp"
            tools:text="533张" />
    
    </LinearLayout>
    

    至此,所有界面设计完成,接下来就是最核心的数据处理逻辑和功能实现。

    三、功能实现

    本项目是基于MVP模式实现的,为了简便实现和展示该功能,代码中并不完全符合MVP的设计。

    1. 接口定义

    公共接口定义:

    public interface RequestCallback<T> {
    
        void onSuccess(T t);
    
        void onFailure(String message);
    }
    

    获取图片接口定义:

    public interface IPhotoPickModel {
    
        void getPhotoes(Context context, RequestCallback<List<PhotoFolder>> callback);
    }
    

    具体实现如下:

    public class PhotoPickModelImpl implements IPhotoPickModel {
    
        @Override
        public void getPhotoes(final Context context, final RequestCallback<List<PhotoFolder>> callback) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    final List<PhotoFolder> list = PhotoUtils.getPhotoes(context);
                    if (list != null) {
                        if (callback != null) {
                            new Handler(Looper.getMainLooper()).post(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onSuccess(list);
                                }
                            });
                        }
                    } else {
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                callback.onFailure("unknown error");
                            }
                        });
                    }
                }
            }).start();
        }
    }
    

    因为查找本机图片是一个耗时的操作,所以我把它放到子线程中去处理,当获取到结果时,通过Handler把数据回调到主线程。

    2. 数据处理

    由于不完全按照MVP设计来,为了演示方便,并没有设计Presenter层去关联View和Model层,这里直接在View层使用Model层的接口,也就是在Activity中直接调用Model的方法。具体代码如下:

        private void loadImage() {
            IPhotoPickModel model = new PhotoPickModelImpl();
            model.getPhotoes(BasicApplication.getApplication(), new RequestCallback<List<PhotoFolder>>() {
                @Override
                public void onSuccess(List<PhotoFolder> photoFolders) {
                    LogUtils.i("getPhotoList");
                    mPhotoFolderList.clear();
                    mPhotoFolderList.addAll(photoFolders);
                    mPhotoFolderAdapter.notifyDataSetChanged();
                    // 设置默认显示
                    String dir = photoFolders.get(0).getDir();
                    String name = photoFolders.get(0).getName();
                    mSpinnerButton.setText(name);
                    File file = new File(dir);
                    if (file.list() != null) {
                        List<String> images = new ArrayList<>();
                        for (String path : file.list()) {
                            images.add(dir + File.separator + path);
                        }
                        mPhotoPickAdapter.setCurrentFolder(dir, images);
                    }
                }
    
                @Override
                public void onFailure(String message) {
                    ToastUtils.showShort(BasicApplication.getApplication(), message);
                }
            });
        }
    

    Bean类的设计是保存文件夹路径和文件夹下第一张图片的路径,这样做是为了把路径和图片分开,提高效率。Model层回调的数据是PopupWindow中的RecyclerView展示所需要的数据,所以要把数据填充到PhotoFolderAdapter中,然后默认取第一个文件夹的图片展示到界面上。

    接下来我遇到了一个坑,一个没注意到的细节。因为Android6.0系统的特性,某些权限需要动态申请,而获取手机图片就是一个读取用户隐私信息的行为,需要用户授权方可继续。这时候我又去学习了一波动态权限申请的知识,然后顺利解决了这个问题。直接上代码:

        private static final int EXTERNAL_STORAGE_PERMISSION_CODE = 1000;
    
        private void getPermission() {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(PhotoPickActivity.this,
                        new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                        EXTERNAL_STORAGE_PERMISSION_CODE);
            } else {
                loadImage();
            }
        }
    
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            if (requestCode == EXTERNAL_STORAGE_PERMISSION_CODE) {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    loadImage();
                } else {
                    showMessage("未授权");
                }
            }
        }
    

    数据处理大致就到这里了,接下来是介绍一些逻辑处理,如选取不同文件夹的逻辑处理,图片选择个数的逻辑处理。

    3. 逻辑处理

    因为文件夹的选取是在PopupWindow中处理的,所以这里的逻辑主要是在PopupWindow中。具体看代码:

        private void initPhotoWindow() {
            mPhotoFolderList = new ArrayList<>();
            mPhotoFolderAdapter = new PhotoFolderAdapter(this, mPhotoFolderList);
            mPhotoSpinnerWindow = new PhotoSpinnerWindow(this, mPhotoFolderList, new PhotoSpinnerWindow.OnItemSelectedListener() {
                @Override
                public void onSelected(View view, String dir, String name, List<String> images) {         
                    mSpinnerButton.setText(name);
                    mPhotoPickAdapter.setCurrentFolder(dir, images);
                    mPhotoSpinnerWindow.dismiss();
                }
            });
        }
    

    因为在PopupWindow中做了数据的出来,回调的数据就是要显示到界面上的数据,所以将数据填充到adapter中,即调用PhotoPickAdapter.setCurrentFolder(dir, images)方法。

    对于图片个数的限制,主要是对CheckBox监听回调的处理。先看代码:

        mPhotoPickAdapter.setOnItemSelectedListener(new PhotoPickAdapter.OnItemSelectedListener() {
                @Override
                public void onChecked(CompoundButton compoundButton, String image) {
                    if (check(compoundButton)) {
                        mSelectedPhotos.add(image);
                    }
                    checkSelectedPhotoCount();
                }
    
                @Override
                public void onRemoved(String image) {
                    mSelectedPhotos.remove(image);
                    checkSelectedPhotoCount();
                }
            });
    

    在监听回调中有两个判断方法,主要就是处理选取张数的逻辑,check()的作用是控制CheckBox状态,checkSelectedPhotoCount()控制预览按钮的可用以及选取的张数个数的显示。具体代码如下:

        private void checkSelectedPhotoCount() {
            if (mSelectedPhotos == null) return;
            if (mSelectedPhotos.size() == 0) {
                mPreviewButton.setText("预览");
                mPreviewButton.setEnabled(false);
            } else {
                mPreviewButton.setEnabled(true);
                mPreviewButton.setText(String.format(Locale.getDefault(), "预览(%d)", mSelectedPhotos.size()));
            }
        }
    
        private boolean check(CompoundButton compoundButton) {
            if (mSelectedPhotos.size() + 1 > mSelectedCount) {
                compoundButton.setChecked(false);
                showMessage(String.format(Locale.getDefault(), "您最多能选择%d张图片", mSelectedCount));
                return false;
            }
            return true;
        }
    

    图片可选数量由mSelectedCount控制,该参数由启动该Activity的Activity觉得,该Activity向外提供一个方法进行调用:

        public static void startActivityForResult(Activity context, int requestCode, int resultCode, int selectedCount) {
            mResultCode = resultCode;
            mSelectedCount = selectedCount;
            Intent intent = new Intent(context, PhotoPickActivity.class);
            context.startActivityForResult(intent, requestCode);
        }
    

    至此,图片选择的功能和核心代码已经介绍完毕,接下来一篇博客是介绍预览界面的实现。

    Android仿微信图片选择器(三)

    相关文章

      网友评论

          本文标题:Android仿微信图片选择器(二)

          本文链接:https://www.haomeiwen.com/subject/hiaotxtx.html