作者 | ReturnYHH
地址 | http://www.jianshu.com/p/c82d4ab45c20
1
前言
最近公司需要做一个类似微信那种选择头像和上传图片的功能,本想上 github 上找的,后来想了想,还是自己做一个,不仅方便以后用(毕竟自己写的修改起来也比较方便),还可以学到一些知识,废话少说,先看效果图:
(ps:大致的功能就这样,拍照功能就是拍了照片之后跳到剪切的页面,这里没有截出来,主要是为了压缩下 gif 图的大小)
现在来简单的介绍下有什么功能:
可根据传入的值控制是选择头像还是上传图片,大于1就是选择图片,等于1救是选择头像
可根据传入的值控制选择图片的数量,并且当选中的图片数量等于这个数量时,则其他没有被选中的图片变成不可选择
可选择不同文件夹下的图片
自定义 ViewGroup 显示选中的图片数,不同的数量显示的格式不一样(gif 最后那一帧录制的不是很清楚),类似微信那种,1 张,2 张还是 9 张显示的格式不同
拍照剪切头像
大概下就是这么些,现在我们来说说实现的思路,主要分为几步
在子线程中读出 sd 卡下所有的文件夹下的图片,并且在 RecyclerView 中显示出来
RecyclerView 采用多 item 布局方式,分开拍照和图片,主要是方便修改拍照的view,这里只是用图片显示
适配每个图片等宽度和高度为屏幕宽度等三分之一
底部采用 PopupWindow 显示出所有的图片所在的文件夹
如果是多选图片,则为每个 view 添加 checkbox 的选中监听,否则就调用系统的剪切图片功能,剪切完成之后显示出来
根据选中的图片数,展示不同的布局
好了,现在我们来看看代码
2
ImageSelectActivity
这个 activity 主要是用来显示从 sd 中读取出来的图片
public class ImageSelectActivity extends AppCompatActivity implements OnItemClickListener, OnChangeListener, View.OnClickListener, PopupWindow.OnDismissListener, OnImageDirItemListener { private static final int PHOTO_REQUEST_CAMERA = 1;// 拍照 private static final int PHOTO_REQUEST_CUT = 2;// 结果 private ProgressDialog mProgressDialog; /** * 存储文件夹中的图片数量 */ private int mPicsSize; /** * 扫描拿到所有的图片文件夹 */ private List<ImageDirBean> imageDirBeans = new ArrayList<>(); /** * 图片数量 */ private int totalCount = 0; /** * 临时的辅助类,用于防止同一个文件夹的多次扫描 */ private HashSet<String> mDirPaths = new HashSet<>(); /** * 所有的图片 */ private List<ImageBean> mImages = new ArrayList<>(); /** * 选中的图片集合 */ private ArrayList<ImageBean> mSelectImages = new ArrayList<>(); /** * 最大的图片数 */ private int maxImageCount = 9; /** * 屏幕高度 */ private int mScreenHeight; /** * 用来存储选中的文件 */ private File mSelectFile; /** * 用于显示全部文件夹 */ private PopupWindow mPopupWindow; /** * 当PopupWindow显示或者消失时改变背景色 */ private WindowManager.LayoutParams lp; /** * 拿到传过来的值,测试选择图片 */ private int select; /** * 存储拍照和选中的图片 */ private File file; private ImageDirBean imageDirBean; private ImageBean imageBean; private RecyclerView rcyImageSelect; private TextView tvImageCount; private TextView tvImageDir; private TextView tvConfirm; private LinearLayout linearLayout; private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { mProgressDialog.dismiss(); //绑定数据 setData(); } }; private MyAdapter mAdapter; private void setData() { mAdapter = new MyAdapter(this, maxImageCount, mImages, this, this); rcyImageSelect.setAdapter(mAdapter); rcyImageSelect.addItemDecoration(new SpacesItemDecoration(2)); tvImageCount.setText(totalCount + "张"); } //图片文件数据 private void setImageDirData() { if (!imageDirBeans.isEmpty()) { View contentView = LayoutInflater.from(this).inflate(R.layout.image_dir_list, null); RecyclerView rcyViewImageDir = (RecyclerView) contentView.findViewById(R.id.rcyView_imageDir); rcyViewImageDir.setLayoutManager(new LinearLayoutManager(this)); rcyViewImageDir.setAdapter(new ImageDirAdapter(this, imageDirBeans, this)); mPopupWindow.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); mPopupWindow.setHeight((int) (mScreenHeight * 0.7f)); mPopupWindow.setContentView(contentView); mPopupWindow.setOutsideTouchable(true); mPopupWindow.setFocusable(true); mPopupWindow.showAsDropDown(linearLayout, 0, 0); // 设置背景颜色变暗 lp.alpha = 0.5f; getWindow().setAttributes(lp); } } private void getImageList() { //判断是否有内存卡 if (!Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { Toast.makeText(this, "暂无外部存储", Toast.LENGTH_SHORT).show(); } else { mProgressDialog = new ProgressDialog(this); mProgressDialog.setMessage("正在加载..."); mProgressDialog.show(); //在子线程中读取最多图片的集合 new Thread(new Runnable() { @Override public void run() { //第一张图片 String firstImage = null; //获取内存卡路径 Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; //通过内容解析器解析出png和jpeg格式的图片 ContentResolver mContentResolver = ImageSelectActivity.this .getContentResolver(); Cursor mCursor = mContentResolver.query(mImageUri, null, MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?", new String[]{"image/png", "image/jpeg"}, MediaStore.Images.Media.DATE_MODIFIED); //判断是否存在图片 if (mCursor.getCount() > 0) { while (mCursor.moveToNext()) { // 获取图片的路径 String path = mCursor.getString(mCursor .getColumnIndex(MediaStore.Images.Media.DATA)); // 拿到第一张图片的路径 if (firstImage == null) firstImage = path; // 获取该图片的文件名 File parentFile = new File(path).getParentFile(); if (parentFile == null) continue; //获取到文件地址 String dirPath = parentFile.getAbsolutePath(); imageBean = new ImageBean(); imageBean.setPath(path); mImages.add(imageBean); if (mDirPaths.contains(dirPath)) { continue; } else { mDirPaths.add(dirPath); imageDirBean = new ImageDirBean(); imageDirBean.setDir(dirPath); imageDirBean.setImagePath(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; imageDirBean.setImageCount(picSize); imageDirBeans.add(imageDirBean); if (picSize > mPicsSize) { mPicsSize = picSize; } } mCursor.close(); mDirPaths = null; // 通知Handler扫描图片完成 mHandler.sendEmptyMessage(0x110); } } }).start(); } } @Override public void onItemClickListener(View view, int position) { if (position != 0) { if (maxImageCount == 1) { clipPhoto(Uri.fromFile(new File(mImages.get(position).getPath())), PHOTO_REQUEST_CUT);//开始裁减图片 } else { Toast.makeText(this, position + "", Toast.LENGTH_SHORT).show(); } } else if (select == 1) { Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 下面这句指定调用相机拍照后的照片存储的路径 cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)); startActivityForResult(cameraIntent, PHOTO_REQUEST_CAMERA);// CAMERA_OK是用作判断返回结果的标识 } } @Override public void OnChangeListener(int position, boolean isChecked) { if (isChecked) { mImages.get(position).setSelect(true); if (!contains(mSelectImages, mImages.get(position))) { mSelectImages.add(mImages.get(position)); if (mSelectImages.size() == maxImageCount) { mAdapter.notifyData(mSelectImages); } } } else { mImages.get(position).setSelect(false); if (contains(mSelectImages, mImages.get(position))) { mSelectImages.remove(mImages.get(position)); if (mSelectImages.size() == maxImageCount - 1) { mAdapter.notifyData(mSelectImages); } } } tvConfirm.setText("确定" + mSelectImages.size() + "/" + maxImageCount); } private boolean contains(List<ImageBean> list, ImageBean imageBean) { for (ImageBean bean : list) { if (bean.getPath().equals(imageBean.getPath())) { return true; } } return false; } @Override public void onClick(View v) { if (v.getId() == R.id.tv_imageDir) { setImageDirData(); } else if (v.getId() == R.id.tv_confirm) { if (mSelectImages.size() != 0) { Intent intent = new Intent(); intent.putParcelableArrayListExtra("selectImages", mSelectImages); setResult(Activity.RESULT_OK, intent); finish(); } else { Toast.makeText(this, "请选择至少一张图片", Toast.LENGTH_SHORT).show(); } } } @Override public void onDismiss() { // 设置背景颜色变暗 lp = getWindow().getAttributes(); lp.alpha = 1.0f; getWindow().setAttributes(lp); } @Override public void onImageDirItemListener(View view, int position) { mImages.clear(); mImages.add(null); if (mSelectImages.size() > 0) { mSelectImages.clear(); } String dir = imageDirBeans.get(position).getDir(); mSelectFile = new File(dir); List<String> imagePath = Arrays.asList(mSelectFile.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; } })); for (int i = 0; i < imagePath.size(); i++) { imageBean = new ImageBean(); imageBean.setPath(dir + "/" + imagePath.get(i)); imageBean.setSelect(false); mImages.add(imageBean); } tvImageDir.setText(imageDirBeans.get(position).getImageName()); tvImageCount.setText(imageDirBeans.get(position).getImageCount() + "张"); mAdapter.notifyDataSetChanged(); mPopupWindow.dismiss(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) switch (requestCode) { case PHOTO_REQUEST_CAMERA: clipPhoto(Uri.fromFile(file), PHOTO_REQUEST_CAMERA);//开始裁减图片 break; case PHOTO_REQUEST_CUT: Bitmap bitmap = data.getParcelableExtra("data"); Intent intent = new Intent(); ByteArrayOutputStream bs = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, bs); byte[] bitmapByte = bs.toByteArray(); intent.putExtra("bitmap", bitmapByte); setResult(RESULT_OK, intent); finish(); break; } }
}
我们在子线程中读取到 sd 卡中的文件夹已经文件夹下的图片,并且用两个bean 来记录他们,当操作完成之后,通过 handler 来设置给 RecyclerView,我们在来看看 adapter
3
MyAdapter
@Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) { if (viewHolder instanceof MyViewHolder) { MyViewHolder holder = (MyViewHolder) viewHolder; holder.cbSelect.setVisibility(View.VISIBLE); Glide.with(mContext).load(mList.get(position).getPath()).into(holder.imageView); if (mList.get(position).isSelect()) { holder.cbSelect.setChecked(true); holder.canSelect(); } else { if (mSelectImages == null) { holder.canSelect(); } else if (mSelectImages.size() == mMaxImageCount) { holder.cannotSelect(); } else { holder.canSelect(); } holder.cbSelect.setChecked(false); } if (mMaxImageCount == 1) { holder.cbSelect.setVisibility(View.GONE); } } }
很简单,onBindViewHolder 里主要做的就是根据 ImageBean 里的 isSelect 来判断图片是否选中,在 ViewHolder 里对图片进行适配,选择完成之后,根据不同数量的图片显示不同的布局,来看看代码
4
NineImageView
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //onLayout会被调用多次,为了预防重叠 mAllViews.clear(); mLineHeight.clear(); //获取总宽度 int width = getWidth(); //单行宽度和当行高度 int lineWidth = 0; int lineHeight = 0; // 存储每一行所有的childView List<View> childViews = new ArrayList<>(); int childCount = getChildCount(); // 遍历所有的子view for (int i = 0; i < childCount; i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); // 如果已经需要换行 if (childWidth + lp.leftMargin + lp.rightMargin + lineWidth > width) { // 记录这一行所有的View以及最大高度 mLineHeight.add(lineHeight); // 将当前行的childView保存,然后开启新的ArrayList保存下一行的childView mAllViews.add(childViews); lineWidth = 0;// 重置行宽 childViews = new ArrayList<>(); } // 如果不需要换行,则累加 lineWidth += childWidth + lp.leftMargin + lp.rightMargin; lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin); childViews.add(child); } // 记录最后一行 mLineHeight.add(lineHeight); mAllViews.add(childViews); int left = getPaddingLeft(); int top = getPaddingTop(); // 得到总行数 int lineNum = mAllViews.size(); for (int i = 0; i < lineNum; i++) { // 每一行的所有的views childViews = mAllViews.get(i); // 当前行的最大高度 lineHeight = mLineHeight.get(i); // 遍历当前行所有的子View for (int j = 0; j < childViews.size(); j++) { View child = childViews.get(j); if (child.getVisibility() != View.GONE) { MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); //计算childView的left,top,right,bottom int childLeft = left + lp.leftMargin; int childTop = top + lp.topMargin; int childRight = childLeft + child.getMeasuredWidth(); int childBottom = childTop + child.getMeasuredHeight(); child.layout(childLeft, childTop, childRight, childBottom); left += child.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; } } //换行后,重新从第一个开始,高度累加 left = getPaddingTop(); top += lineHeight; } }
因为我们是根据数量来显示图片布局,也就是说需要自定义的说 ViewGroup,onMeasure方法里,根据子 view 的数量来测量出布局的宽高,它的宽高说由子 view 的宽高以及数量决定,onLayout 就是根据不同的位置来摆放子 view,具体思路可以看看我的博客http://www.jianshu.com/p/730333c61ea3
这篇博客就是当初写这个自定义 ViewGroup 时写的,感兴趣的可以去看看
最后我们来看看MainActivity
5
MainActivity
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if(resultCode==Activity.RESULT_OK){ switch (requestCode){ //多图 case 0: Bundle bundle = data.getExtras(); ArrayList<ImageBean> list = bundle.getParcelableArrayList("selectImages"); LayoutInflater inflater = LayoutInflater.from(this); //避免重复添加 if (mNineImageView.getChildCount() > 0) { mNineImageView.removeAllViews(); } for (int i = 0; i < list.size(); i++) { ImageView imageView = (ImageView) inflater.inflate(R.layout.nine_image, mNineImageView, false); Glide.with(this).load(list.get(i).getPath()).into(imageView); mNineImageView.addView(imageView); } break; //头像 case 1: byte [] bitmap=data.getByteArrayExtra("bitmap"); Glide.with(this).load(bitmap).into(imageView); break; } } }}
不难,相信大家都很容易理解,这里不做过多的解析
最后说说两点需要注意的地方:
如何处理选中图片,图片数量达到最大可选数和取消选中图片的时候,RecyclerView刷新时数据不会错乱
如何处理 RecyclerView 复用时,数据不会错乱
其实实现的思路时一样,给bean对象添加一个子段,记录当前图片等状态,根据状态来改变 view 等状态,相信有些人会遇到 checkbox 复用等时候到坑,当 RecyclerView 复用的时候,checkbox 的 OnChangeListener 是一定会触发,它有两个状态,选中和没选中,所以,我们需要在复用等时候去做处理,个人认为最好的办法就是在 bean 中记录状态,不仅能保证数据的正确性,也是最容易处理,万物皆对象嘛
最后附上 github 地址,感兴趣的可以下载来看下,有问题欢迎提出
(ps:这里没有做 6.0 权限处理,自己可以添加上去,记得添加权限)
https://github.com/ReturnYhh/ImageSelectContainer
Android之高仿微信朋友圈图片上传(推荐参考)
code小生
简书:http://www.jianshu.com/u/645342ca3cad
csdn:http://blog.csdn.net/wufeng55
分享技术
程序员不再猿