Kotlin 实战翻译 —— 仿微信图片选择开源库ImagePicker

2017.10.27补充

在用Kotlin写项目的时候由于不能使用生成成员变量的快捷键,导致我写findViewById浪费了好多时间,后来才发现Kotlin对Android有更好的支持,可以完全不用写findViewById,连变量都不用自己声明,简直比ButterKnift都好用!
首先要在项目的build.gradle添加依赖:

buildscript {
    ext.kotlin_version = '1.1.51'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"//这个~
    }
}

其次在module的build.gradle中添加:

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'//这个~

然后就可以在Activity等中直接使用布局中的id了,会提示你导包,这时候会有两个包让你选,一个R文件的,另一个就是我们需要的:
import kotlinx.android.synthetic.main.你的布局名字.*
例如这个textview:


在activity中可以直接使用id作为成员变量去使用

btn_preview.text = ""

虽然在java规范对成员变量的命名是驼峰式,但是我个人认为这里的控件使用:控件名缩写_功能名,的方式命名更好,能够一眼就知道这是一个控件,区别去其他的成员变量。

正文

自看了Kotlin的教程后,总感觉简短的示例代码并不能熟练掌握Kotlin,而直接从公司项目练手又又太过风险了。
正巧项目中用到的一个仿微信图片选择库ImagePicker出现了进图片预览界面crash的bug(android.os.TransactionTooLargeException),查找github发现作者已经声明不维护这个库了,issues中也有人提出类似的问题,但并没有解决。于是只能自给自足了,定位到问题是intent的extra数据过大导致了,其实就是从Grid界面到预览界面时会把手机中的所有图片信息,通过intent传递过去,而如果手机中的图片数量超过1200张,就会出现数据过大的crash。既然找到了问题的原因就有思路了,数据量过大,那我们就减少数据量,不管总的图片数量是多少,每次最多只传递1000张就不会过大了嘛~后来发现微信也是这样处理的,当图片数量过多时,只会取1108张图片,解决思路完全一致,只是最大的图片张数肯定是经过测试的一个最大值。
好像扯远了。。。其实就是既然作者不维护这个库了,那我就自己来维护,顺便通过用Kotlin重新实现来熟悉代码逻辑,并做一些能力内的优化工作。

ImagePicker原作者的库
我用Kotlin实现的ImagePicker
有没有人会说这不就是把代码clone下来,然后用as的Kotlin插件的”Convert Java File to Kotlin File "转一下不就ok了嘛。那不是自欺欺人嘛,既然是Kotlin的实战练习,当然是重新自己写啦,才能起到练习熟练的效果嘛。还有一点原因是我并不会按原java代码原模原样翻译,而是会对部分代码和调用方式做修改。

开始翻译了,首先是第一个界面ImageGridActivity

分析一下有哪些主要逻辑功能:获取手机中的图片,网格布局显示图片,可以切换图片的文件夹,选择图片,进入预览界面,完成图片选择。

  • 获取手机中的图片

通过CursorLoader来实现的,原作者封装了一个ImageDataSource方便调用,关键代码:

//获取LoaderManager
LoaderManager loaderManager = activity.getSupportLoaderManager();

//注册 第三个参数为LoaderManager.LoaderCallbacks
loaderManager.initLoader(LOADER_CATEGORY, bundle, this);

//实现LoaderCallbacks的方法
public Loader onCreateLoader(int id, Bundle args)//创建Loader
public void onLoadFinished(Loader loader, Cursor data) //当Lodaer加载到数据时
public void onLoaderReset(Loader loader)//重启Loader时调用,一般无用

这个类就是翻译,修改仅仅是将initLoader单独抽到一个方法中

public ImageDataSource(FragmentActivity activity, String path, OnImagesLoadedListener loadedListener) {
        this.activity = activity;
        this.loadedListener = loadedListener;

        LoaderManager loaderManager = activity.getSupportLoaderManager();
        if (path == null) {
            loaderManager.initLoader(LOADER_ALL, null, this);//加载所有的图片
        } else {
            //加载指定目录的图片
            Bundle bundle = new Bundle();
            bundle.putString("path", path);
            loaderManager.initLoader(LOADER_CATEGORY, bundle, this);
        }
    }

Kotlin

class ImageDataSource(private val activity: FragmentActivity) : LoaderManager.LoaderCallbacks {
    
    fun loadImage(loadedListener: OnImagesLoadedListener) {
        loadImage(null, loadedListener)
    }

    /**
     * @param path           指定扫描的文件夹目录,可以为 null,表示扫描所有图片
     * @param loadedListener 图片加载完成的监听
     */
    fun loadImage(path: String?, loadedListener: OnImagesLoadedListener) {
        this.loadedListener = loadedListener

        val loaderManager = activity.supportLoaderManager
        val bundle = Bundle()
        if (path == null) {
            loaderManager.initLoader(LOADER_ALL, bundle, this)//加载所有的图片
        } else {
            //加载指定目录的图片
            bundle.putString("path", path)
            loaderManager.initLoader(LOADER_CATEGORY, bundle, this)
        }
    }

这样做的目的是让调用者明确做了哪些操作,第一种使用时new ImageDataSource(this, null, this); ,第二种使用时mageDataSource(this).loadImage(this),第一种只知道new了一个对象,但是具体做了什么还得点进入看才知道,第二种就能明确知道我是创建了一个对象,并且还加载了图片。
这边还有一个Kotlin的小坑,java中loaderManager.initLoader(LOADER_ALL, null, this)可以传入null,但是Kotlin的NULL值检测机制导致这里只能传非null值,否则会报错,因此我只能传入一个空的bundle对象。

cursorLoader这边也一个小坑,当手机旋转屏幕,activity销毁重建,重走生命周期的时候,onLoadFinished(Loader loader, Cursor data)方法中的cursor还是第一次的对象,里面的值已经被取掉了,因此没有数据,甚至还会crash。原作者的处理方式是在Manifest文件中对应的activity添加android:configChanges="orientation|screenSize"属性,表示旋转屏幕不重走生命周期。实际上这个问题的本质是initLoader有个对应的destroyLoader方法,没有执行该方法的话,下次init相同id的loader时,还是会复用之前的loader,直接将上次的结果对象作为新的结果给出,可以看api源码当info != null的情况:

public  Loader initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) {
        if (mCreatingLoader) {
            throw new IllegalStateException("Called while creating a loader");
        }

        LoaderInfo info = mLoaders.get(id);

        if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);

        if (info == null) {
            // Loader doesn't already exist; create.
            info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks)callback);
            if (DEBUG) Log.v(TAG, "  Created new loader " + info);
        } else {
            if (DEBUG) Log.v(TAG, "  Re-using existing loader " + info);
            info.mCallbacks = (LoaderManager.LoaderCallbacks)callback;
        }

        if (info.mHaveData && mStarted) {
            // If the loader has already generated its data, report it now.
            info.callOnLoadFinished(info.mLoader, info.mData);
        }

        return (Loader)info.mLoader;
    }
 
 

我的做法是在ImageDataSource添加destroyLoader方法:

private var currentMode: Int? = null

fun loadImage(path: String?, loadedListener: OnImagesLoadedListener) {
        this.loadedListener = loadedListener
        destroyLoader()

        val loaderManager = activity.supportLoaderManager
        val bundle = Bundle()
        if (path == null) {
            currentMode = LOADER_ALL
            loaderManager.initLoader(LOADER_ALL, bundle, this)//加载所有的图片
        } else {
            currentMode = LOADER_CATEGORY
            //加载指定目录的图片
            bundle.putString("path", path)
            loaderManager.initLoader(LOADER_CATEGORY, bundle, this)
        }
    }

fun destroyLoader() {
        if (currentMode != null) {
            activity.supportLoaderManager.destroyLoader(currentMode!!)
        }
    }

并且在activity的onDestroy中调用销毁loader的方法:

override fun onDestroy() {
        super.onDestroy()
        imageDataSource.destroyLoader()
    }

以保证每次进入activity时loader都是新的。

  • 网格显示图片

这个没啥好说的,一个多类型(拍摄)recylerview就搞定了

  • 切换文件夹

使用PopupWindow实现,这里原作者有一个比较巧妙的思路,PopupWindow实际上是占据整个屏幕的,,并不只是可见的文件夹列表的,最下方"所有图片"的位置其实有一层透明的布局,点击会触发popupWindow的消失,上方半透明的背景也是popupWindow布局的一部分,同样点击会执行消失动画。
这里的优化项是对象的创建,原作者虽然将popupWindow申明成了成员变量,但是每次显示还是会创建新的对象:

          //点击文件夹按钮
            createPopupFolderList();
            mImageFolderAdapter.refreshData(mImageFolders);  //刷新数据
            if (mFolderPopupWindow.isShowing()) {
                mFolderPopupWindow.dismiss();
            } else {
                mFolderPopupWindow.showAtLocation(mFooterBar, Gravity.NO_GRAVITY, 0, 0);
                //默认选择当前选择的上一个,当目录很多时,直接定位到已选中的条目
                int index = mImageFolderAdapter.getSelectIndex();
                index = index == 0 ? index : index - 1;
                mFolderPopupWindow.setSelection(index);
            }

/**
     * 创建弹出的ListView
     */
    private void createPopupFolderList() {
        mFolderPopupWindow = new FolderPopUpWindow(this, mImageFolderAdapter);
        mFolderPopupWindow.setOnItemClickListener(new FolderPopUpWindow.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView adapterView, View view, int position, long l) {
                mImageFolderAdapter.setSelectIndex(position);
                imagePicker.setCurrentImageFolderPosition(position);
                mFolderPopupWindow.dismiss();
                ImageFolder imageFolder = (ImageFolder) adapterView.getAdapter().getItem(position);
                if (null != imageFolder) {
//                    mImageGridAdapter.refreshData(imageFolder.images);
                    mRecyclerAdapter.refreshData(imageFolder.images);
                    mtvDir.setText(imageFolder.name);
                }
            }
        });
        mFolderPopupWindow.setMargin(mFooterBar.getHeight());
    }

并且if (mFolderPopupWindow.isShowing()) { mFolderPopupWindow.dismiss(); } 这一段是无效的逻辑,代码执行到这里mFolderPopupWindow实际上是另一个新建的对象了,因此isShowing()方法必返回false。
后来在优化的过程中我可能知道了原作者一开始是想避免重复创见对象的,但是该popupWindow的显示是要执行动画,而动画需要的参数只有在界面绘制完成时才会被初始化,原作者通过如下方式实现:

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int maxHeight = view.getHeight() * 5 / 8;
                int realHeight = listView.getHeight();
                ViewGroup.LayoutParams listParams = listView.getLayoutParams();
                listParams.height = realHeight > maxHeight ? maxHeight : realHeight;
                listView.setLayoutParams(listParams);
                LinearLayout.LayoutParams marginParams = (LinearLayout.LayoutParams) marginView.getLayoutParams();
                marginParams.height = marginPx;
                marginView.setLayoutParams(marginParams);
                enterAnimator();
            }
        });
private void enterAnimator() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(masker, "alpha", 0, 1);
        ObjectAnimator translationY = ObjectAnimator.ofFloat(listView, "translationY", listView.getHeight(), 0);
        AnimatorSet set = new AnimatorSet();
        set.setDuration(400);
        set.playTogether(alpha, translationY);
        set.setInterpolator(new AccelerateDecelerateInterpolator());
        set.start();
    }

该方法是在popupWindow的构造中,添加view的视图树监听,当绘制完成移除该监听,同时获取视图高度之类数据,执行入场动画。这种方式监听只会触发一次,因此如果复用对象下次显示的时候动画就会有问题,而如果把动画放到showAtLocation()方法中,由于此时界面还没有绘制listView.getHeight()获取肯定是0,动画显示也会有问题。
我作出的调整,第一次调用showAtLocation()enterSet为null,enterSet?.start()就不会执行,接着首次显示界面会触发onGlobalLayout,初始化动画并且执行,第二次之后showAtLocation()中的enterSet?.start()就会执行,实现正常的动画显示:

init {
        ...
        view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                view.viewTreeObserver.removeOnGlobalLayoutListener(this)
                Log.e("hubert", "view created")
                val maxHeight = view.height * 5 / 8
                val realHeight = listView.height
                val listParams = listView.layoutParams
                listParams.height = if (realHeight > maxHeight) maxHeight else realHeight
                listView.layoutParams = listParams
                val marginParams = marginView.layoutParams as LinearLayout.LayoutParams
                marginParams.height = marginPx
                marginView.layoutParams = marginParams

                initEnterSet()
                enterSet?.start()
            }
        })
    }

private fun initEnterSet() {
        val alpha = ObjectAnimator.ofFloat(masker, "alpha", 0f, 1f)
        val translationY = ObjectAnimator.ofFloat(listView, "translationY", listView.height.toFloat(), 0f)
        enterSet = AnimatorSet()
        enterSet!!.duration = 400
        enterSet!!.playTogether(alpha, translationY)
        enterSet!!.interpolator = AccelerateDecelerateInterpolator()
    }

override fun showAtLocation(parent: View, gravity: Int, x: Int, y: Int) {
        super.showAtLocation(parent, gravity, x, y)
        enterSet?.start()
    }

接下来是第二个界面ImagePreviewActivity

这个界面比较简单就是viewPager+photoView展示图片,需要注意的点是intent传值得问题,也就是我开头提到的intent传值数据过大的问题(android.os.TransactionTooLargeException),在调整数据量的时候也要注意当前点击图片位置也需要做相应的调整。

override fun onImageItemClick(imageItem: ImageItem, position: Int) {
        var images = adapter.images
        var p = position
        if (images.size > INTENT_MAX) {//数据量过大
            val s: Int
            val e: Int
            if (position < images.size / 2) {//点击position在list靠前
                s = Math.max(position - INTENT_MAX / 2, 0)
                e = Math.min(s + INTENT_MAX, images.size)
            } else {
                e = Math.min(position + INTENT_MAX / 2, images.size)
                s = Math.max(e - INTENT_MAX, 0)
            }
            p = position - s
            Log.e("hubert", "start:$s , end:$e , position:$p")
//            images = ArrayList()
//            for (i in s until e) {
//                images.add(adapter.images[i])
//            }
            //等同于上面,IDE提示换成的Kotlin的高阶函数
            images = (s until e).mapTo(ArrayList()) { adapter.images[it] }
        }
        ImagePreviewActivity.startForResult(this, REQUEST_PREVIEW, p, images)
    }

由于对已选择的图片在这几个activity需要共享,采用静态类持有PickHelper对象来保存一些选择图片的参数以及已选择的图片。
在PreviewActivity界面也可以选择图片或者取消,但并没有点击“完成”,只是返回的GridActivity时,也需要把选中等数据刷新:

 override fun onResume() {
        super.onResume()
        //数据刷新
        adapter.notifyDataSetChanged()
        onCheckChanged(pickerHelper.selectedImages.size, pickerHelper.limit)
    }

拍摄照片

拍照的话就是调用系统的Camera,与原作者一致,只是用Kotlin,单独将方法抽到了一个Object类中:

object CameraUtil {
    fun takePicture(activity: Activity, requestCode: Int): File {
        var takeImageFile =
                if (Utils.existSDCard())
                    File(Environment.getExternalStorageDirectory(), "/DCIM/camera/")
                else
                    Environment.getDataDirectory()
        takeImageFile = createFile(takeImageFile, "IMG_", ".jpg")
        val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        takePictureIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
        if (takePictureIntent.resolveActivity(activity.packageManager) != null) {

            // 默认情况下,即不需要指定intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
            // 照相机有自己默认的存储路径,拍摄的照片将返回一个缩略图。如果想访问原始图片,
            // 可以通过dat extra能够得到原始图片位置。即,如果指定了目标uri,data就没有数据,
            // 如果没有指定uri,则data就返回有数据!

            val uri: Uri
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
                uri = Uri.fromFile(takeImageFile)
            } else {
                // 7.0 调用系统相机拍照不再允许使用Uri方式,应该替换为FileProvider
                // 并且这样可以解决MIUI系统上拍照返回size为0的情况
                uri = FileProvider.getUriForFile(activity, ProviderUtil.getFileProviderName(activity), takeImageFile)
                //加入uri权限 要不三星手机不能拍照
                val resInfoList = activity.packageManager.queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY)
                resInfoList
                        .map { it.activityInfo.packageName }
                        .forEach { activity.grantUriPermission(it, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) }
            }
            Log.e("nanchen", ProviderUtil.getFileProviderName(activity))
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
        }
        activity.startActivityForResult(takePictureIntent, requestCode)
        return takeImageFile
    }

    /**
     * 根据系统时间、前缀、后缀产生一个文件
     */
    fun createFile(folder: File, prefix: String, suffix: String): File {
        if (!folder.exists() || !folder.isDirectory) folder.mkdirs()
        val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA)
        val filename = prefix + dateFormat.format(Date(System.currentTimeMillis())) + suffix
        return File(folder, filename)
    }
}

object的作用是等同于java中只包含static方法的工具类,但实际转换成java是一个单例类里面,上面这种方式声明的方法,在java中调用:CameraUtil.INSTANCE.takePicture(),如果想要java中工具类一致的体验,需要在方法前添加@JvmStatic,这样的使用的时候就可以省略INSTANCE,于java中使用static方法的调用相同。

然后在对应Activity的onActivityResult中处理结果:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CAMERA && resultCode == Activity.RESULT_OK) {//相机返回
            Log.e("hubert", takeImageFile.absolutePath)
            //广播通知新增图片
            val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
            mediaScanIntent.data = Uri.fromFile(takeImageFile)
            sendBroadcast(mediaScanIntent)

            val imageItem = ImageItem(takeImageFile.absolutePath)
            pickerHelper.selectedImages.clear()
            pickerHelper.selectedImages.add(imageItem)

            if (pickerHelper.isCrop) {//需要裁剪

            } else {
                setResult()
            }
        } else if (requestCode == REQUEST_PREVIEW) {//预览界面返回
            if (resultCode == Activity.RESULT_OK) {
                setResult()
            }
        }
    }

相机拍摄了照片返回后需要发送一条广播通知CursorLoader有新的图片,需要重新加载数据。

剪裁

剪裁的话由于看到微信原版的剪裁好像跟原作者的ImagePicker的剪裁不一致,不知是后来更新还是原本就不一样,我的想法是实现微信一致的功能,但貌似是一个模块,内容还不少。因此只能退而求其次,按照原作者的方式翻译一下。

调用

其实这也是想重写这个库的一个重要原因,其他方面都非常好,就是在调用的时候还是系统原生的方式,还要先要通过ImagePicker设置参数:

//打开选择,本次允许选择的数量
ImagePicker.getInstance().setSelectLimit(maxImgCount - selImageList.size());
Intent intent = new Intent(WxDemoActivity.this, ImageGridActivity.class);
intent.putExtra(ImageGridActivity.EXTRAS_TAKE_PICKERS, true); // 是否是直接打开相机
startActivityForResult(intent, REQUEST_CODE_SELECT);

并且在onActivityResult接受结果:

 @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == ImagePicker.RESULT_CODE_ITEMS) {
            //添加图片返回
            if (data != null && requestCode == REQUEST_CODE_SELECT) {
                images = (ArrayList) data.getSerializableExtra(ImagePicker.EXTRA_RESULT_ITEMS);
                if (images != null) {
                    selImageList.addAll(images);
                    adapter.setImages(selImageList);
                }
            }
        }

这种方式比较繁琐,而且比较容易出错。对于使用一个库的人来说其实最方便的是只需要一行代码就可以搞定。我的想法是将上述的操作替使用者完成,使用者只需要调用并获取结果就可以了,就像这样:

ImagePicker.pick(this, object : ImagePicker.OnImagePickedListener {
                        override fun onImagePickResult(imageItems: ArrayList) {
                            textView.text = imageItems.toString()
                            ImagePicker.resetConfig()
                        }
                    })

ImagePicker是我定义的入口,用于初始化库以及选择图片的参数设置

object ImagePicker {
    init {
        println("imagePicker init ...")
    }

    internal var imageLoader: ImageLoader by InitializationCheck("imageLoader is not initialized, please call 'ImagePicker.init(XX)' in your application's onCreate")

    internal var pickHelper: PickHelper = PickHelper()

    internal var listener: ImagePicker.OnPickImageResultListener? = null

    /**
     * 在Application中初始化图片加载框架
     */
    @JvmStatic
    fun init(imageLoader: ImageLoader) {
        this.imageLoader = imageLoader
    }

    /**
     * 图片选择参数恢复默认
     */
    @JvmStatic
    fun defaultConfig(): ImagePicker {
        pickHelper = PickHelper()
        return this
    }

    /**
     * 清楚缓存的已选择图片
     */
    @JvmStatic
    fun clear() {
        pickHelper.selectedImages.clear()
        pickHelper.historyImages.clear()
    }

    /**
     * 图片数量限制,默认9张
     */
    @JvmStatic
    fun limit(max: Int): ImagePicker {
        pickHelper.limit = max
        return this
    }

    /**
     * 是否显示相机,默认显示
     */
    @JvmStatic
    fun showCamera(boolean: Boolean): ImagePicker {
        pickHelper.isShowCamera = boolean
        return this
    }

    /**
     * 是否多选,默认显示
     */
    @JvmStatic
    fun multiMode(boolean: Boolean): ImagePicker {
        pickHelper.isMultiMode = boolean
        return this
    }

    /**
     * 是否裁剪
     */
    @JvmStatic
    fun isCrop(boolean: Boolean): ImagePicker {
        pickHelper.isCrop = boolean
        return this
    }

    @JvmStatic
    fun pick(context: Context, listener: OnPickImageResultListener) {
        this.listener = listener
        ShadowActivity.start(context, 0, 0)
    }

    @JvmStatic
    fun camera(context: Context, listener: OnPickImageResultListener) {
        this.listener = listener
        ShadowActivity.start(context, 2, 0)
    }

    @JvmStatic
    fun review(context: Context, position: Int, listener: OnPickImageResultListener) {
        this.listener = listener
        ShadowActivity.start(context, 1, position)
    }

    interface OnPickImageResultListener {
        fun onImageResult(imageItems: ArrayList)
    }
}

不知你注意到这个internal var imageLoader: ImageLoader by InitializationCheck()没有,这是Kotlin的新特性:委托,继承自Kotlin提供的ReadWriteProperty类。

internal class InitializationCheck(private val message: String? = null) : ReadWriteProperty {
    private var value: T? = null
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException(message ?: "not initialized")
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
    }
}

参考了原作者,将图片加载框架抽离出来,使用者可以根据自己的图片加载框架实现ImageLoader接口:

interface ImageLoader : Serializable {

    fun displayImage(activity: Activity, path: String, imageView: ImageView, width: Int, height: Int)

    fun displayImagePreview(activity: Activity, path: String, imageView: ImageView, width: Int, height: Int)

    fun clearMemoryCache()
}

并在Application的onCreate中初始化:

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        ImagePicker.init(GlideImageLoader())
    }
}

接下来使用者只需要配置参数,设置监听就能接收到结果。ImagePicker中的ShadowActivity就是做了上述的一些操作:

class ShadowActivity : BaseActivity() {

    private var type: Int = 0//0pick 1review 2camera
    private var position: Int = 0

    companion object {
        fun start(context: Context, type: Int, position: Int) {
            val intent = Intent(context, ShadowActivity::class.java)
            intent.putExtra(C.EXTRA_TYPE, type)
            intent.putExtra(C.EXTRA_POSITION, position)
            context.startActivity(intent)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        type = intent.extras[C.EXTRA_TYPE] as Int
        position = intent.extras[C.EXTRA_POSITION] as Int
        startPick()
    }

    private fun startPick() {
        when (type) {
            0 -> ImageGridActivity.startForResult(this, 101, false)
            1 -> ImagePreviewDelActivity.startForResult(this, 102, position)
            2 -> ImageGridActivity.startForResult(this, 101, true)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        val historyImages = ImagePicker.pickHelper.historyImages
        if (resultCode == Activity.RESULT_OK && data != null) {
            val images = data.extras[C.EXTRA_IMAGE_ITEMS] as ArrayList
            historyImages.let {
                it.clear()
                it.addAll(images)
            }
            ImagePicker.listener?.onImageResult(images)
        } else if (resultCode == Activity.RESULT_CANCELED) {
            ImagePicker.pickHelper.selectedImages.let {
                it.clear()
                it.addAll(historyImages)
            }
            ImagePicker.listener?.onImageResult(historyImages)
        }
        ImagePicker.listener = null
        finish()
    }
}

这样就可以很简便的调用选择图片,原库还可以回顾已选择的图片,并且支持删除,于是新增一个ImagePreviewDelActivity,类似于ImagePreviewActivity。
ImagePicker.review方法是用来进入回顾已选择图片的入口,在ShadowActivity中增加:

class ShadowActivity : BaseActivity() {

    private var type: Int = 0
    private var position: Int = 0


    companion object {
        fun start(context: Context, type: Int, position: Int) {
            val intent = Intent(context, ShadowActivity::class.java)
            intent.putExtra(C.EXTRA_TYPE, type)
            intent.putExtra(C.EXTRA_POSITION, position)
            context.startActivity(intent)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        type = intent.extras[C.EXTRA_TYPE] as Int
        position = intent.extras[C.EXTRA_POSITION] as Int
        startPick()
    }

    private fun startPick() {
        if (type == 1) {
            ImagePreviewDelActivity.startForResult(this, 102, position)
        } else {
            ImageGridActivity.startForResult(this, 101)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK && data != null) {
            val images = data.extras[C.EXTRA_IMAGE_ITEMS] as ArrayList
            ImagePicker.listener?.onImageResult(images)
        }
        ImagePicker.listener = null
        finish()
    }
}

相应的调用可以这样:

recycler_view.layoutManager = GridLayoutManager(this, 3)
        val imageAdapter = ImageAdapter(ArrayList())
        imageAdapter.listener = object : ImageAdapter.OnItemClickListener {
            override fun onItemClick(position: Int) {
                //回顾已选择图片,可以删除
                ImagePicker.review(this@MainActivity, position, this@MainActivity)
            }
        }
        recycler_view.addItemDecoration(GridSpacingItemDecoration(3, Utils.dp2px(this, 2f), false))
        recycler_view.adapter = imageAdapter

ImagePicker在第二次调用prepare或调用clear方法之前会缓存已选择的图片,因此回顾已有图片只需要传入当前图片的位置。
完整的demo Activity

class MainActivity : AppCompatActivity(), ImagePicker.OnPickImageResultListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ImagePicker.prepare().limit(8)
        //默认不裁剪
        cb_crop.setOnCheckedChangeListener({ _, isChecked -> ImagePicker.isCrop(isChecked) })
        cb_multi.isChecked = true//默认是多选
        cb_multi.setOnCheckedChangeListener { _, isChecked -> ImagePicker.multiMode(isChecked) }
        btn_pick.setOnClickListener {
            //选择图片,第二次进入会自动带入之前选择的图片(未重置图片参数)
            ImagePicker.pick(this@MainActivity, this@MainActivity)
        }
        btn_camera.setOnClickListener {
            //直接打开相机
            ImagePicker.camera(this@MainActivity, this@MainActivity)
        }
        recycler_view.layoutManager = GridLayoutManager(this, 3)
        val imageAdapter = ImageAdapter(ArrayList())
        imageAdapter.listener = object : ImageAdapter.OnItemClickListener {
            override fun onItemClick(position: Int) {
                //回顾已选择图片,可以删除
                ImagePicker.review(this@MainActivity, position, this@MainActivity)
            }
        }
        recycler_view.addItemDecoration(GridSpacingItemDecoration(3, Utils.dp2px(this, 2f), false))
        recycler_view.adapter = imageAdapter
    }

    override fun onImageResult(imageItems: ArrayList) {
        (recycler_view.adapter as ImageAdapter).updateData(imageItems)
    }

    override fun onDestroy() {
        super.onDestroy()
        ImagePicker.clear()//清除缓存已选择的图片
    }
}

虽然库是用Kotlin编写的,在java中也可以完全一致地无缝使用,附上java的使用:

/**
 * Created by hubert
 * 

* Created on 2017/10/31. *

* 这个类与MainActivity的java实现,内容完全相同 */ public class MainJavaActivity extends AppCompatActivity implements ImagePicker.OnPickImageResultListener { private ImageAdapter adapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ImagePicker.prepare().limit(8); //默认不裁剪 CheckBox cb_crop = (CheckBox) findViewById(R.id.cb_crop); cb_crop.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { ImagePicker.isCrop(isChecked); } }); CheckBox cb_multi = (CheckBox) findViewById(R.id.cb_multi); cb_multi.setChecked(true);//默认是多选 cb_multi.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { ImagePicker.multiMode(isChecked); } }); Button btn_pick = (Button) findViewById(R.id.btn_pick); btn_pick.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //选择图片,第二次进入会自动带入之前选择的图片(未重置图片参数) ImagePicker.pick(MainJavaActivity.this, MainJavaActivity.this); } }); Button btn_camera = (Button) findViewById(R.id.btn_camera); btn_camera.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //直接打开相机 ImagePicker.camera(MainJavaActivity.this, MainJavaActivity.this); } }); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new GridLayoutManager(this, 3)); adapter = new ImageAdapter(new ArrayList()); adapter.setListener(new ImageAdapter.OnItemClickListener() { @Override public void onItemClick(int position) { //回顾已选择图片,可以删除 ImagePicker.review(MainJavaActivity.this, position, MainJavaActivity.this); } }); recyclerView.addItemDecoration(new GridSpacingItemDecoration(3, Utils.dp2px(this, 2f), false)); recyclerView.setAdapter(adapter); } @Override public void onImageResult(@NotNull ArrayList imageItems) { adapter.updateData(imageItems); } @Override protected void onDestroy() { super.onDestroy(); ImagePicker.clear();//清除缓存已选择的图片 } }

来看下效果

Kotlin 实战翻译 —— 仿微信图片选择开源库ImagePicker_第1张图片
demo界面
Kotlin 实战翻译 —— 仿微信图片选择开源库ImagePicker_第2张图片
ImageGridActivity
Kotlin 实战翻译 —— 仿微信图片选择开源库ImagePicker_第3张图片
ImagePreviewActivity

2017.11.2 更新

为了向微信原版靠拢,新增了如下功能:

  1. 网格视图界面增加滑动显示图片时间
  2. 预览界面增加已选图片的缩略图

第一个实现比较简单,在布局新增一个textview,并且设置recylerView的滑动监听,动态改变textview的显示:

recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (tv_date.visibility == View.VISIBLE) {
                        tv_date.animation = AnimationUtils.loadAnimation(this@ImageGridActivity, R.anim.fade_out)
                        tv_date.visibility = View.GONE
                    }
                } else {
                    if (tv_date.visibility == View.GONE) {
                        tv_date.animation = AnimationUtils.loadAnimation(this@ImageGridActivity, R.anim.fade_in)
                        tv_date.visibility = View.VISIBLE
                    }
                    val gridLayoutManager = recycler.layoutManager as GridLayoutManager
                    val position = gridLayoutManager.findFirstCompletelyVisibleItemPosition()
                    val addTime = adapter.getItem(position)?.addTime
                    Log.d("hubert", "图片,position:$position ,addTime: $addTime")
                    if (addTime != null) {
                        val calendar = Calendar.getInstance()
                        calendar.timeInMillis = addTime * 1000
                        if (isSameDate(calendar.time, Calendar.getInstance().time)) {
                            tv_date.text = "本周"
                        } else {
                            val format = SimpleDateFormat("yyyy/MM", Locale.getDefault())
                            tv_date.text = format.format(calendar.time)
                        }
                    }
                }
            }
        })

其中的isSameDate()是我定义的顶层函数,这也是Kotlin实战推荐的工具类的写法:

@file:JvmName("DateUtil")

package com.huburt.library.util

import java.util.*

/**
 * Created by hubert
 *
 * Created on 2017/11/2.
 */
fun isSameDate(date1: Date, date2: Date): Boolean {
    val cal1 = Calendar.getInstance()
    val cal2 = Calendar.getInstance()
    cal1.firstDayOfWeek = Calendar.MONDAY//将周一设为一周的第一天,默认周日为一周的第一天
    cal2.firstDayOfWeek = Calendar.MONDAY
    cal1.time = date1
    cal2.time = date2
    val subYear = cal1.get(Calendar.YEAR) - cal2.get(Calendar.YEAR)
    if (subYear == 0) {
        if (cal1.get(Calendar.WEEK_OF_YEAR) == cal2.get(Calendar.WEEK_OF_YEAR))
            return true
    } else if (subYear == 1 && cal2.get(Calendar.MONTH) == 11) {
        if (cal1.get(Calendar.WEEK_OF_YEAR) == cal2.get(Calendar.WEEK_OF_YEAR))
            return true
    } else if (subYear == -1 && cal1.get(Calendar.MONTH) == 11) {
        if (cal1.get(Calendar.WEEK_OF_YEAR) == cal2.get(Calendar.WEEK_OF_YEAR))
            return true
    }
    return false
}

不需要定义类,直接在kt文件中定义方法,实际编辑过后生成了对应的static方法,类名默认是.kt文件名+Kt,可以通过@file:JvmName("DateUtil")改变生成java的类名。

第二个界面新增的缩略图也比较简单,就是新增一个横向的recylerView,关键在于与viewPager,CheckBox的相互联动。
Adapter:

class SmallPreviewAdapter(
        private val mActivity: Activity,
        var images: List = ArrayList()
) : RecyclerView.Adapter() {

    var current: ImageItem? = null
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    var listener: OnItemClickListener? = null

    override fun getItemCount(): Int = images.size

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): SmallPreviewViewHolder {
        return SmallPreviewViewHolder(LayoutInflater.from(parent?.context).inflate(R.layout.item_small_preview, parent, false))
    }

    override fun onBindViewHolder(holder: SmallPreviewViewHolder?, position: Int) {
        holder?.bind(position)
    }

    override fun getItemId(position: Int): Long = position.toLong()

    inner class SmallPreviewViewHolder(private var mView: View) : RecyclerView.ViewHolder(mView) {

        val iv_small = mView.findViewById(R.id.iv_small) as ImageView
        val v_frame = mView.findViewById(R.id.v_frame)

        fun bind(position: Int) {
            mView.setOnClickListener {
                listener?.onItemClick(position, images[position])
            }
            if (TextUtils.equals(current?.path, images[position].path)) {
                v_frame.visibility = View.VISIBLE
            } else {
                v_frame.visibility = View.GONE
            }
            ImagePicker.imageLoader.displayImage(mActivity, images[position].path!!, iv_small, iv_small.width, iv_small.height)

        }
    }

    interface OnItemClickListener {
        fun onItemClick(position: Int, imageItem: ImageItem)
    }
}

初始化:

private fun init() {
  ...
 rv_small.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        previewAdapter.listener = object : SmallPreviewAdapter.OnItemClickListener {
            override fun onItemClick(position: Int, imageItem: ImageItem) {
                viewpager.setCurrentItem(imageItems.indexOf(imageItem), false)
            }
        }
        rv_small.adapter = previewAdapter
        updatePreview()
}

private fun updatePreview() {
        if (pickHelper.selectedImages.size > 0) {
            rv_small.visibility = View.VISIBLE
            val index = pickHelper.selectedImages.indexOf(imageItems[current])
            previewAdapter.current = if (index >= 0) pickHelper.selectedImages[index] else null
            if (index >= 0) {
                rv_small.smoothScrollToPosition(index)
            }
        } else {
            rv_small.visibility = View.GONE
        }
    }

展示效果:

Kotlin 实战翻译 —— 仿微信图片选择开源库ImagePicker_第4张图片
显示图片时间
Kotlin 实战翻译 —— 仿微信图片选择开源库ImagePicker_第5张图片
已选图片的缩略图

写在最后

由于本人是刚开始用Kotlin写Android应用,因此翻译这个库的主要功能也花了不少时间,刚开始敲代码的速度比用java慢了好多,有些用java敲代码常用的快捷键在Kotlin中好像没有实现,比如生成成员变量,java的时候只需要command+option+F(mac)/ctrl+alt+F(Window),而Kotlin时就无法有效生成。同样的对于Kotlin的一些高阶函数也不熟悉,都是通过IDE的代码提示才会用到。如果各位看官发现有不合理的地方或者更优的写法,请不吝指教,谢谢!

你可能感兴趣的:(Kotlin 实战翻译 —— 仿微信图片选择开源库ImagePicker)