该文章讲述了Android原生态开发过程中设置用户原型头像的实现过程。主要使用到技术有:Android原生态开发、CircleImageView圆形图片视图、Crop裁剪工具等。
业务具体流程可以分为一下几个过程:
1.用户点击进行圆形头像设置,可以选择拍照设置和从本地选择图片进行设置两种设计方案。(一下以拍照设置为例进行说明)
2.调用手机相机进行拍照
3.获取拍照照片后调用Crop工具进行照片裁剪。
4.将照片资源添加到CircleImageView视图中。
下面将对整个过程进行详细讲解。
头像来源可以时本地也可以时拍照,系统可以为用户提供两种选择途径。该功能的实现方式可以采用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)) {
// 有存储的SDCard
return 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文件。在该文件中做出一下配置:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
paths>
2.在AndroidManifest.xml文件中进行配置刚才的xml信息。
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="包名.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android: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方法进行处理
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.CircleImageView
android: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" />
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
paths>
AndroidManifest.xml文件
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.baiyang.instant_messaging_based_on_android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android: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)) {
// 有存储的SDCard
return 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;
}
}