GalleryFinal GitHub [使用方法]
相信做过照片选择的童鞋都会觉得比较痛苦, 由于手机Rom差异化很大, 导致使用原生的相机/相册可能出现很多兼容性问题. 比如在这几款手机上是正常的, 到了另外一个手机就会闪退. 还有部分手机照片被翻转, MIUI当年与众不同的Uri等等各种问题. 为了避免这些问题再继续折磨我们, 我选用了一个开源的第三方库GalleryFinal, 链接在最上方.
由于这个库目前也不是很成熟, 再加上作者也挺长时间没有更新, 所以这也是一个过渡方案. 最好的方案是抽时间自己造一个.
在这里不打算讲GalleryFinal, 只讲一个多选的例子. 首先GalleryFinal使用之前需要大量的配置, 因此将这些配置代码专门汇总到GalleryFinalConfigurator类, 避免在每一个项目里都要复制粘贴:
GalleryFinalConfigurator.config(context);
调用上述代码之后我们就可以直接使用GalleryFinal了. 现在有一个需求, 用户可以选任意0-9张图片, 并且可以删除已选的图片. 根据需求, 应选用GridView作为展示选择后的图片的载体. 那么作为GridView的item应该提供两个ImageView控件, 一个用于显示选中的图片, 一个用于删除.
基本上多选图片都包含有以下逻辑:
初始情况下, 显示一个"添加照片"的按钮, 除非达到最大图片数, 不然该按钮一直显示在最后一个. 达到最大数则隐藏该按钮.
删除图片需要判断"添加照片"按钮是否是显示的, 不显示的情况下需要显示出来. 显示的情况下按钮也需要跟随照片往前挪移.
这些逻辑可以完全写在一个帮助类中, 以后我们使用的时候只需要设置最大数, 提供布局, 控制照相/相册弹窗的显示即可. 因此ChooseImageHelper诞生了. 首先看看ChooseImageHelper的构造函数与公共方法:
public ChooseImageHelper(int maxSize, GridView display, int itemLayoutResId,
OnOpenChooseDialogCallback callback);
public List<File> getChosenImages();
public void openGallery(int position);
public void openCamera(int position);
public interface OnOpenChooseDialogCallback {
void onOpen(int position);
}
构造函数中, 需要传入一个最大数量, 比如上述需求是"用户可以选任意0-9张图片", 因此maxSize应该是9, 然后传入一个GridView控件用作展示. 接着需要传入一个布局Id, 作为GridView的布局, 需要注意的是此布局中需要包含两个ImageView, 一个id为image, 另一个为delete. 最后传入一个OnOpenChooseDialogCallback实现, ChooseImageHelper会在需要显示相机/相册选择框的时候调用onOpen方法, 并且将点击的position传递出来. 接着一旦我们监控到用户选择了相机或相册, 就可以调用openGallery(position)/openCamera(position)
来使用GalleryFinal. 后续的事情我们就不用控制了. 最后如果需要将选择的图片上传至服务器, 则可以调用getChosenImages()方法获取File集合.
接下来看看具体代码:
ChooseImageHelper helper = new ChooseImageHelper(9, gvImages, R.layout.item_choose_image,
new ChooseImageHelper.OnOpenChooseDialogCallback() {
@Override public void onOpen(int position) {
if (window == null)
window = new ChooseAvatarPopupWindow(getActivity());
window.setOnTypeChosenListener(new ChooseAvatarPopupWindow.OnTypeChosenListener() {
@Override public void onCamera() {
helper.openCamera(position);
}
@Override public void onGallery() {
helper.openGallery(position);
}
});
window.showAtBottom();
}
});
代码很简单, 但是这样就足够完成多选功能了. 在onOpen回调中, window是一个PopupWindow对象, 并且在屏幕底部显示. 当然, 既然提供了回调(OnOpenChooseDialogCallback), 当然也会提供一份相应的Observable.
ChooseImageHelper helper = new ChooseImageHelper(9, gvImages, R.layout.item_choose_image);
helper.toObservable().subscribe(new Action1<Integer>() {
@Override public void call(final Integer position) {
if (window == null)
window = new ChooseAvatarPopupWindow(getActivity());
window.setOnTypeChosenListener(new ChooseAvatarPopupWindow.OnTypeChosenListener() {
@Override public void onCamera() {
helper.openCamera(position);
}
@Override public void onGallery() {
helper.openGallery(position);
}
});
window.showAtBottom(nav);
}
});
由于需要显示"添加照片"的按钮, 而GridView又是由Adapter控制, 如果没有选择照片的情况下, 理论上adapter的getCount应该为0, 但是这样就无法显示按钮. 因此需要额外添加一个实体类, 包含一个标志位 - 是否选择了图片. 如果true, 就显示图片, false就显示按钮. 只要adapter中默认包含一个标志位为false的实例即可.
public class ChosenImageFile implements Parcelable {
public boolean chosen;
public File image;
public ChosenImageFile(boolean chosen, File image) {
this.chosen = chosen;
this.image = image;
}
public static ChosenImageFile emptyInstance() {
return new ChosenImageFile();
}
public ChosenImageFile() {
}
// 省略Parcelable代码
}
接下来是Adapter的代码, Adapter使用了QuickAdapter. Adapter中也持有一个maxSize, 用于计算"添加照片"按钮是否显示. 关键则在onClick事件中有关删除的代码:
public class ChooseImageAdapter extends QuickAdapter<ChosenImageFile>
implements View.OnClickListener {
private int maxSize;
public ChooseImageAdapter(Context context, int layoutResId, int maxSize) {
super(context, layoutResId);
this.maxSize = maxSize;
}
@Override
protected void convert(BaseAdapterHelper helper, ChosenImageFile item) {
helper.setVisible(R.id.image, item.chosen)
.setVisible(R.id.delete, item.chosen)
.setTag(R.id.delete, helper.getPosition())
.setOnClickListener(R.id.delete, this);
if (item.chosen) {
RequestCreator creator = Picasso.with(context).load(item.image)
.config(Bitmap.Config.RGB_565).resize(720, 720).centerInside()
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE);
helper.setImageBuilder(R.id.image, creator);
}
}
public int getChosenCount() {
int count = 0;
for (ChosenImageFile file : data) {
if (file.chosen) count++;
}
return count;
}
@Override
public void onClick(View v) {
int position = (int) v.getTag();
if (getChosenCount() == maxSize)
// 如果当前选择的图片已经达到最大数量了, 则添加一个按钮, 并删除点击的图片
// 如果不是, 则不做处理, 只删除图片, 这样在list的末尾就会一直保留有一个choose为false的实体.
// 其他逻辑则在ChooseImageHelper中实现.
add(ChosenImageFile.emptyInstance());
remove(position);
}
public List<ChosenImageFile> getData() {
return data;
}
}
public class ChooseImageHelper implements AdapterView.OnItemClickListener {
public static final int REQUEST_CAMERA = 1;
public static final int REQUEST_GALLERY = 2;
private int maxSize;
private ChooseImageAdapter adapter;
private OnOpenChooseDialogCallback callback;
private Observable.OnSubscribe<Integer> onSubscribe = new Observable.OnSubscribe<Integer>() {
@Override public void call(final Subscriber<? super Integer> subscriber) {
callback = new OnOpenChooseDialogCallback() {
@Override public void onOpen(int position) {
subscriber.onNext(position);
}
};
}
};
public ChooseImageHelper(int maxSize, GridView display, int itemLayoutResId,
OnOpenChooseDialogCallback callback) {
this(maxSize, display, itemLayoutResId);
this.callback = callback;
}
public ChooseImageHelper(int maxSize, GridView display, int itemLayoutResId) {
this.maxSize = maxSize;
adapter = new ChooseImageAdapter(display.getContext(), itemLayoutResId, maxSize);
display.setAdapter(adapter);
display.setOnItemClickListener(this);
// 默认情况下, 为adapter添加一个choose为false的实例, 当作按钮.
adapter.add(ChosenImageFile.emptyInstance());
}
public List<File> getChosenImages() {
List<ChosenImageFile> files = adapter.getData();
List<File> result = new ArrayList<>();
for (ChosenImageFile file : files) {
if (file.chosen)
result.add(file.image);
}
return result;
}
private FunctionConfig setupFunctionConfig(int maxSize) {
return new FunctionConfig.Builder()
.setEnableCamera(false)
.setEnableEdit(false)
.setEnablePreview(false)
.setEnableRotate(false)
.setEnableCrop(false)
.setMutiSelectMaxSize(maxSize) // 通过maxSize控制可选择图片的数量
.build();
}
private FunctionConfig setupFunctionConfig() {
return setupFunctionConfig(1);
}
public Observable<Integer> toObservable() {
return Observable.create(onSubscribe);
}
public interface OnOpenChooseDialogCallback {
void onOpen(final int position);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
// 如果被点击的位置已经是被选择了, 则不响应
if (adapter.getItem(position).chosen) return;
// 如果不是, 则回调onOpen, 让用户去选择相机/相册
if (callback != null) callback.onOpen(position);
}
// 用户选择相册后调用此方法
public void openGallery(final int position) {
// 通过计算maxSize - position来控制可选图片的数量
GalleryFinal.openGalleryMuti(REQUEST_GALLERY, setupFunctionConfig(maxSize - position),
new GalleryFinal.OnHanlderResultCallback() {
@Override
public void onHanlderSuccess(int requestCode, List<PhotoInfo> resultList) {
int size = resultList.size();
for (int i = 0; i < size; i++) {
PhotoInfo info = resultList.get(i);
if (i == 0)
adapter.remove(position); // 移除末尾的按钮占位实体
// 如果当前显示的图片数量不大于最大数量才继续添加
if (adapter.getCount() <= maxSize)
adapter.add(new ChosenImageFile(true,
new File(info.getPhotoPath())));
// 当遍历到最后一个元素时, 并且当前显示的图片数量不大于最大数量时, 在末尾添加按钮占位
if (i == size - 1 && adapter.getCount() < maxSize)
adapter.add(ChosenImageFile.emptyInstance());
}
}
@Override
public void onHanlderFailure(int requestCode, String errorMsg) {}
});
}
// 用户选择相机后调用此方法
public void openCamera(final int position) {
GalleryFinal.openCamera(REQUEST_CAMERA, setupFunctionConfig(),
new GalleryFinal.OnHanlderResultCallback() {
@Override
public void onHanlderSuccess(int requestCode, List<PhotoInfo> resultList) {
PhotoInfo info = resultList.get(0);
// 移除末尾的按钮占位实体
adapter.remove(position);
// 如果当前显示的图片数量不大于最大数量才继续添加
if (adapter.getCount() <= maxSize)
adapter.add(new ChosenImageFile(true, new File(info.getPhotoPath())));
// 当前显示的图片数量不大于最大数量时, 在末尾添加按钮占位
if (adapter.getCount() < maxSize)
adapter.add(ChosenImageFile.emptyInstance());
}
@Override
public void onHanlderFailure(int requestCode, String errorMsg) {}
});
}
}
以上就是整个ChooseImageHelper的关键代码. 你以为这样就完了? 不是, 这里有个坑, 通俗点就是有个内存泄漏, 请看GalleryFinal类的部分代码:
public class GalleryFinal {
private static OnHanlderResultCallback mCallback;
public static void openGalleryMuti(int requestCode, FunctionConfig config,
OnHanlderResultCallback callback) {
mCallback = callback;
}
}
可以看到, OnHanlderResultCallback内部类被复制给了GalleryFinal类的静态成员变量, 并且没有置空. 这意味着OnHanlderResultCallback会一直存在, 一旦OnHanlderResultCallback内直接或间接的引用了Context, 就会内存泄漏. 在上面的代码中, OnHanlderResultCallback内部引用了adapter, adapter内部又引用了context, 所以造成了内存泄漏.
解决办法很简单, 在适当的时候将mCallback置空. 但是想要加上上述代码, 只能将GalleryFinal代码down下来修改. 因此实现自己的相片选择理应提上日程.