preface

Now a lot of the APP have to upload pictures of logic, such as head, audit information such as pictures, so will design to choose, compression, such as operation, these if you want to pure hand, difficulty is big, the most important thing is to good adaptation of mainstream models, version, so I finally became a framework to piece together, but how to piece together, which together, this is a problem, For example, when selecting pictures, whether to call the system’s selection function directly or to customize the UI, although it is rarely seen that there is a call system to select, but there are.

Step by step to do a framer.

Pictures to choose

If you want to call the system to select, you can do so in the following three ways. However, retrieving from onActivityResult requires a series of calculations, mainly to translate the Uri into the real address.


private void openPickerMethod1(a) {
    Intent intent = new Intent();
    intent.setType("image/*");
    intent.setAction(Intent.ACTION_GET_CONTENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    startActivityForResult(intent, PICK_REQUEST_CODE);
}

private void openPickerMethod2(a) {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("image/*");
    startActivityForResult(intent, PICK_REQUEST_CODE);
}

private void openPickerMethod3(a) {
    Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    intent.setType("image/*");
    startActivityForResult(intent, PICK_REQUEST_CODE);
}

Copy the code

imagepicker

There is a framework for this type of selection. The following one is on AndroidX, which handles compatibility issues, including permissions, compression, clipping, and in onActivityResult you can get the real address directly from ImagePicker.Companion.

 //https://github.com/Dhaval2404/ImagePicker
 implementation 'com. Making. Dhaval2404: imagepicker: 1.8'
Copy the code
// Set the system to select
ImagePicker.Companion.with(this)
                .crop()
                .start();

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        File file = ImagePicker.Companion.getFile(data);
        Log.i(TAG, "onActivityResult: "+file);
}
Copy the code

PictureSelector

But not too many of us are going to like that. It’s more of a custom UI, so once we’ve pieced together a selection of pictures, we’ve looked up the PictureSelector library, and the PictureSelector library is pretty cool. We also had to introduce a new picture loading library, which was definitely Glide, because PictureSelector had interface the pictures-loading logic to developers, and we could choose Glide or Picasso to load them.

//https://github.com/LuckSiege/PictureSelector
implementation 'com. Making. LuckSiege. PictureSelector: picture_library: v2.6.0'

implementation 'com. Making. Bumptech. Glide: glide: 4.12.0'
annotationProcessor 'com. Making. Bumptech. Glide: the compiler: 4.12.0'
Copy the code

PictureSelector also integrates PhotoView, Luban and UCROP libraries to preview, zoom out and crop out. PictureSelector copies their code directly into their own projects. So they are not visible in Android Studio depending on the preview.

There are two ways to get the selected image: onActivityResult and callback.

The returned image is contained in the LocalMedia object. There are three main ways to obtain the image:

localMedia.getRealPath() // Get the absolute path of truthLocalMedia. GetPath ()// Get the path represented by a Uri
localMedia.getCompressPath()// Get the compressed path
Copy the code

You can also get other information about the picture, such as height and width.

On the mobile terminal, we usually need to compress before uploading. Most photos taken by the mobile phone are over 5M. Without compression, the upload speed will be very slow.

Compressed path in Android/data/package/files/Pictures /, can also custom path, remember to delete after upload, through PictureFileUtils. DeleteAllCacheDirFile can.

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "TAG";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        PictureSelector.create(this)
                .openGallery(PictureMimeType.ofImage())
                .imageEngine(new GlideImageEngine())
                .forResult(PictureConfig.CHOOSE_REQUEST);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case PictureConfig.CHOOSE_REQUEST:
                    List<LocalMedia> selectList = PictureSelector.obtainMultipleResult(data);
                    for (LocalMedia localMedia : selectList) {
                        Log.i(TAG, "onActivityResult: "+localMedia.getRealPath());
                    }
                    break;
                default:
                    break; }}}class GlideImageEngine  implements  ImageEngine{
        @Override
        public void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView, SubsamplingScaleImageView longImageView, OnImageCompleteCallback callback) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView, SubsamplingScaleImageView longImageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadFolderImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadAsGifImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadGridImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) { Glide.with(context).load(url).into(imageView); }}}Copy the code

upload

The last step is uploading, and we use Okhtto+Retrofit, which is very popular now.

Before doing this, we first write an interface, which is used to save the image uploaded by the client, we will do a bit more strict this time, determine whether the client is not the image file, if all are, then save, if any is not, then refuse to upload.

So how do you tell if it’s a valid image file? The best way is to determine the first N bytes, the first 3 bytes in JPG format are FFD8FF, and the first 8 bytes in PNG format are 89504e470D0A1A0A. We get the uploaded bytes and determine whether they start with these two.


@RestController()
@RequestMapping("file")
public class FileController {
    @PostMapping("upload")
    public String upload(@RequestParam("image") List<MultipartFile> multipartFiles) {
        try {
            if(! checkAllImageFile(multipartFiles)) {return "Please upload the correct resource";
            }
            for (MultipartFile multipartFile : multipartFiles) {
                if (multipartFile.getSize() == 0) {
                    continue;
                }
                String fileName = multipartFile.getOriginalFilename();
                String extend = fileName.substring(fileName.lastIndexOf(".")); Path path = Paths.get(getImageStorage(), UUID.randomUUID() + extend); multipartFile.transferTo(path); }}catch (IOException e) {
            e.printStackTrace();
        }
        return "OK";
    }

    private boolean checkAllImageFile(List<MultipartFile> multipartFiles) {
        for (MultipartFile multipartFile : multipartFiles) {
            try {
                if(! isImageFile(multipartFile.getBytes())) {return false; }}catch(IOException e) { e.printStackTrace(); }}return true;
    }

    private boolean isImageFile(byte[] bytes) {
        String[] IMAGE_HEADER = {"ffd8ff"."89504e470d0a1a0a"};
        for (String s : IMAGE_HEADER) {
            if (checkHeaderHex(bytes, s)) {
                return true; }}return false;
    }


    private static boolean checkHeaderHex(byte[] sourceByte, String targetHex) {
        byte[] byteForHexString = getByteForHexString(targetHex);
        if (sourceByte.length < byteForHexString.length) {
            return false;
        }
        for (int i = 0; i < byteForHexString.length; i++) {
            if(sourceByte[i] ! = byteForHexString[i]) {return false; }}return true;
    }

    private static byte[] getByteForHexString(String targetHex) {
        StringBuffer stringBuffer = new StringBuffer(targetHex);
        int index;
        for (index = 2; index < stringBuffer.length(); index += 3) {
            stringBuffer.insert(index, ', ');
        }
        String[] array = stringBuffer.toString().split(",");
        byte[] bytes = new byte[array.length];
        for (int i = 0; i < array.length; i++) {
            bytes[i] = (byte) Integer.parseInt(array[i], 16);
        }
        return bytes;
    }

    private String getImageStorage(a) {
        ApplicationHome applicationHome = new ApplicationHome();
        Path upload = Paths.get(applicationHome.getDir().toString(), "upload");
        if(! Files.exists(upload)) {try {
                Files.createDirectory(upload);
            } catch(IOException e) { e.printStackTrace(); }}returnupload.toString(); }}Copy the code

It was finally uploaded on Android via Retrofit.


      
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:onClick="onUploadImageClick"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Upload"></Button>

</RelativeLayout>
Copy the code
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "TAG";
    private Retrofit mRetrofit;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRetrofit = new Retrofit.Builder().baseUrl("http://192.168.0.106:8080/").build();
    }

    private MultipartBody.Part convertToRequestBody(String path) {
        File file = new File(path);
        RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        MultipartBody.Part body =
                MultipartBody.Part.createFormData("image", file.getName(), requestFile);
        return body;
    }

    private List<MultipartBody.Part> createMultipartBody(List<LocalMedia> selectList) {
        List<MultipartBody.Part> list = new ArrayList<>();
        for (LocalMedia localMedia : selectList) {
            list.add(convertToRequestBody(localMedia.getRealPath()));
        }
        return list;
    }

    private void uploadImages(List<LocalMedia> selectList) {
        mRetrofit.create(Apis.class)
                .upload(createMultipartBody(selectList)).enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                try {
                    Toast.makeText(MainActivity.this, response.body().string(), Toast.LENGTH_SHORT).show();
                } catch(IOException e) { e.printStackTrace(); }}@Override
            public void onFailure(Call<ResponseBody> call, Throwable t) { t.printStackTrace(); }}); PictureFileUtils.deleteAllCacheDirFile(this);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Log.i(TAG, "onActivityResult: ");
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case PictureConfig.CHOOSE_REQUEST:
                    List<LocalMedia> selectList = PictureSelector.obtainMultipleResult(data);
                    for (LocalMedia localMedia : selectList) {
                        Log.i(TAG, "onResult: " + localMedia.getRealPath() + "" + localMedia.getPath() + "" + localMedia.getCompressPath());
                    }
                    uploadImages(selectList);
                    break;
                default:
                    break; }}}public void onUploadImageClick(View view) {
        PictureSelector.create(this)
                .openGallery(PictureMimeType.ofImage())
                .imageEngine(new GlideImageEngine())
                .isCompress(true)
                .forResult(PictureConfig.CHOOSE_REQUEST);
    }

    class GlideImageEngine implements ImageEngine {
        @Override
        public void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView, SubsamplingScaleImageView longImageView, OnImageCompleteCallback callback) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView, SubsamplingScaleImageView longImageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadFolderImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadAsGifImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) {
            Glide.with(context).load(url).into(imageView);
        }

        @Override
        public void loadGridImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView) { Glide.with(context).load(url).into(imageView); }}}Copy the code

The compression

As we’ve said, PictureSelector is compressed through LuBan, which claims to be the closest thing to wechat moments.

https://github.com/Curzibn/Luban
implementation 'top. Zibin: Luban: 1.1.8'
Copy the code

The following is a basic compression process.

Luban.with(this)
        .load(photos)
        .ignoreBy(100)
        .setTargetDir(getPath())
        .filter(new CompressionPredicate() {
          @Override
          public boolean apply(String path) {
            return! (TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
          }
        })
        .setCompressListener(new OnCompressListener() {
          @Override
          public void onStart(a) {
            // TODO is called before compression begins, so that the loading UI can be started within the method
          }

          @Override
          public void onSuccess(File file) {
            // TODO is called after the compression succeeds, and returns the compressed image file
          }

          @Override
          public void onError(Throwable e) {
            TODO is called when there is a problem with the compression process
          }
        }).launch();
Copy the code

Graceful upload

Above is A basic selection, the upload logic, but sometimes will run into so of logic, A interface has many parameters, there are two is users to upload pictures, but is the URL address of the picture, need to return after by B interface to upload pictures, and the backend interface is not we write, pictures can only upload one by one, can not add more at the same time, So how do you write gracefully?

Many people will open A thread and upload one by one in A synchronized way. If one fails in the process, the user will be prompted. Otherwise, save the address returned after uploading and call interface A to pass it.

But this is a bit slow after all, we need to upgrade.

CountDownLatch, a JDK utility class, is used to make the caller wait until the condition is met that the internal count is zero and that count is subtracted by one by another thread.

For example, if 9 images are uploaded, thread A starts 9 threads and creates CountDownLatch(9) to wait. The other 9 threads reduce 1 after uploading. If thread A is aroused, it will determine whether all images have been uploaded successfully and then call interface A.

Let’s simulate it now:

    @Multipart
    @POST("/file/upload")
    Call<ResponseBody> upload(@Part MultipartBody.Part file);
Copy the code
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "TAG";
    private Retrofit mRetrofit;
    private CountDownLatch mCountDownLatch;

    private String[] mUploadResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRetrofit = new Retrofit.Builder().baseUrl("http://192.168.0.106:8080/").build();
    }

    private MultipartBody.Part convertToRequestBody(String path) {
        File file = new File(path);
        RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        MultipartBody.Part body =
                MultipartBody.Part.createFormData("image", file.getName(), requestFile);
        return body;
    }


    private void uploadImages(List<LocalMedia> selectList) {
        mUploadResult = new String[selectList.size()];
        mCountDownLatch = new CountDownLatch(selectList.size());
        new DispatchThread(selectList).start();
    }

    class DispatchThread extends Thread {
        private List<LocalMedia> mLocalMedia;

        public DispatchThread(List<LocalMedia> selectList) {
            this.mLocalMedia = selectList;
        }

        @Override
        public void run(a) {
            for (int i = 0; i < mLocalMedia.size(); i++) {
                new UploadThread(mLocalMedia.get(i), i).start();
            }
            try {
                /** * Wait for the upload thread to complete, the wait timeout is 10 seconds */
                mCountDownLatch.await(10, TimeUnit.SECONDS);
                Log.i(TAG, "Run: Upload completed");
                for (int i = 0; i < mUploadResult.length; i++) {
                    Log.i(TAG, "run: " + i + "=" + mUploadResult[i]);
                }
                /** * If one of the mUploadResult values is null, the upload fails. ** If none of the mUploadResult values are null, the upload succeeds
            } catch(InterruptedException e) { e.printStackTrace(); }}}class UploadThread extends Thread {
        private LocalMedia mLocalMedia;
        private int mIndex;

        public UploadThread(LocalMedia localMedia, int index) {
            mLocalMedia = localMedia;
            mIndex = index;
        }

        @Override
        public void run(a) {
            mRetrofit.create(Apis.class)
                    .upload(convertToRequestBody(mLocalMedia.getRealPath())).enqueue(new Callback<ResponseBody>() {
                @Override
                public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                    if (response.isSuccessful()) {
                        try {
                            mUploadResult[mIndex] = response.body().string();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    mCountDownLatch.countDown();
                }

                @Override
                public void onFailure(Call<ResponseBody> call, Throwable t) { mCountDownLatch.countDown(); }}); }}@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Log.i(TAG, "onActivityResult: ");
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case PictureConfig.CHOOSE_REQUEST:
                    uploadImages(PictureSelector.obtainMultipleResult(data));
                    break;
                default:
                    break; }}}public void onUploadImageClick(View view) {
        PictureSelector.create(this)
                .openGallery(PictureMimeType.ofImage())
                .imageEngine(new GlideImageEngine())
                .isCompress(true) .forResult(PictureConfig.CHOOSE_REQUEST); }}Copy the code