Kotlin实战之仿开源图片选择库ImagePicker

Posted 郭霖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin实战之仿开源图片选择库ImagePicker相关的知识,希望对你有一定的参考价值。



今日科技快讯


昨日上午,国内首次通过网络平台进行司法拍卖的三架波音747飞机,最终成交两架。记者了解到,经过多轮竞价,两架波音747最终由顺丰航空公司摘得,这也意味着其豪掷3.2亿元拍下两架波音747扩充其货机编队。据悉,此次上拍的三架波音747货机,均来自中国首家中外合资的航空货运公司——翡翠国际货运航空有限责任公司。2013年2月6日,翡翠航空清算组以资产不足以清偿全部债务,并且明显缺乏清偿能力为由,向深圳中院申请翡翠航空进行破产清算。


作者简介


本篇文章来自 胡奚冰 的投稿。主要讲解了用kotlin编写一个开源库的过程,希望对大家有所帮助!

http://www.jianshu.com/u/002f99a0df6b


概述


自看了 Kotlin 的教程后,总感觉简短的示例代码并不能熟练掌握 Kotlin,而直接从公司项目练手又又太过风险了。

正巧项目中用到的一个仿微信图片选择库 ImagePicker 出现了进图片预览界面 crash 的bug(android.os.TransactionTooLargeException),查找 github 发现作者已经声明不维护这个库了,issues 中也有人提出类似的问题,但并没有解决。于是只能自给自足了,定位到问题是intent的extra数据过大导致了,其实就是从 Grid 界面到预览界面时会把手机中的所有图片信息,通过 intent 传递过去,而如果手机中的图片数量超过1200张,就会出现数据过大的 crash。既然找到了问题的原因就有思路了,数据量过大,那我们就减少数据量,不管总的图片数量是多少,每次最多只传递1000张就不会过大了嘛~后来发现微信也是这样处理的,当图片数量过多时,只会取1108张图片,解决思路完全一致,只是最大的图片张数肯定是经过测试的一个最大值。

好像扯远了。。。其实就是既然作者不维护这个库了,那我就自己来维护,顺便通过用Kotlin 重新实现来熟悉代码逻辑,并做一些能力内的优化工作。

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

https://github.com/jeasonlzy/ImagePicker

https://github.com/huburt-Hu/ImagePicker/tree/master


正文


ImageGridActivity

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

  • 获取手机中的图片

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

//获取LoaderManager 
LoaderManager loaderManager = activity.getSupportLoaderManager(); //注册 第三个参数为LoaderManager.LoaderCallbacks<Cursor>
loaderManager.initLoader(LOADER_CATEGORY, bundle, this); //实现LoaderCallbacks的方法
public Loader<Cursor> onCreateLoader(int id, Bundle args)//创建Loader
public void onLoadFinished(Loader<Cursor> loader, Cursor data) //当Lodaer加载到数据时
public void onLoaderReset(Loader<Cursor> 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

Kotlin实战之仿开源图片选择库ImagePicker

这样做的目的是让调用者明确做了哪些操作,第一种使用时 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<Cursor> loader, Cursor data)方法中的cursor还是第一次的对象,里面的值已经被取掉了,因此没有数据,甚至还会crash。原作者的处理方式是在Manifest文件中对应的 activity 添加 android:configChanges="orientation|screenSize" 属性,表示旋转屏幕不重走生命周期。实际上这个问题的本质是 initLoader 有个对应的destroyLoader方法,没有执行该方法的话,下次 init 相同id的 loader 时,还是会复用之前的loader,直接将上次的结果对象作为新的结果给出,可以看api源码当info != null的情况:

public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> 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<Object>)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<Object>)callback; 
    } 

    if (info.mHaveData && mStarted) { 
        // If the loader has already generated its data, report it now. 
        info.callOnLoadFinished(info.mLoader, info.mData); 
    } 
    return (Loader<D>)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")          //等同于上面,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<ImageItem>) 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<ImageItem>) { 
                            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<ImageItem>)      }   }

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

Kotlin实战之仿开源图片选择库ImagePicker

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

Kotlin实战之仿开源图片选择库ImagePicker

并在 Application 的 onCreate 中初始化:

Kotlin实战之仿开源图片选择库ImagePicker

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

Kotlin实战之仿开源图片选择库ImagePicker

这样就可以很简便的调用选择图片,原库还可以回顾已选择的图片,并且支持删除,于是新增一个ImagePreviewDelActivity,类似于ImagePreviewActivity。

ImagePicker.review方法是用来进入回顾已选择图片的入口,在ShadowActivity中增加。相应的调用可以这样:

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

Kotlin实战之仿开源图片选择库ImagePicker

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

/** * Created by hubert * <p> * Created on 2017/10/31. * <p> * 这个类与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<ImageItem>());        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<ImageItem> imageItems) {        adapter.updateData(imageItems);    }    @Override    protected void onDestroy() {        super.onDestroy();        ImagePicker.clear();//清除缓存已选择的图片    } }

来看下效果

Kotlin实战之仿开源图片选择库ImagePicker

demo界面

Kotlin实战之仿开源图片选择库ImagePicker

ImageGridActivity

Kotlin实战之仿开源图片选择库ImagePicker

ImagePreviewActivity

补充

在用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文件的,另一个就是我们需要的。例如这个textview:

<TextView     
   android:id
="@+id/btn_preview"    android:layout_width="wrap_content"    android:layout_height="match_parent"    android:layout_alignParentRight="true"    android:background="@null"    android:gravity="center"    android:paddingLeft="16dp"    android:paddingRight="16dp"    android:text="预览(3)"    android:textAllCaps="false"    android:textColor="#FFFFFF"    android:textSize="16sp"/>

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

btn_preview.text = ""

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

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

  • 网格视图界面增加滑动显示图片时间

  • 预览界面增加已选图片的缩略图

第一个实现比较简单,在布局新增一个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的相互联动。

初始化

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

显示图片时间

Kotlin实战之仿开源图片选择库ImagePicker

已选图片的缩略图


结尾


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


欢迎长按下图 -> 识别图中二维码

以上是关于Kotlin实战之仿开源图片选择库ImagePicker的主要内容,如果未能解决你的问题,请参考以下文章

Android图片加载库的封装实战之路

Android App实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)

Android自定义View实战之仿百度加载动画,一种优雅的Loading方式

Android自定义View实战之仿百度加载动画,一种优雅的Loading方式

微信小程序实战之仿美丽说

阿里架构师开源《Kotlin入门教程指南》+《高级Kotlin强化实战》