照片选择

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下来修改. 因此实现自己的相片选择理应提上日程.

Last updated