Android 头像设置基本解决方案

作者: fengmlo | 来源:发表于2017-05-19 16:38 被阅读210次

    头像设置是Android 开发中比较常见的需求,基本内容有:

    • 通过相机拍取一张图片,或者从图库、文件中选择一张图片
    • 对图片进行裁剪
    • 上传图片,监听上传进度

    通过相机拍取图片

    调用相机拍照需要两个步骤:

    1. 应用内生成一个空文件,使用FileProvider获取文件的Uri,照片最后会保存在这个文件里。一定要确保这个文件所在目录在FileProvider的共享目录下
    • 使用Intent启动相机,并将文件Uri传入(相当于告诉相机将照片保存到这个文件里)

    创建FileProvider
    AndroidManifest.xml中添加provider,注意android:authorities属性:

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.xxx.xxx.fileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"/>
    </provider>
    

    /res/xml/目录下新建file_paths.xml文件,定义FileProvider共享文件的目录:

    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <external-path
            name="download"
            path="Download"/>
        <external-files-path
            name="my_images"
            path="Pictures/"/>
    </paths>
    

    具体使用方式参见API FileProvider

    创建文件,注意文件一定要在FileProvider共享文件的目录中,否则会报错:

    public static File createImageFile(Context context) throws IOException {
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
        String imageFileName = "IMG_" + timeStamp;
        File dir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        return File.createTempFile(imageFileName, ".jpg", dir);
    }
    

    生成UriAUTHORITYAndroidManifest.xmlandroid:authorities属性:

    File imageFile = FileUtil.createImageFile(this);
    Uri imageUri = FileProvider.getUriForFile(getContext(), AUTHORITY, imageFile);
    

    使用Intent启动相机拍照并传入生成的Uri

    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
    }
    

    这样当拍照成功后照片就存储在我们创建的文件中了


    从图库或文件选择图片

    从图库选择文件:

    Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            startActivityForResult(intent, REQUEST_IMAGE_PICK);
    

    从图库或文件选择图片:

    Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
    intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
    startActivityForResult(intent, REQUEST_IMAGE_PICK);
    

    获取结果:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
    
        switch (requestCode) {
            case REQUEST_IMAGE_PICK:
            if (resultCode == RESULT_OK) {
                Uri uri = data.getData();
                if (uri != null) {
                    // 获得了图片
                } else {
                    // TODO 处理异常
                }
            }
            break;
        }
    }
    

    对图片进行裁剪

    由于系统提供了图片裁剪的功能,这样就减小了开发者自己实现图片裁剪的难度,这里使用系统提供的图片裁剪功能。和拍照类似,为了获得裁剪后的图片,我们也需要创建一个空文件,并将其传入Intent

    void startCropImage(Uri uri) {
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(uri, "image/*");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // 使图片处于可裁剪状态
        intent.putExtra("crop", "true");
        // 裁剪框的比例(根据需要显示的图片比例进行设置)
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        // 让裁剪框支持缩放
        intent.putExtra("scale", true);
        // 裁剪后图片的大小(注意和上面的裁剪比例保持一致)
        intent.putExtra("outputX", 300);
        intent.putExtra("outputY", 300);
        // 传递原图路径
        try {
            cropFile = FileUtil.getCacheImageFile(this);
        } catch (IOException e) {
            e.printStackTrace();
            // 处理错误
            showDialog("打开文件失败");
            return;
        }
        cropImageUri = FileProvider.getUriForFile(getContext(), AUTHORITY, cropFile);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, cropImageUri);
        // 设置裁剪区域的形状,默认为矩形,也可设置为原形
        intent.putExtra("circleCrop", false);
        // 设置图片的输出格式
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        // return-data=true传递的为缩略图,小米手机默认传递大图,所以会导致onActivityResult调用失败
        intent.putExtra("return-data", false);
        // 是否需要人脸识别
        intent.putExtra("noFaceDetection", true);
        List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            grantUriPermission(packageName, cropImageUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        }
        if (resInfoList.size() > 0) {
            startActivityForResult(intent, REQUEST_IMAGE_CROP);
        }
    }
    

    好了,现在来梳理下拍照、选图片、裁剪的流程:

    1. 通过Initent启动系统功能并传入处理后图片的保存位置(如果需要)
    • 通过onActivityResult获取调用是否成功,并做进一步处理

    注意:

    1. 因为系统权限的限制,可能需要对目标程序加入对应的读写权限
    • 选择图片返回的Uri直接做为图片裁剪的源文件可能导致出错,这个时候需要将原来的Uri复制到我们自己生成的文件中,再启动图片裁剪

    图片上传并显示进度

    图片上传本质是文件上传,这里使用Retrofit

    添加进度回调接口:

    public interface ProgressRequestListener {
        void onRequestProgress(long bytesSend, long contentLength, boolean done);
    }
    

    编写ProgressRequestBody类实现插入进度回调:

    public class ProgressRequestBody extends RequestBody {
    
        private final RequestBody requestBody;
        private final List<ProgressRequestListener> listeners;
        private BufferedSink bufferedSink;
    
        public ProgressRequestBody(RequestBody requestBody, List<ProgressRequestListener> listeners) {
            this.requestBody = requestBody;
            this.listeners = listeners;
        }
    
        @Override
        public MediaType contentType() {
            return requestBody.contentType();
        }
    
        @Override
        public long contentLength() throws IOException {
            return requestBody.contentLength();
        }
    
        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            //包装
            bufferedSink = Okio.buffer(sink(sink));
            //写入
            requestBody.writeTo(bufferedSink);
            //必须调用flush,否则最后一部分数据可能不会被写入
            bufferedSink.flush();
        }
    
        private Sink sink(Sink sink) {
            return new ForwardingSink(sink) {
                //当前写入字节数
                long bytesWritten = 0L;
                //总字节长度,避免多次调用contentLength()方法
                long contentLength = 0L;
    
                @Override
                public void write(Buffer source, long byteCount) throws IOException {
                    super.write(source, byteCount);
                    if (contentLength == 0) {
                        //获得contentLength的值,后续不再调用
                        contentLength = contentLength();
                    }
                    //增加当前写入的字节数
                    bytesWritten += byteCount;
                    //回调
                    for (ProgressRequestListener listener : listeners) {
                        listener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
                    }
                }
            };
        }
    }
    

    编写服务器接口:

    @Multipart
    @POST("xxx")
    Observable<FileBean> uploadAvatar(@Part MultipartBody.Part part);
    

    服务器接口调用:

    public Observable<FileBean> uploadAvatar(File imageFile) {
        RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), imageFile);
        ProgressRequestBody progressRequestBody = new ProgressRequestBody(requestBody, requestListeners);
        MultipartBody.Part body =
                MultipartBody.Part.createFormData("name", "filename", progressRequestBody);  // 根据需要填写名字
        return serverApi.uploadAvatar(body);
    }
    

    好了,我们只需要把裁剪后的图片上传,并添加ProgressRequestListener,头像设置就搞定了~


    参考文章

    官方培训
    Android 启动系统相机,相册,裁剪图片及6.0权限管理
    你需要知道的Android拍照适配方案
    Android之图片选择与裁剪
    Retrofit 2.0 超能实践(三),轻松实现多文件/图片上传/Json字符串/表单
    OkHttp与Retrofit上传文件详解
    Retrofit Multipart多文件上传

    相关文章

      网友评论

        本文标题:Android 头像设置基本解决方案

        本文链接:https://www.haomeiwen.com/subject/qclyxxtx.html