自看了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 = 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<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")
// 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)
}
}
然后在对应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 ...")
}
var imageLoader: ImageLoader? = null
var pickHelper: PickHelper = PickHelper()
var listener: ImagePicker.OnPickImageResultListener? = null
/**
* 在Application中初始化图片加载框架
*/
fun init(imageLoader: ImageLoader) {
this.imageLoader = imageLoader
}
/**
* 准备图片选择,初始化参数配置
*/
fun prepare(): ImagePicker {
pickHelper = PickHelper()
return this
}
/**
* 重置图片选择参数
*/
fun resetConfig() {
pickHelper = PickHelper()
}
fun limit(max: Int): ImagePicker {
pickHelper.limit = max
return this
}
fun showCamera(boolean: Boolean): ImagePicker {
pickHelper.isShowCamera = boolean
return this
}
fun multiMode(boolean: Boolean): ImagePicker {
pickHelper.isMultiMode = boolean
return this
}
fun pick(context: Context, listener: OnPickImageResultListener) {
checkImageLoader()
this.listener = listener
ShadowActivity.start(context, 0, 0)
}
fun review(context: Context, position: Int, listener: OnPickImageResultListener) {
checkImageLoader()
this.listener = listener
ShadowActivity.start(context, 1, position)
}
private fun checkImageLoader() {
if (imageLoader == null) {
throw IllegalArgumentException("""imagePicker has not init,please call "ImagePicker.init(xx)" in your Application's onCreate """)
}
}
interface OnPickImageResultListener {
fun onImageResult(imageItems: ArrayList<ImageItem>)
}
}
使用者只需要配置参数,设置监听就能接收到结果。这里的ShadowActivity就是做了上述的一些操作:
class ShadowActivity : BaseActivity() {
companion object {
fun start(context: Context) {
context.startActivity(Intent(context, ShadowActivity::class.java))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startPick()
}
private fun startPick() {
ImageGridActivity.startForResult(this, 1234)
}
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?.onImagePickResult(images)
}
ImagePicker.listener = null
finish()
}
}
这样就可以很简便的调用选择图片,原库还可以回顾已选择的图片,并且支持删除,于是新增一个ImagePreviewDelActivity,类似于ImagePreviewActivity。
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<ImageItem>
ImagePicker.listener?.onImageResult(images)
}
ImagePicker.listener = null
finish()
}
}
相应的调用可以这样:
class MainActivity : AppCompatActivity(), ImagePicker.OnPickImageResultListener {
private lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ImagePicker.prepare().limit(8)//初始化选择图片参数,会一直保留直到下次调用prepare或resetConfig方法
findViewById(R.id.tv).setOnClickListener({
//选择图片,第二次进入会自动带入之前选择的图片(未重置图片参数)
ImagePicker.pick(this@MainActivity, this@MainActivity)
})
recyclerView = findViewById(R.id.recycler_view) as RecyclerView
recyclerView.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)
}
}
recyclerView.addItemDecoration(GridSpacingItemDecoration(3, Utils.dp2px(this, 2f), false))
recyclerView.adapter = imageAdapter
}
override fun onImageResult(imageItems: ArrayList<ImageItem>) {
(recyclerView.adapter as ImageAdapter).updateData(imageItems)
}
}
由于本人是刚开始用Kotlin写Android应用,因此翻译这个库的主要功能也花了不少时间,刚开始敲代码的速度比用java慢了好多,有些用java敲代码常用的快捷键在Kotlin中好像没有实现,比如生成成员变量,java的时候只需要command+option+F(mac)/ctrl+alt+F(Window),而Kotlin时就无法有效生成。同样的对于Kotlin的一些高阶函数也不熟悉,都是通过IDE的代码提示才会用到。如果各位看官发现有不合理的地方或者更优的写法,请不吝指教,谢谢!