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:

<TextViewandroid: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规范对成员变量的命名是驼峰式,但是我个人认为这里的控件使用:控件名缩写_功能名,的方式命名更好,能够一眼就知道这是一个控件,区别去其他的成员变量。

正文

自看了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<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

class ImageDataSource(private val activity: FragmentActivity) : LoaderManager.LoaderCallbacks<Cursor> {fun loadImage(loadedListener: OnImagesLoadedListener) {loadImage(null, loadedListener)}/*** @param path           指定扫描的文件夹目录,可以为 null,表示扫描所有图片* @param loadedListener 图片加载完成的监听*/fun loadImage(path: String?, loadedListener: OnImagesLoadedListener) {this.loadedListener = loadedListenerval loaderManager = activity.supportLoaderManagerval 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<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? = nullfun loadImage(path: String?, loadedListener: OnImagesLoadedListener) {this.loadedListener = loadedListenerdestroyLoader()val loaderManager = activity.supportLoaderManagerval bundle = Bundle()if (path == null) {currentMode = LOADER_ALLloaderManager.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() {@Overridepublic 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() {@Overridepublic 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 / 8val realHeight = listView.heightval listParams = listView.layoutParamslistParams.height = if (realHeight > maxHeight) maxHeight else realHeightlistView.layoutParams = listParamsval marginParams = marginView.layoutParams as LinearLayout.LayoutParamsmarginParams.height = marginPxmarginView.layoutParams = marginParamsinitEnterSet()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 = 400enterSet!!.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.imagesvar p = positionif (images.size > INTENT_MAX) {//数据量过大val s: Intval e: Intif (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 - sLog.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/")elseEnvironment.getDataDirectory()takeImageFile = createFile(takeImageFile, "IMG_", ".jpg")val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)takePictureIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOPif (takePictureIntent.resolveActivity(activity.packageManager) != null) {// 默认情况下,即不需要指定intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);// 照相机有自己默认的存储路径,拍摄的照片将返回一个缩略图。如果想访问原始图片,// 可以通过dat extra能够得到原始图片位置。即,如果指定了目标uri,data就没有数据,// 如果没有指定uri,则data就返回有数据!val uri: Uriif (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())) + suffixreturn 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接受结果:

 @Overridepublic 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中初始化图片加载框架*/@JvmStaticfun init(imageLoader: ImageLoader) {this.imageLoader = imageLoader}/*** 图片选择参数恢复默认*/@JvmStaticfun defaultConfig(): ImagePicker {pickHelper = PickHelper()return this}/*** 清楚缓存的已选择图片*/@JvmStaticfun clear() {pickHelper.selectedImages.clear()pickHelper.historyImages.clear()}/*** 图片数量限制,默认9张*/@JvmStaticfun limit(max: Int): ImagePicker {pickHelper.limit = maxreturn this}/*** 是否显示相机,默认显示*/@JvmStaticfun showCamera(boolean: Boolean): ImagePicker {pickHelper.isShowCamera = booleanreturn this}/*** 是否多选,默认显示*/@JvmStaticfun multiMode(boolean: Boolean): ImagePicker {pickHelper.isMultiMode = booleanreturn this}/*** 是否裁剪*/@JvmStaticfun isCrop(boolean: Boolean): ImagePicker {pickHelper.isCrop = booleanreturn this}@JvmStaticfun pick(context: Context, listener: OnPickImageResultListener) {this.listener = listenerShadowActivity.start(context, 0, 0)}@JvmStaticfun camera(context: Context, listener: OnPickImageResultListener) {this.listener = listenerShadowActivity.start(context, 2, 0)}@JvmStaticfun review(context: Context, position: Int, listener: OnPickImageResultListener) {this.listener = listenerShadowActivity.start(context, 1, position)}interface OnPickImageResultListener {fun onImageResult(imageItems: ArrayList<ImageItem>)}
}

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

internal class InitializationCheck<T>(private val message: String? = null) : ReadWriteProperty<Any?, T> {private var value: T? = nulloverride 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 2cameraprivate var position: Int = 0companion 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 Intposition = intent.extras[C.EXTRA_POSITION] as IntstartPick()}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.historyImagesif (resultCode == Activity.RESULT_OK && data != null) {val images = data.extras[C.EXTRA_IMAGE_ITEMS] as ArrayList<ImageItem>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 = nullfinish()}
}

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

class ShadowActivity : BaseActivity() {private var type: Int = 0private var position: Int = 0companion 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 Intposition = intent.extras[C.EXTRA_POSITION] as IntstartPick()}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<ImageItem>ImagePicker.listener?.onImageResult(images)}ImagePicker.listener = nullfinish()}
}

相应的调用可以这样:

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<ImageItem>) {(recycler_view.adapter as ImageAdapter).updateData(imageItems)}override fun onDestroy() {super.onDestroy()ImagePicker.clear()//清除缓存已选择的图片}
}

虽然库是用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;@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {ImagePicker.multiMode(isChecked);}});Button btn_pick = (Button) findViewById(R.id.btn_pick);btn_pick.setOnClickListener(new View.OnClickListener() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void onItemClick(int position) {//回顾已选择图片,可以删除ImagePicker.review(MainJavaActivity.this, position, MainJavaActivity.this);}});recyclerView.addItemDecoration(new GridSpacingItemDecoration(3, Utils.dp2px(this, 2f), false));recyclerView.setAdapter(adapter);}@Overridepublic void onImageResult(@NotNull ArrayList<ImageItem> imageItems) {adapter.updateData(imageItems);}@Overrideprotected void onDestroy() {super.onDestroy();ImagePicker.clear();//清除缓存已选择的图片}
}

来看下效果

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 GridLayoutManagerval position = gridLayoutManager.findFirstCompletelyVisibleItemPosition()val addTime = adapter.getItem(position)?.addTimeLog.d("hubert", "图片,position:$position ,addTime: $addTime")if (addTime != null) {val calendar = Calendar.getInstance()calendar.timeInMillis = addTime * 1000if (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.utilimport 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.MONDAYcal1.time = date1cal2.time = date2val 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<ImageItem> = ArrayList()
) : RecyclerView.Adapter<SmallPreviewAdapter.SmallPreviewViewHolder>() {var current: ImageItem? = nullset(value) {field = valuenotifyDataSetChanged()}var listener: OnItemClickListener? = nulloverride fun getItemCount(): Int = images.sizeoverride 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 ImageViewval 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 = previewAdapterupdatePreview()
}private fun updatePreview() {if (pickHelper.selectedImages.size > 0) {rv_small.visibility = View.VISIBLEval index = pickHelper.selectedImages.indexOf(imageItems[current])previewAdapter.current = if (index >= 0) pickHelper.selectedImages[index] else nullif (index >= 0) {rv_small.smoothScrollToPosition(index)}} else {rv_small.visibility = View.GONE}}

展示效果:

写在最后

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

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

  1. Android LruCache和DiskLruCache相结合打造图片加载框架(仿微信图片选择,照片墙)

    LrcCache和DiskLruCache相结合打造图片加载框架 转载请标明出处:http://blog.csdn.net/luoshishou/article/details/51299169 源码 ...

  2. 仿微信图片选择及多张图片上传

    最近做项目涉及到图片上传这块,多张图片上传是用xutils实现的,图片选择.预览是参考了微信图片选择的demo,先上图: 这是网上参考了仿微信图片上传实现的,这里就不多讲了,文章底部有下载链接,下面介 ...

  3. android仿微信图片选择预览裁剪,仿微信图片选择

    Android图片选择器,仿微信的图片选择器的样式和效果. 支持图片单选.多选.裁剪形状自定义.裁剪比例设置.解耦图片加载框架. 项目地址:https://github.com/1izheng/Ima ...

  4. 利用Album 实现仿微信图片选择(加入了图片剪裁)

    本文图片选择基于轻量开源相册 Album,在选择图片完成后会调用系统图片剪裁页面,对图片进行剪裁 效果图: 选择图片(可以自定义选择几张): 剪切图片: 使用方法 1.Gradle: compile ...

  5. 仿微信拍照和图片选择开源库(适配android7.0)multi-image-selector

    资源下载地址,使用时以Module的形式导入项目中使用 链接:http://pan.baidu.com/s/1cbySwe 提取码:yl9u 使用准备 /*** 选择的照片*/private List ...

  6. android仿微信图片选择器

    最近根据项目需求,要做一个仿微信图片选择的功能.首先我们先来整理一下思路. 1.显示选择图片的界面 1.1选择的图片数量小于9 最后一张图片是一个加号. 1.2选择的图片数量等于9,加号消失. 2.选 ...

  7. android仿微信图片上传进度,android高仿微信发布动态(选择图片)

    [实例简介]Android 超高仿微信图片选择器 [实例截图] [核心代码] public class MainActivity extends Activity implements OnImage ...

  8. Android仿微信图片上传,可以选择多张图片,缩放预览,拍照上传

    仿照微信,朋友圈分享图片功能 .可以进行图片的多张选择,拍照添加图片,以及进行图片的预览,预览时可以进行缩放,并且可以删除选中状态的图片 .很不错的源码,大家有需要可以下载看看 . 如果不知道如何上传 ...

  9. 安卓开发仿微信图片拖拽_仿微信朋友圈发表图片拖拽和删除功能

    原标题:仿微信朋友圈发表图片拖拽和删除功能 中国联通在香港公布了上市公司2017年中期业绩.2017年上半年,公司主要业绩指标持续向好,收入稳步回升,服务收入达到人民币1,241.1亿元,同比增长3. ...

最新文章

  1. Swift和Javascript的神奇魔法
  2. CentOS下与Apache连接的PHP多版本共存方案实现详解
  3. 成功解决python\ops\seq2seq.py TypeError: ms_error() got an unexpected keyword argument 'labels'
  4. 结合批处理功能,配置SQL Server 2005,使其打开远程连接功能
  5. leetcode剑指 Offer 42. 连续子数组的最大和(动态规划)
  6. HTML5 API详解(18):IndexedDB 本地存储
  7. solution: stuch on 'setting up your MAC'
  8. unique去除重复的向量_R语言向量与因子
  9. mysql 传输表空间_Oracle传输表空间总结
  10. Linux上zk节点在哪存着,Kafka在Zookeeper上的节点信息和查看方式
  11. linux操作系统学习心得
  12. Docker容器资源管理
  13. java cmd 乱码_java在cmd运行时出现乱码解决方法
  14. linux安装2870无线网卡,『求助』RaLink雷凌RT2870 无线网卡怎样安装驱动?
  15. 解决VMware虚拟机无法上网
  16. 【转载】冗余与热备的概念区别
  17. 探究CSS3中的transition和transform属性方法使用
  18. matlab零序五次谐波,基于MATLAB的高次谐波接地选线保护仿真设计
  19. element整理<el-calendar>日历组件-假期(整理)
  20. 【安卓】Android Studio 3.5的引用包错误

热门文章

  1. Vue的生命周期是什么
  2. 修改微信小程序单选,复选框样式
  3. idr寄存器、_STM32 GPIO寄存器 IDR ODR BSRR BRR
  4. 2021国防科技大学计算机学院无军籍考研经验贴
  5. 一个更加强大的查壳工具, 更新版本
  6. 安卓识别身份证,自动提取身份证信息功能实现(附源码)
  7. Ubuntu16.04 安装NVIDIA英伟达驱动教程
  8. H - Shifting Sort
  9. Java设计模式——依赖倒转原则
  10. win10此电脑默认7个文件夹(附+ OneDrive、Catch!)隐藏方法