5

图片选择、预览、压缩、上传一气呵成

 2 years ago
source link: http://www.androidchina.net/11769.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

图片选择、预览、压缩、上传一气呵成 – Android开发中文站

现在很多APP中都有上传图片的逻辑,比如头像、审核信息之类的图片,那么就会设计到要选择、压缩等操作,这些如果要纯手写,难度也比较大,最重要的是要适配好主流的机型、版本,所以最后我成了个框架拼凑师,但是如何拼凑,拼凑哪家的,这又是一个问题,比如图片选择时是直接调用系统的选择功能还是要自定义UI,虽然现在很少看到有调用系统的来进行选择,但是也有。

下面来一步步的做一个框架拼凑师吧。

如果要调用系统来选择,可以通过以下三种方式。但是在onActivityResult中获取的时候还要进行一系列的计算,主要就是把Uri转换成真实地址。


private void openPickerMethod1() {
    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() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("image/*");
    startActivityForResult(intent, PICK_REQUEST_CODE);
}

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

imagepicker

这类选择方式有一个框架,下面这个是AndroidX版的,他会处理好兼容性的问题,也包括权限问题,可以进行压缩、裁剪,在onActivityResult中可以通过ImagePicker.Companion可以直接拿到真实的地址。

 //https://github.com/Dhaval2404/ImagePicker
 implementation 'com.github.dhaval2404:imagepicker:1.8'

//调起系统进行选择
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);
}

PictureSelector

但是估计没有多少人会喜欢这样的方式,更多是自定义UI,所以在拼凑一个图片选择框架,经过查找PictureSelector这个库的star较高,群众的眼睛是雪亮的,PictureSelector确实非常强大,不过,还需要额外引入一个图片加载库,无疑是Glide了,因为PictureSelector把加载图片的逻辑做成了接口,开放给开发者,我们就可以选择Glide或者是Picasso进行加载。

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

implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'

PictureSelector还集成了PhotoView、luban、ucrop这三个库,用来预览、压缩、裁剪,PictureSelector的做法是直接复制他们的代码到自身的项目中,所以在Android Studio中依赖预览是看不到他们三的存在。

获取选择的图片有两种办法,通过onActivityResult和回调,二者只能选一个。

返回的图片被包含在LocalMedia对象中,获取路径主要有以下三个方法:

localMedia.getRealPath() //获取真是的绝对路径
localMedia.getPath() //获取以Uri表示的路径
localMedia.getCompressPath()//获取压缩后的路径

还可以获取图片其他信息,如高、宽。

在移动端,我们通常都需要进行压缩后在上传,手机拍的照片大部分都在5M以上,如果不进行压缩,上传速度会非常慢,如果同时要上传9张,每张5M,那么一共45M,谁能接受?

压缩后的路径保存在Android/data/包名/files/Pictures/,也可以自定义路径,上传后记得要删除,通过PictureFileUtils.deleteAllCacheDirFile即可。

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);
        }
    }
}

最后一步就是上传,我们采用现在非常流行的Okhtto+Retrofit。

在此之前,我们先写一个接口,用来保存客户端上传的图片,我们这回做的严格一点,判断客户端传入的是不是图片文件,如果全部都是,那么保存,但凡有一个不是,那么就拒绝上传。

那么如何判断是不是有效的图片文件呢?最好的办法就是判断他的前N个字节,jpg格式前3个字节是ffd8ff,png格式前8个字节是89504e470d0a1a0a,我们拿到上传的字节,判断开头是不是这两个即可。


@RestController()
@RequestMapping("file")
public class FileController {
    @PostMapping("upload")
    public String upload(@RequestParam("image") List<MultipartFile> multipartFiles) {
        try {
            if (!checkAllImageFile(multipartFiles)) {
                return "请上传正确的资源";
            }
            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() {
        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();
            }
        }
        return upload.toString();
    }
}

最后在Android通过Retrofit上传。

<?xml version="1.0" encoding="utf-8"?>
<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="上传"></Button>

</RelativeLayout>

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);
        }
    }
}

上面我们说过了,PictureSelector压缩是通过LuBan来压缩的,而他号称是最接近微信朋友圈的图片压缩算法。

https://github.com/Curzibn/Luban
implementation 'top.zibin:Luban:1.1.8'

下面是一个最基本的压缩过程。

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() {
            // TODO 压缩开始前调用,可以在方法内启动 loading UI
          }

          @Override
          public void onSuccess(File file) {
            // TODO 压缩成功后调用,返回压缩后的图片文件
          }

          @Override
          public void onError(Throwable e) {
            // TODO 当压缩过程出现问题时调用
          }
        }).launch();

优雅的上传

上面就是一个基本的选择、上传逻辑,但是有时候会碰到这样的逻辑, A接口有众多参数,其中有两个是用户所上传的图片,但是是图片的URL地址,需要通过B接口上传图片后返回,并且后端接口并不是我们写的,图片只能一个一个上传,不能同时添加多个,那么如何优雅的写?

很多人会开启一个线程,以同步的方式一张张的上传,中途有一张失败,那么就提示用户,否则保存下上传后返回的地址,之后调用A接口传过去。

但是这样毕竟有些慢,就需要我们升升级。

可以借助JDK的一个工具类CountDownLatch,CountDownLatch的作用是使调用者等待,直到条件满足,这个条件就是内部count为0,而这个count是由其他线程去减1。

所以可以这样做,假如上传9张图片,A线程启动9个线程,并创建CountDownLatch(9)进行等待,另外9个线程上传后进行减1操作,如果A线程被唤起,那么在判断所有图片是不是都上传成功了,然后在调用A接口。

我们现在来模拟下:

    @Multipart
    @POST("/file/upload")
    Call<ResponseBody> upload(@Part MultipartBody.Part file);

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() {
            for (int i = 0; i < mLocalMedia.size(); i++) {
                new UploadThread(mLocalMedia.get(i), i).start();
            }
            try {
                /**
                 * 等待上传线程全部完成,等待超时时间是10秒
                 */
                mCountDownLatch.await(10, TimeUnit.SECONDS);
                Log.i(TAG, "run: 上传完成");
                for (int i = 0; i < mUploadResult.length; i++) {
                    Log.i(TAG, "run: " + i + "=" + mUploadResult[i]);
                }
                /**
                 * 只要mUploadResult中有一个是null,则判定是有一个上传失败
                 *
                 * 都不是null则都上传成功,进行下一步
                 */
            } 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() {
            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);
    }

}

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK