# 照片选择

> GalleryFinal [GitHub](https://github.com/pengjianbo/GalleryFinal) \[[使用方法](http://www.jianshu.com/p/48ddd6756b7a)]

相信做过照片选择的童鞋都会觉得比较痛苦, 由于手机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](/android-architecture-journey/utilities/adapter.md). 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, 所以造成了内存泄漏.

![Memory leak](/files/-LsLm89yyqRWVyosDE3z)

解决办法很简单, 在适当的时候将mCallback置空. 但是想要加上上述代码, 只能将GalleryFinal代码down下来修改. 因此实现自己的相片选择理应提上日程.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryan-8.gitbook.io/android-architecture-journey/utilities/pick_image.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
