最近,小灵狐得知了一种能够加快修炼速度的绝世秘法,那便是修炼android神功。小灵狐打算用android神功做一个app,今天他的修炼内容就是头像功能。可是小灵狐是个android小白啊,所以修炼过程也是困难重重。下面我们来看看他的修炼过程吧。
控件
小灵狐想要能够拥有头像,那么首先就要有显示头像的地方,也就是控件。首先可以采用ImageView来,但是小灵狐不喜欢用ImageView来,他选择了另一种控件——CircleImageView。这是一种能够将图片圆形化的控件,用法和ImageView是完全一样的,就是会自动把头像转化成圆形的。要使用这个控件,首先要引入第三方库de.hdodenhof:circleimageview:2.2.0
,剩下的就和ImageView一样了。比如他在布局中加入了如下的样式:
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/civ_my_avatar" // 控件id,唯一表示控件
android:layout_width="100dp" // 设置宽为100dp
android:layout_height="100dp" // 设置高为100dp
android:layout_gravity="center" // 设置在布局中的显示位置为中间
/>
之后小灵狐发现这样子如果没有图片的话,就看不见这个控件,他就想要在控件外围显示一个圈,在这样就算没有头像,那么这个圈也会显示出来,让人一看就知道这里是要显示头像的。于是,他就在上面的代码中加入了以下的设置
app:civ_border_width="1dp" // 设置边框宽度,默认为0,这样就看不到那个圈了
app:civ_border_overlay="true" // 设置边框是否覆盖在图片上,默认为false
app:civ_border_color="@color/silver" // 设置边框的颜色,默认为黑色,这里用了一种自定义的颜色——银色
app:civ_fill_color="@color/whiteSmoke"// 设置填充底色,默认为透明,这里用了自定义的颜色——白烟色
加上了上面的设置,便可以显示出一个头像的圈圈了。这样,基本的东西就准备好了,但是还是没有图片。如果想要显示某张图片(假设图片为drawable文件夹下的一张名为“fox”的图片),只要再加上下面的设置就好了。
android:src="@drawable/fox"
如果想要了解更多的关于CircleImageView的内容,可以参考
CircleImageView的项目源码链接的项目源码链接
修改头像
现在已经能够成功显示出头像了,但是小灵狐喜欢用头像来表达自己的心情,所以他经常会换头像,这可怎么办呢?不急,在更换头像之前,我们需要能够选择一张用来替换的图片。小灵狐想要点击头像就可以选择系统相册里面的图片并把它设为头像。我们一步步来。
设置头像框监听事件
我们给头像框设置一个监听事件,使得点击它后会调用系统相册。这就要先给头像框设置监听事件了。给控件设置监听事件只要两步就好啦,首先创建一个对应控件类型的对象,并通过findViewById(int id)
函数找到控件id将对象与空间进行绑定。然后就可以通过对象来调用setOnClickListener()
函数,从而达到监听的效果,代码如下:
// 根据上面指定的控件id创建CircleImageView控件对象
CircleImageView circleImageView = (CircleImageView) findViewById(R.id.civ_my_avatar);
// 调用监听函数
circleImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 相关操作
}
});
调用系统相册
设置好监听事件后,就可以在“相关操作”的地方添加响应事件,这里我们需要的是能够调用系统相册。但是小灵狐发现要调用系统相册,需要先申请SD卡读写权限。申请读写权限主要分为两个步骤:一是在注册文件(AndroidManifest.xml)里声明所需权限,这里需要的是SD卡读写权限,所以应该添加如下声明:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
WRITE_EXTERNAL_STORAGE
权限是同时授予对SD卡读和写的能力。二是在Activity中查看权限,如果没有授予,需要动态申请,代码如下:
if (ContextCompat.checkSelfPermission(InfoSettingActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(InfoSettingActivity.this, new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
} else {
// 调用系统相册
}
对于上面的代码,小灵狐给出了解释:ContexCompat.checkSelfPermission()
函数用来查看权限,该函数的第一个参数是一个上下文context,第二个参数就是刚才我们在注册文件中声明的权限,若该函数返回的值等于PackageManager.PERMISSION_GRANTED
,说明用户已经授权,我们可以直接进行后续操作。如果不是,就需要动态申请权限,ActivityCompat.requestPermissions()
函数用来动态申请权限,并且需要重写onRequestPermissionsResult()
函数,重写代码如下:
@Override
// 申请用户权限
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.
PERMISSION_GRANTED) {
// 用户允许授权
/* ————调用系统相册————*/
} else {
// 用户拒绝授权
Toast.makeText(this, "You denied the permission",
Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
动态申请权限的时候会弹出一个权限申请框,询问是否授权,对于这样的弹框,用过安卓手机的人应该都不陌生吧。这时候如果用户点击允许,那么便成功获得对SD卡的读写权限,可以开开心心的进行后续操作了,如果被用户拒绝,那么就不能成功的执行后续操作了。
做了这一切,还是没有说怎么调用系统相册,不急,我们继续看看小灵狐是怎么做的吧。其实调用系统相册很简单,只有三句代码,我们把它们用函数openAlbum()
进行封装,这样只要把上面出现“调用系统相册”的注释全部改为openAlbum()
,就可以成功的执行调用系统相册的操作了。
private void openAlbum() {
// 使用Intent来跳转
Intent intent = new Intent("android.intent.action.GET_CONTENT");
// setType是设置类型,只要系统中满足该类型的应用都会被调用,这里需要的是图片
intent.setType("image/*");
// 打开满足条件的程序,CHOOSE_PHOTO是一个常量,用于后续进行判断,下面会说
startActivityForResult(intent, CHOOSE_PHOTO);
}
选择图片
我们知道开启一个活动直接用startActivity()
就可以,但上面使用startActivityForResult()
来开启活动而不直接用startActivity()
,这是为什么呢?注意到多了'forResult',顾名思义,不难想象出原因。是因为我们不仅仅是要打开系统相册,更重要的是能够在系统相册中选择一张图片并且返回图片的url,所以要'forResult'。为了这个'result',我们需要重写onActivityResult()
方法:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
// 上面的CHOOSE_PHOTO就是在这里用于判断
case CHOOSE_PHOTO:
// 判断手机系统版本号
if (resultCode == RESULT_OK) {
if (Build.VERSION.SDK_INT >= 19) {
// 手机系统在4.4及以上的才能使用这个方法处理图片
handleImageOnKitKat(data);
} else {
// 手机系统在4.4以下的使用这个方法处理图片
handleImageBeforeKitKat(data);
}
}
break;
default:
break;
}
}
上面的代码还是很好理解的,但是可能会感到奇怪,为什么要判断Build.VERSION.SDK_INT
的值呢?自习观察两个函数的命名,差了一个before,所以大概能猜出点什么了吧。没错,这两个函数都是用来处理图片的,那他们的区别是什么呢?Android系统从4.4版本开始,选取相册中的图片不再返回图片真实的Uri了,而是一个分装过的Uri,因此如果是4.4版本以上的系统要对这个Uri进行解析才行。handleImageOnKitKat()
函数包含了对Uri的解析,4.4版本以后的就要用这个函数,而4.4版本之前的就要用handleImageBeforeKitKat()
函数了,现在明白了before是指什么了吧。因为要区分手机系统版本,所以我们加了对Build.VERSION.SDK_INT
的判断。好了,现在我们明白了要用到这两个函数来处理图片,可是这两个函数到底是什么啊,我们继续往下看。
@TargetApi(19)
private void handleImageOnKitKat(Intent data) {
Uri uri = data.getData();
String imagePath = null;
// 如果是document类型的Uri,,则通过document id处理
if (DocumentsContract.isDocumentUri(this, uri)) {
String docId = DocumentsContract.getDocumentId(uri);
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
// 解析出数字格式的id
String id = docId.split(":")[1];
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
selection);
}else if("com.android.providers.downloads.documents".equals(uri.getAuthority())){
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content:" +
"//downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null);
}
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
// 如果是file类型的Uri,直接获取图片路径
imagePath = uri.getPath();
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// 如果是content类型的Uri,使用普通方式处理
imagePath = getImagePath(uri, null);
}
// 根据得到的图片路径显示图片
}
private void handleImageBeforeKitKat(Intent data) {
Uri uri = data.getData();
String imagePath = getImagePath(uri, null);
// 根据得到的图片路径显示图片
}
从上面的代码我们也可以看出这两个函数的差别。
显示图片
现在我们就差最后一步了,就是把图片显示出来。安卓中常用的显示图片的函数有两个setImageResource()
和setImageBitmap()
,前者的参数只能是 int id
,也就是说它只能显示在drawable文件夹下的图片,但是我们要从相册里面获得,显然要用后者,我们可以把显示图片的过程分装成一个函数:
private void displayImage(String imagePath){
if(imagePath!=null){
Bitmap bitmap= BitmapFactory.decodeFile(imagePath);
circleImageView.setImageBitmap(bitmap);
}else{
Toast.makeText(this,"failed to get image",Toast.LENGTH_SHORT).show();
}
}
做到这里,我们就完成了用户头像的功能啦。小灵狐迫不及待的从相册中随便找了一张表情包试用,成功的把表情包显示在了头像框内。但是,事情真的就这么简单吗?太天真了。
图片压缩
小灵狐实现了用户头像,非常开心。赶紧拿出手机,拍了一张高清图片,准备作为自己的头像。但是,当他选择该图片之后,却惊讶的发现竟然没有显示出来,而且程序还会崩溃,这可把小灵狐急坏了。经过研究学习,他发现,原来是图片太大了,直接加载进去,会导致内存泄漏,从而使得图片无法正常显示,甚至是程序奔溃。所以在显示图片之前,要经过一定的压缩,然后再加载进去。Bitmap压缩都是围绕这个来做文章:Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数。3个参数,任意减少一个的值,就达到了压缩的效果。所以,根据这个,小灵狐修改了displayImage()
函数,采用了如下的压缩方式:
//显示图片
private void displayImage(String imagePath){
if(imagePath!=null){
BitmapFactory.Options options = new BitmapFactory.Options();// 解析位图的附加条件
options.inJustDecodeBounds = true; // 不去解析位图,只获取位图头文件信息
Bitmap bitmap= BitmapFactory.decodeFile(imagePath,options);
circleImageView.setImageBitmap(bitmap);
int btwidth = options.outWidth; // 获取图片的宽度
int btheight = options.outHeight; //获取图片的高度
int dx = btwidth/100; // 获取水平方向的缩放比例
int dy = btheight/100; // 获取垂直方向的缩放比例
int sampleSize = 1; // 设置默认缩放比例
// 如果是水平方向
if (dx>dy&&dy>1) {
sampleSize = dx;
}
//如果是垂直方向
if (dy>dx&&dx>1) {
sampleSize = dy;
}
options.inSampleSize = sampleSize; // 设置图片缩放比例
options.inJustDecodeBounds = false; // 真正解析位图
// 把图片的解析条件options在创建的时候带上
bitmap = BitmapFactory.decodeFile(imagePath, options);
circleImageView.setImageBitmap(bitmap); // 设置图片
}else{
Toast.makeText(this,"failed to get image",Toast.LENGTH_SHORT).show();
}
}
关于图片压缩的方式网上有很多种方式方法,其实内容和原理都是大同小异,如果想要了解更多的方法,这里有几个参考链接可以看一看:
Android图片压缩(质量压缩和尺寸压缩)&Bitmap转成字符串上传
黑科技
小灵狐学习了图片的压缩,也自己写了一种压缩方式,但是他发现到底要把图片压缩到什么程度很难把握,而且不同的屏幕,不同的手机效果也不同。有没有其他的方法来加载图片,能够有效的避免因图片太大而导致内存泄漏的问题呢?经过不断地探索发现,小灵狐发现了一个很好用的加载图片方式——glide。这是一种神奇的黑科技,关于它的使用方法,可以参考以下博客,讲的非常清楚,非常好,我就不再啰嗦啦。
总结
到此,用户头像功能算是全部实现了。小灵狐从零开始,一步步摸索出来,经历了很多的困难,当然也学到了很多。现在,小灵狐终于可以把自己的高清图片作为自己的头像啦。这部分的修炼暂告一段落,敬请期待以后的修炼。