Android——一次选取本地相册多张图片
由于项目需求,需要上传一些图片信息到服务器,以前的方案是采用H5标签,但是只能一次选择一张图片,不太方便,需要一次选择多张图片上传,采用的方案是使用ContentResolver查询本地相册图片信息,然后采用GridView(ImageView+CheckBox)进行展示,同时,采用MVP模式进行开发,下面是效果图:
首先是模型类(M):
public class PhotoItem implements Parcelable { public String id; public String path; public boolean isChecked = false; public PhotoItem() { } public PhotoItem(String id, String path) { this.id = id; this.path = path; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.id); dest.writeString(this.path); dest.writeByte(this.isChecked ? (byte) 1 : (byte) 0); } protected PhotoItem(Parcel in) { this.id = in.readString(); this.path = in.readString(); this.isChecked = in.readByte() != 0; } public static final Parcelable.Creator<PhotoItem> CREATOR = new Parcelable.Creator<PhotoItem>() { @Override public PhotoItem createFromParcel(Parcel source) { return new PhotoItem(source); } @Override public PhotoItem[] newArray(int size) { return new PhotoItem[size]; } }; }
上传图片需要图片文件路径,同时需要要id去区别不同的图片,isChecked表示图片是否被选中上传。需要实现序列化接口,便于不同Activity之间的传递。
然后是View类,主要是PhotoPickActivity和IPhotoPickActivity接口,IPhotoPickActivity主要有四个方法:
public interface IPhotoPickActivity { void showDialog(); void hideDialog(); void showErrorMessage(String errorMsg); void showPhotos(List<PhotoItem> photoItems); }
分别为显示加载Dialog、隐藏加载Dialog、显示错误信息以及显示从相册中查询到的照片。PhotoPickActivity需要实现这个接口。
PhotoPickActivity的布局如下:
<?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="#393A3F" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageButton android:id="@+id/btn_back" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:background="@drawable/btn_back_icon"/> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginBottom="10dp" android:layout_marginLeft="20dp" android:layout_marginTop="10dp" android:layout_toRightOf="@+id/btn_back" android:text="相册图片" android:textColor="@color/white" android:textSize="20sp"/> <Button android:id="@+id/btn_send_photo" android:layout_width="70dp" android:layout_height="35dp" android:layout_alignParentRight="true" android:layout_margin="7dp" android:background="@drawable/btn_green_selector" android:text="发送" android:textColor="@color/white" android:textSize="16sp"/> </RelativeLayout> <GridView android:id="@+id/gdv_photo" android:layout_width="match_parent" android:layout_height="match_parent" android:horizontalSpacing="2dp" android:numColumns="3" android:stretchMode="columnWidth" android:verticalSpacing="2dp"/> </LinearLayout>
主要为上面一个TitleBar加下面一个GridView。
业务逻辑类为PhotoBiz,在其中是查询相册信息,使用ContentResolver,查询的URI为: MediaStore.Images.Media.EXTERNAL_CONTENT_URI,在使用之前需要判断外置存储是否存在,查询出的结果按照拍摄时间降序排列:
public class PhotoBiz { private Context mContext; public PhotoBiz(@NonNull Context context) { mContext = context; } public List<PhotoItem> getPhotoItems() { List<PhotoItem> items = new ArrayList<PhotoItem>(); if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && !Environment.isExternalStorageRemovable()) { ContentResolver contentResolver = mContext.getContentResolver(); Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; //根据拍摄时间降序排列 Cursor cursor = contentResolver.query(uri, null, null, null, MediaStore.Images.Media.DATE_TAKEN + " DESC"); if (cursor != null) { while (cursor.moveToNext()) { String id = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID)); String data = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); PhotoItem photoItem = new PhotoItem(id, data); items.add(photoItem); } } } return items; } }
PhotoPickPresenter为展示类(P),主要是关联业务逻辑类和View,如下:
public class PhotoPickPresenter { private Context mContext; private IPhotoPickActivity mIPhotoPickActivity; private PhotoBiz mPhotoBiz; private static final int MSG_GET_PHOTOS_SUC = 1; private static final int MSG_GET_PHOTOS_ERROR = -1; private List<PhotoItem> mPhotoItems; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_GET_PHOTOS_ERROR: mIPhotoPickActivity.hideDialog(); String errorMsg = (String) msg.obj; mIPhotoPickActivity.showErrorMessage(errorMsg); break; case MSG_GET_PHOTOS_SUC: mIPhotoPickActivity.hideDialog(); mIPhotoPickActivity.showPhotos(mPhotoItems); break; } } }; public PhotoPickPresenter(Context context, IPhotoPickActivity iPhotoPickActivity) { mContext = context; mIPhotoPickActivity = iPhotoPickActivity; mPhotoBiz = new PhotoBiz(context); } public void getData() { mIPhotoPickActivity.showDialog(); ThreadPoolManager.getInstance().getThreadPool().execute(new Thread(new Runnable() { @Override public void run() { mPhotoItems = mPhotoBiz.getPhotoItems(); if (mPhotoItems == null || mPhotoItems.size() == 0) { Message message = Message.obtain(); message.what = MSG_GET_PHOTOS_ERROR; message.obj = "无法获取用户相册图片信息!"; mHandler.sendMessage(message); } else { mHandler.sendEmptyMessage(MSG_GET_PHOTOS_SUC); } } })); } }
需要注意的问题:
1、GridView每个Item宽度的问题? 采用三等分,由于设置了每个Item之间的间隔为2dp,因此,计算如下:
a.首先获取屏幕宽度screenWidth,单位为像素‘
b.itemsize = (screenWidth - (2 * density + 0.5f )) / 3,其中density为屏幕密度,为屏幕dpi/160,获取方法如下:
context.getApplicationContext().getResources().getDisplayMetrics().density;
c.动态设置宽度和高度:
final AbsListView.LayoutParams params = new AbsListView.LayoutParams(mPhotoSize, mPhotoSize); convertView.setLayoutParams(params);
2、GridView复用convertView导致CheckBox错乱的问题? 使用PhotoItem的isChecked去标识对应的CheckBox是否被选中,在设置OnCheckedChangeListener之后,根据PhotoItem的isChecked标识从新设置CheckBox的选中状态:
viewHolder.mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { photoItem.isChecked = isChecked; } }); viewHolder.mCheckBox.setChecked(photoItem.isChecked);
3.动态设置convertView导致GridView第一个Item无法响应? 解决方案:在convertView第一次初始化时(在if之内设置),动态设置convertView的宽度和高度,而不是每次都设置:
if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.item_photo, null); //要在这里进行设置,不然每次都会动态的设置convertView宽高,导致第一个item无法响应或点击 final AbsListView.LayoutParams params = new AbsListView.LayoutParams(mPhotoSize, mPhotoSize); convertView.setLayoutParams(params); viewHolder = new ViewHolder(); viewHolder.mImageView = (ImageView) convertView.findViewById(R.id.iv_photo); viewHolder.mCheckBox = (CheckBox) convertView.findViewById(R.id.cb_photo); convertView.setTag(viewHolder); }