After watching Kotlin’s tutorials, I always felt that short sample code was not enough to master Kotlin, and that learning directly from a company project was too risky. As used in the project an imitation WeChat pictures choose library ImagePicker emerged into the image preview interface crash bug (android. OS. TransactionTooLargeException), find the lot found that the author has not been maintaining the library statement, Similar issues have been raised but not resolved in issues. The problem is that the extra data of the intent is too large. In fact, all the picture information in the phone will be transmitted through the intent from the Grid interface to the preview interface. However, if the number of pictures in the phone exceeds 1200, there will be a crash with too much data. Since found the cause of the problem you have the idea, the data quantity is too large, then we will reduce the amount of data, no matter how much is the total picture, every time passed, at most, only 1000 copies will not be too much ~ later found the WeChat also is such processing, while the number of pictures too much, will only take 1108 pictures, solution completely consistent, However, the maximum number of images is definitely a maximum that has been tested. Kind of digress… Since the authors don’t maintain the library, I’ll do it myself, familiarize myself with the code logic by re-implementing it with Kotlin, and do some in-capability optimizations.

ImagePicker, the original author’s library that I implemented with Kotlin for ImagePicker and anyone who would say this isn’t just clone the code, Convert Java File to Kotlin File using the as Kotlin plugin. That is not self-deception, since it is Kotlin’s actual practice, of course, to write their own, in order to practice the effect of proficiency. Another reason is that I don’t translate the original Java code exactly as it is, but I change parts of the code and how it is called.

Here we go, the first interface, ImageGridActivity

Analyze the main logic functions: get the picture in the phone, display the picture in grid layout, switch the folder of the picture, select the picture, enter the preview interface, complete the picture selection.

  • Get pictures from your phone

Through the CursorLoader to achieve, the original author package an ImageDataSource easy to call, key code:

/ / get LoaderManager LoaderManager LoaderManager = activity. GetSupportLoaderManager (); / / registered the third parameter of LoaderManager. LoaderCallbacks < Cursor > LoaderManager. InitLoader (LOADER_CATEGORY, bundle, this); Public Loader<Cursor> onCreateLoader(int id, Public void onLoadFinished(Loader<Cursor> Loader, Cursor data) // Public void onLoaderReset(Loader<Cursor> Loader)// Used when Lodaer is loaded to dataCopy the code

This class is the translation, and the modification is simply to pump the initLoader into a separate method

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); Bundle = new Bundle();} else {Bundle = new Bundle(); bundle.putString("path", path); loaderManager.initLoader(LOADER_CATEGORY, bundle, this); }}Copy the code

Kotlin

class ImageDataSource(private val activity: FragmentActivity) : LoaderManager.LoaderCallbacks<Cursor> { fun loadImage(loadedListener: OnImagesLoadedListener) {loadImage(null, loadedListener)} /** * @param path specifies the directory to scan. @param loadedListener: funloadimage (path: String? , loadedListener: OnImagesLoadedListener) { this.loadedListener = loadedListener val loaderManager = activity.supportLoaderManager val bundle = Bundle() if (path == null) { loaderManager.initLoader(LOADER_ALL, bundle, Bundle. putString("path", path) loaderManager.initLoader(LOADER_CATEGORY, bundle, this) } }Copy the code

New ImageDataSource(this, null, this); MageDataSource (this).loadimage (this) mageDataSource(this).loadimage (this) mageDataSource(this).loadimage (this) Loadermanager. initLoader(LOADER_ALL, null, this) can be passed null, but Kotlin’s null detection mechanism causes only non-null values to be passed. So I can only pass in an empty bundle object.

The Cursor in the onLoadFinished(Loader

Loader, Cursor Data) method is still the first object to be used when the phone rotates the screen and the activity is destroyed and rebuilt. The value inside has been removed, so there is no data, and even crash. The processing means of original author is corresponding activity in the Manifest file to add the android: configChanges = “orientation” | screenSize “attribute, said rotary screen not retrace the life cycle. In fact, the essence of this problem is that the initLoader has a corresponding destroyLoader method. If this method is not executed, the next init loader with the same ID will reuse the previous loader, and the last result object will be given as a new result. = 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;
    }
Copy the code

I did this by adding a destroyLoader method to the ImageDataSource:

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, } else {currentMode = LOADER_CATEGORY} bundle.putString("path", path) loaderManager.initLoader(LOADER_CATEGORY, bundle, this) } } fun destroyLoader() { if (currentMode ! = null) { activity.supportLoaderManager.destroyLoader(currentMode!!) }}Copy the code

And call the loader destruction method in the activity’s onDestroy:

override fun onDestroy() {
        super.onDestroy()
        imageDataSource.destroyLoader()
    }
Copy the code

To ensure that the loader is new each time the activity is entered.

  • Grid display picture

There is nothing to say about this, a multi-type recylerView will do

  • Switching folders

PopupWindow actually takes up the entire screen, not just the visible folder list, but the bottom “All pictures” position has a transparent layout. Clicking triggers the popupWindow to disappear, and the translucent background above is part of the popupWindow layout. Also clicking causes the popupWindow to disappear. PopupWindow = popupWindow; popupWindow = popupWindow; popupWindow = popupWindow; popupWindow = popupWindow; popupWindow = popupWindow; popupWindow = popupWindow;

// Click the folder button createPopupFolderList(); mImageFolderAdapter.refreshData(mImageFolders); / / the refresh data if (mFolderPopupWindow isShowing ()) {mFolderPopupWindow. Dismiss (); } else { mFolderPopupWindow.showAtLocation(mFooterBar, Gravity.NO_GRAVITY, 0, 0); / / the default choice of the currently selected one, when many directory location directly to have the selected entry int index = mImageFolderAdapter. GetSelectIndex (); index = index == 0 ? index : index - 1; mFolderPopupWindow.setSelection(index); } /** * 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()); }Copy the code

And the if (mFolderPopupWindow isShowing ()) {mFolderPopupWindow. Dismiss (); } mFolderPopupWindow is actually another new object, so isShowing() must return false. Later in the optimization process, I may have learned that the original author originally wanted to avoid repeating the creation object, but the popupWindow display is to perform animation, and the parameters required by animation will only be initialized when the interface is drawn. The original author achieved this by the following way:

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(); }});Copy the code
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();
    }
Copy the code

The method is to add a view tree listener to the popupWindow construction, remove the listener when the drawing is complete, and perform an entry animation with data such as view height. This way the listener only fires once, so if you reuse the object the next time it is displayed the animation will have a problem. If you put the animation in the showAtLocation() method, the animation will have a problem because listView.getheight () has not been drawn yet. The adjustment I made, the first time showAtLocation() is called enterSet is null, enterSet? .start() will not be executed, and then the first display will trigger onGlobalLayout, which initializes the animation and executes, and then enterSet in showAtLocation() after the second? .start() is executed to achieve normal animation:

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() }Copy the code

Next up is the second interface, ImagePreviewActivity

This interface is relatively simple viewPager + photoView display pictures, point to note is intent worth problem, I mentioned at the beginning of the intent and the data is too big problem (android. OS. TransactionTooLargeException), When adjusting the amount of data, it should also be noted that the current click position of the picture also needs to be adjusted accordingly.

override fun onImageItemClick(imageItem: ImageItem, position: Int) {var images = adapter.images var p = position if (images.size > INTENT_MAX) { Int if (position < images.size / 2) {// click position at the top of the 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]) //} // equal to the above, Images = (s until e).mapto (ArrayList()) {adapter.images[it]}} ImagePreviewActivity.startForResult(this, REQUEST_PREVIEW, p, images) }Copy the code

Because the selected image needs to be shared among these activities, a static class holds a PickHelper object that holds parameters for selecting the image as well as the selected image. In the PreviewActivity screen, you can also select an image or cancel it, but you don’t click “Finish”, and just return the GridActivity to refresh the selected data:

Override fun onResume () {super. OnResume () / / data refresh adapter notifyDataSetChanged () onCheckChanged(pickerHelper.selectedImages.size, pickerHelper.limit) }Copy the code

Taking pictures

To take a photo, we call the Camera of the system, which is the same as the original author, but with Kotlin, we separately extract the method into an Object class:

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); // The camera has its own default storage path, and photos taken will return a thumbnail. If you want to access the original image, // dat extra can be used to obtain the original image location. If the destination URI is specified, data has no data, and if no URI is specified, data returns data! val uri: Uri if (build.version.sdk_int <= build.version_codes.m) {Uri = uri.fromfile (takeImageFile)} else {// 7.0 Call system camera photography is no longer allowed to use Uri mode, Should be replaced with FileProvider / / and so that we can solve the beautiful MIUI system returns the size of 0 is a uri = FileProvider. GetUriForFile (activity, ProviderUtil.getFileProviderName(activity), / / to join the uri takeImageFile) No photos or samsung mobile phone val resInfoList = activity. The 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} /** * 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) } }Copy the code

The result is then processed in the onActivityResult corresponding to the Activity:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, Data) if (requestCode == REQUEST_CAMERA && resultCode == activity.result_ok) {// Camera returns log.e (" Hubert ", TakeImageFile. AbsolutePath) / / broadcast announcement of new image 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) {// Preview interface returns if (resultCode  == Activity.RESULT_OK) { setResult() } } }Copy the code

After the camera takes a photo and returns it needs to send a broadcast to inform CursorLoader that it has a new image and needs to reload the data.

clipping

Clipping words due to see the original wechat clipping seems to be inconsistent with the original author’s ImagePicker clipping, I do not know whether it is later updated or the original is not the same, I intend to put it first, and then achieve the function of clipping.

call

In fact, this is a big reason to rewrite the library, otherwise very good, is when the system is still called in the native way, first through ImagePicker set parameters:

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);Copy the code

And accept the result in onActivityResult:

@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); If (resultCode == imagepicker.result_code_items) {// Add the image to return 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); }}}Copy the code

This approach is cumbersome and error-prone. The most convenient thing for anyone using a library is that it only takes one line of code. My idea is to do this for the user, who can just call and get the result, like this:

ImagePicker.pick(this, object : ImagePicker.OnImagePickedListener {
                        override fun onImagePickResult(imageItems: ArrayList<ImageItem>) {
                            textView.text = imageItems.toString()
                            ImagePicker.resetConfig()
                        }
                    })
Copy the code

ImagePicker is the entry I defined to initialize the library and call the image selection

object ImagePicker { init { println("imagePicker init ..." ) } var imageLoader: ImageLoader? = null var pickHelper: PickHelper = PickHelper() var listener: ImagePicker.OnPickImageResultListener? = null /** * Init (imageLoader: ImageLoader) {this. ImageLoader = ImageLoader} /** * ImagePicker {pickHelper = pickHelper () return this} /** * 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>) } }Copy the code

The user only needs to configure the parameters and set the listener to receive the results. ShadowActivity here does just that:

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() } }Copy the code

This makes it easy to call and select images, and the library can also review selected images and delete them, so we add an ImagePreviewDelActivity, similar to ImagePreviewActivity. The review method is used to enter the entry for reviewing the selected image. In ShadowActivity, add:

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() } }Copy the code

The corresponding call could look like this:

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) FindViewById (R.id.tv).setonClickListener ({// Select an image, The second entry will automatically bring in the previously selected image (image parameters are not reset) 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) {// Review the selected image, 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) } }Copy the code

Since I just started to use Kotlin to write Android applications, it took me a long time to translate the main functions of this library. At the beginning, the speed of typing code was much slower than that of using Java. Some shortcuts commonly used in Java typing are not implemented in Kotlin, such as generating member variables. Java requires command+ Option +F (MAC)/CTRL + Alt +F (Window), but Kotlin does not. Similarly, I am not familiar with some of Kotlin’s higher-order functions, which can only be used through IDE code prompts. If you find any unreasonable place or better writing method, please do not hesitate to advise, thank you!