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);
}