该文章讲述了Android原生态开发过程中设置用户原型头像的实现过程。主要使用到技术有:Android原生态开发、CircleImageView圆形图片视图、Crop裁剪工具等。
1.业务介绍
业务具体流程可以分为一下几个过程:
1.用户点击进行圆形头像设置,可以选择拍照设置和从本地选择图片进行设置两种设计方案。(一下以拍照设置为例进行说明)
2.调用手机相机进行拍照
3.获取拍照照片后调用Crop工具进行照片裁剪。
4.将照片资源添加到CircleImageView视图中。
下面将对整个过程进行详细讲解。
2.详细过程说明
选择照片来源
头像来源可以时本地也可以时拍照,系统可以为用户提供两种选择途径。该功能的实现方式可以采用Dialog实现。具体可见:一个好看的Dialog样式实现,仿IOS。
获取照片资源
上述已经说明,获取照片的途径有两种,如果进行拍照设置,那么系统应该调用手机相机进行拍照,如果选择本地照片,那么系统应该打开手机本地图库。该过程的具体代码如下:
/*** 从本地相册选取图片作为头像* 将为用户打开本地图库*/public void choseHeadImageFromGallery() {Intent intentFromGallery = new Intent();// 设置文件类型intentFromGallery.setType("image/*");intentFromGallery.setAction(Intent.ACTION_GET_CONTENT);activity.startActivityForResult(intentFromGallery, CODE_GALLERY_REQUEST);}/*** 启动手机相机拍摄照片作为头像* 将调用本地相机* 注意:该过程中首先判断了系统是否有存储卡,如果有的情况下将为Intent设置一个Uri对象,该对象可以理解为资源标识符。* Uri资源标识符将标识一个资源的存在,可以通过它获取一个资源信息。* 当为Intent设置 MediaStore.EXTRA_OUTPUT 输出位置时onActivityResult方法的intent.getData()方法将获取的时一个null* 否则获取是Bitmap对象*/public void choseHeadImageFromCameraCapture() {Intent intentFromCapture = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// 判断存储卡是否可用,存储照片文件if (hasSdcard()) {uri = getUriForFile(activity,"head");intentFromCapture.putExtra(MediaStore.EXTRA_OUTPUT,uri);}activity.startActivityForResult(intentFromCapture, CODE_CAMERA_REQUEST);}
上述过程中使用了hasSdcard()方法和getUriForFile(activity,“head”)方法,这两个方法的实现过程如下:
/*** 检查设备是否存在SDCard的工具方法*/public boolean hasSdcard() {String state = Environment.getExternalStorageState();if (state.equals(Environment.MEDIA_MOUNTED)) {// 有存储的SDCardreturn true;} else {return false;}}/** 获取uri资源 **/public static Uri getUriForFile(Context context,String path){// 生成文件makeRootDirectory(basePath + DataTool.sdf_ymd.format(new Date()));// 生成文件File file = new File(basePath +DataTool.sdf_ymd.format(new Date()) , path + ".jpg");PicTool.file = file.getAbsolutePath();return getUriForFile(context,file);};/** 生成文件夹 **/public static boolean makeRootDirectory(String filePath) {File file = null;try {file = new File(filePath);if (!file.exists()) {file.mkdirs();}else{file.delete();file.mkdirs();}return true;} catch (Exception e) {Log.i("error:", e+"");return false;}}/*** 生成URL* @param context* @param file* @return*/public static Uri getUriForFile(Context context, File file) {if (context == null || file == null) {throw new NullPointerException();}Uri uri;if (Build.VERSION.SDK_INT >= 24) {uri = FileProvider.getUriForFile(context.getApplicationContext(), "包名.fileprovider", file);} else {uri = Uri.fromFile(file);}return uri;}
由以上代码可以看出,在生成Uri对象的过程中使用到了FileProvider,这是由于在Android7.0以后使用FileProvider在应用中共享文件资源。在使用FileProvider过程中需要进行配置。配置过程如下:
1.首先在res文件夹下床架xml文件夹,并创建file_path.xml文件。在该文件中做出一下配置:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-pathname="external"path="." /><external-files-pathname="external_files"path="." /><cache-pathname="cache"path="." /><external-cache-pathname="external_cache"path="." /><files-pathname="files"path="." />
</paths>
2.在AndroidManifest.xml文件中进行配置刚才的xml信息。
<providerandroid:name="androidx.core.content.FileProvider"android:authorities="包名.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_path" /></provider>
以上两个操作可以完成FileProvider使用设置,该过程实际上标识了该应用可获取文件资源的范围。
拍照后的操作
拍照后的结果是由onActivityResult来接受处理的,在前面代码中
Intent intentFromCapture = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// 判断存储卡是否可用,存储照片文件if (hasSdcard()) {uri = getUriForFile(activity,"head");intentFromCapture.putExtra(MediaStore.EXTRA_OUTPUT,uri);}activity.startActivityForResult(intentFromCapture, CODE_CAMERA_REQUEST);
我们可以看出给Intent对象传入的uri并不是个局部对象,这是由于当intent对象携带uri时,onActivityResult方法的intent对象并不能再次接受到Uri信息。因此需要将Uri对象作为全局变量存储。当调用onActivityResult方法处理照片资源时其实是处理全局的Uri对象。onAcitivityResult方法的具体操作输入:
public void deal(int requestCode, int resultCode,Intent intent){switch (requestCode) {case CODE_GALLERY_REQUEST: // 从本地获取if (PicTool.hasSdcard()) {Crop.of(intent.getData(), resultUri).asSquare().start(activity);} else {Toast.makeText(activity, "没有SDCard!", Toast.LENGTH_LONG).show();}break;case CODE_CAMERA_REQUEST: // 拍照成功后调用if (PicTool.hasSdcard()) {Uri resultUri = PicTool.getUriForFile(activity,"headPic");Crop.of(uri, resultUri).asSquare().start(activity);} else {Toast.makeText(activity, "没有SDCard!", Toast.LENGTH_LONG).show();}break;case Crop.REQUEST_CROP://使用Crop裁剪之后调用{if(resultCode == Crop.RESULT_ERROR) {Toast.makeText(activity,"裁剪失败",Toast.LENGTH_SHORT).show();}else{//裁剪成功后调用 如果setImageURL设置的url是同一个值的话 则无法改变前端显示Uri result = Crop.getOutput(intent);view.setImageURI(null);view.setImageURI(result);}break;}}}
调用裁剪
在该过程中,调用裁剪使用了Crop工具。使用该工具需要做出一下引入:
//圆形头像implementation 'de.hdodenhof:circleimageview:3.1.0'//裁剪照片compile 'com.soundcloud.android:android-crop:1.0.1@aar'compile 'com.github.bumptech.glide:glide:3.7.0'
Crop的实际使用过程相对比较简单,可以分为两个过程,如下:
1.在AndroidManifest.xml文件中进行配置
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
实际上就是声明了一个acitivity,这是因为在进行裁剪的过程中实际上实在Crop实现的Acitivity上进行的,该Activity已经在crop包中实现了,因此需要在AndroidManifest.xml文件中文件中进行说明。
2.代码中调用
/**
* 在该方法中需要三个参数:uri、resultUri和activity
* uri是需要裁剪的照片资源标识符
* resultUri是存储裁剪后的照片的资源标识符
* activity是调用裁剪的主体
**/
Crop.of(uri, resultUri).asSquare().start(activity);
当裁剪操作接受后,其实还是由onResultActivity进行接受处理,处理过程:
case Crop.REQUEST_CROP://使用Crop裁剪之后调用{if(resultCode == Crop.RESULT_ERROR) {Toast.makeText(activity,"裁剪失败",Toast.LENGTH_SHORT).show();}else{//裁剪成功后调用 如果setImageURL设置的url是同一个值的话 则无法改变前端显示Uri result = Crop.getOutput(intent);view.setImageURI(null);view.setImageURI(result);}break;}
注意:在该过程中,对view进行设置图片资源,首先执行了view.setImageURI(null);方法。这是由于setImageURI方法做了优化处理,它首先判断Uri指向的是否是同一个资源(路径以及文件名是否相同),如果是同一个资源的话,该方便并不会对view再进行Uri设置。
以上整个过程就完成了,下面进行简单梳理一下:
1.调用dialog显示
2.调用相机或者本地图库(如果调用相机的过程中该Intent设置了Uri,则onResultActivity方法的Intent对象的getData方法将接受不到信息)
3.图片资源信息获取成功后通过Crop进行裁剪
4.裁剪后的信息依旧交由onResultActivity方法进行处理
3.具体实现过程
1.jar包的引入:
//圆形头像implementation 'de.hdodenhof:circleimageview:3.1.0'//裁剪照片compile 'com.soundcloud.android:android-crop:1.0.1@aar'compile 'com.github.bumptech.glide:glide:3.7.0'
2.在xml视图文件中配置圆形图片信息
<de.hdodenhof.circleimageview.CircleImageViewandroid:id="@+id/register_user_head_pic"android:layout_width="150dp"android:layout_height="120dp"android:layout_marginTop="50dp"app:civ_border_color="@color/gray"app:civ_border_width="1dp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/toolbar" />
3.配置file_path方法:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-pathname="external"path="." /><external-files-pathname="external_files"path="." /><cache-pathname="cache"path="." /><external-cache-pathname="external_cache"path="." /><files-pathname="files"path="." />
</paths>
AndroidManifest.xml文件
<providerandroid:name="androidx.core.content.FileProvider"android:authorities="com.baiyang.instant_messaging_based_on_android.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_path" /></provider>
provider是和activity标签同级的。此外还需要声明Crop的activity资源:
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
4.工具类,下面给将直接给出整个过程中的工具类:
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;import androidx.core.content.FileProvider;import com.soundcloud.android.crop.Crop;import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;import de.hdodenhof.circleimageview.CircleImageView;/*** 圆形头像工具类*/
public class CircleImageTool {// 圆形视图private CircleImageView view;// 调用主体private Activity activity;private String fileprovider;/*** 构造函数* @param activity -- 进行头像蛇者的activity* @param view -- 圆形视图对象* @param fileprovider -- AndroidManifest.xml文件中provider资源的所有者:android:authorities*/public CircleImageTool(Activity activity,CircleImageView view,String fileprovider){this.activity = activity;this.view = view;this.fileprovider = fileprovider;}/* 请求识别码 */private static final int CODE_GALLERY_REQUEST = 0xa0; // 本地照片private static final int CODE_CAMERA_REQUEST = 0xa1; // 拍照// 图片资源标识符private Uri uri;/*** 从本地相册选取图片作为头像* 将为用户打开本地图库** 当用户点击从本地获取时直接调用该方法*/public void choseHeadImageFromGallery() {Intent intentFromGallery = new Intent();// 设置文件类型intentFromGallery.setType("image/*");intentFromGallery.setAction(Intent.ACTION_GET_CONTENT);activity.startActivityForResult(intentFromGallery, CODE_GALLERY_REQUEST);}/*** 启动手机相机拍摄照片作为头像* 将调用本地相机* 注意:该过程中首先判断了系统是否有存储卡,如果有的情况下将为Intent设置一个Uri对象,该对象可以理解为资源标识符。* Uri资源标识符将标识一个资源的存在,可以通过它获取一个资源信息。* 当为Intent设置 MediaStore.EXTRA_OUTPUT 输出位置时onActivityResult方法的intent.getData()方法将获取的时一个null* 否则获取是Bitmap对象** 当用户点击拍照时直接调用该方法*/public void choseHeadImageFromCameraCapture() {Intent intentFromCapture = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// 判断存储卡是否可用,存储照片文件if (hasSdcard()) {uri = getUriForFile(activity,"head");intentFromCapture.putExtra(MediaStore.EXTRA_OUTPUT,uri);}activity.startActivityForResult(intentFromCapture, CODE_CAMERA_REQUEST);}/*** 接受到返回结果时调用* @param requestCode* @param resultCode* @param intent*/public void deal(int requestCode, int resultCode,Intent intent){switch (requestCode) {case CODE_GALLERY_REQUEST: // 从本地获取if (PicTool.hasSdcard()) {Uri resultUri = getUriForFile(activity,"headPic");// 从本地获取 intent.getData()方法返回选中图片资源Crop.of(intent.getData(), resultUri).asSquare().start(activity);} else {Toast.makeText(activity, "没有SDCard!", Toast.LENGTH_LONG).show();}break;case CODE_CAMERA_REQUEST: // 拍照成功后调用if (hasSdcard()) {Uri resultUri = getUriForFile(activity,"headPic");Crop.of(uri, resultUri).asSquare().start(activity);} else {Toast.makeText(activity, "没有SDCard!", Toast.LENGTH_LONG).show();}break;case Crop.REQUEST_CROP://使用Crop裁剪之后调用{if(resultCode == Crop.RESULT_ERROR) {Toast.makeText(activity,"裁剪失败",Toast.LENGTH_SHORT).show();}else{//裁剪成功后调用 如果setImageURL设置的url是同一个值的话 则无法改变前端显示Uri result = Crop.getOutput(intent);view.setImageURI(null);view.setImageURI(result);}break;}}}/*** 检查设备是否存在SDCard的工具方法*/private boolean hasSdcard() {String state = Environment.getExternalStorageState();if (state.equals(Environment.MEDIA_MOUNTED)) {// 有存储的SDCardreturn true;} else {return false;}}private static final SimpleDateFormat sdf_ymd = new SimpleDateFormat("yyyy-MM-dd", Locale.CANADA);private Uri getUriForFile(Context context,String path){// 文件管理下:/Android/Data/包名/file/日期/String basePath = activity.getExternalFilesDir("").getAbsolutePath()+ File.separator + sdf_ymd.format(new Date());// 生成文件makeRootDirectory(basePath);// 生成文件File file = new File(basePath , path + ".jpg");return getUriForFile(context,file);};// 生成文件夹private boolean makeRootDirectory(String filePath) {File file = null;try {file = new File(filePath);if (file.exists()) {file.delete();}file.mkdirs();return true;} catch (Exception e) {Log.i("error:", e+"");return false;}}/*** 生成URL* @param context* @param file* @return*/private Uri getUriForFile(Context context, File file) {if (context == null || file == null) {throw new NullPointerException();}Uri uri;if (Build.VERSION.SDK_INT >= 24) {uri = FileProvider.getUriForFile(context.getApplicationContext(), fileprovider, file);} else {uri = Uri.fromFile(file);}return uri;}}