将拍照和相册选图封装成Demo,图片压缩用的是Android 开源压缩框架——Luban。先上效果动图:
效果图
一言以蔽之,拍照和相册选图这个开发中常见的功能,版本之间的重要区别是:
6.0以下的版本:只要在AndroidManifest中注明相机的使用权限,SDCard的读写2权限,就能在正常的写功能了
6.0:6.0以后Google不再允许开发者直接或许应用的权限,需要在用户知情的情况下授予权限,这便是运行时权限。相机,读,写3个权限除了要在AndroidManifest先声明,在写功能代码的时候还要做动态申请的操作。
7.0+: 众所周知Android四大组件之一内容提供器的作用,便是在不同的应用程序之间实现数据共享, Android 从 N 版本开始禁止通过 file://Uri 的方式在不同 App 之间共享文件,如此以来7.0版本以下直接获取图片文件路径的方法行不通了,所以我们写的应用,就需要通过内容提供器的方式,来获取系统文件管理器中目标文件的路径。
主要源码如下:
MainActivity:
package p.com.lubandemo.activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import java.io.File;
import p.com.lubandemo.R;
import p.com.lubandemo.rx.RxBus;
import p.com.lubandemo.rx.RxBusBaseMessage;
import p.com.lubandemo.rx.RxCode;
import p.com.lubandemo.util.STContext;
import p.com.lubandemo.view.FaceBlackPopWindow;
import rx.Subscription;
import rx.functions.Action1;
import top.zibin.luban.Luban;
import top.zibin.luban.OnCompressListener;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Handler uiHandler;
private Context context;
private FaceBlackPopWindow popupWindow;
private Button btn_popwindow;
MainActivity activity;
//动态写权限所用
public static boolean writeAccepted = false;
public static final int TAKE_PICTURE = 0;
public static final int PHOTO_REQUEST_GALLERY = 1; // 从相册中选择
public static Uri phototUri = null;
//7.0+ 拍照源路径所用
String loadUrl = "";
//所选相册图片的路径
String albumPath = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
uiHandler = new Handler();
activity = this;
btn_popwindow = findViewById(R.id.btn_popwindow);
context = MainActivity.this;
btn_popwindow.setOnClickListener(this);
Subscription subscription = RxBus.getDefault()
.toObservable(RxBusBaseMessage.class)
.subscribe(new Action1() {
@Override
public void call(RxBusBaseMessage message) {
if (message.getCode() == RxCode.FACE_BLACK_DISMISS) {
uiHandler.post(new Runnable() {
@Override
public void run() {
/**
更新UI
*/
dismissPop();
}
});
}
}
});
}
//6.0+运行时权限---写权限请求回掉函数(相机用)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case 1:
writeAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
break;
}
}
private void dismissPop()
{
if(popupWindow!=null &&popupWindow.isShowing())
{
popupWindow.dismiss();
}
}
private void showFaceBlackPop()
{
popupWindow = new FaceBlackPopWindow(activity,activity.getWindow());
//弹出修改/次增加人脸黑名单弹出窗
//控件,为了popWindow一个基准``````````````
Button lin_face_warn = btn_popwindow;
popupWindow.showWindow(lin_face_warn);
}
// 压缩后图片文件存储位置
private String getPath() {
String path = STContext.getInst().imageFilePath;
File file = new File(path);
if (file.mkdirs()) {
return path;
}
return path;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) { // 如果返回码是可以用的
switch (requestCode) {
case TAKE_PICTURE:
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
//6.0以及以下的处理 luban 图片压缩的开源库
Luban.with(context)
.load(phototUri.getPath()) // 传人要压缩的图片列表
.ignoreBy(100) // 忽略不压缩图片的大小
.setTargetDir(getPath()) // 设置压缩后文件存储位置
.setCompressListener(new OnCompressListener() { //设置回调
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
//通知PopWindow绘制图片
RxBus.getDefault().post(RxCode.FACE_BLACK_IMAGE_CHANGE, file.getPath());
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch(); //启动压缩
} else {
//7.0+的处理
String[] arr = phototUri.getPath().split("/");
loadUrl = "";
for (int i = 2; i < arr.length; i++) {
if (i == arr.length - 1) {
loadUrl += arr[i];
} else {
loadUrl += arr[i] + "/";
}
}
Luban.with(context)
.load(Environment.getExternalStorageDirectory() + "/" + loadUrl) // 传人要压缩的图片列表 (生成照片存储的绝对路径)
.ignoreBy(100) // 忽略不压缩图片的大小
.setTargetDir(getPath()) // 设置压缩后文件存储位置
.setCompressListener(new OnCompressListener() { //设置回调
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
//通知PopWindow绘制图片
RxBus.getDefault().post(RxCode.FACE_BLACK_IMAGE_CHANGE, file.getPath());
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch(); //启动压缩
}
break;
case PHOTO_REQUEST_GALLERY:
//获取所选相册的图片路径
Uri albumUri = data.getData();
Uri selectedImage = data.getData();
String[] filePathColumns = {MediaStore.Images.Media.DATA};
Cursor c = getContentResolver().query(selectedImage, filePathColumns, null, null, null);
c.moveToFirst();
int columnIndex = c.getColumnIndex(filePathColumns[0]);
albumPath = c.getString(columnIndex);
c.close();
//6.0以及以下的处理
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
//6.0以及以下的处理
Luban.with(context)
.load(albumPath) // 传人要压缩的图片列表 (相册所选照片的绝对路径)
.ignoreBy(100) // 忽略不压缩图片的大小
.setTargetDir(getPath()) // 设置压缩后文件存储位置
.setCompressListener(new OnCompressListener() { //设置回调
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
//通知PopWindow绘制图片
RxBus.getDefault().post(RxCode.FACE_BLACK_IMAGE_CHANGE, file.getPath());
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch(); //启动压缩
} else {
//7.0+的处理
Luban.with(context)
.load(albumPath) // 传人要压缩的图片列表
.ignoreBy(100) // 忽略不压缩图片的大小
.setTargetDir(getPath()) // 设置压缩后文件存储位置
.setCompressListener(new OnCompressListener() { //设置回调
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
//通知PopWindow绘制图片
RxBus.getDefault().post(RxCode.FACE_BLACK_IMAGE_CHANGE, file.getPath());
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch(); //启动压缩
}
break;
}
}
}
@Override
public void onClick(View view) {
switch (view.getId())
{
case R.id.btn_popwindow:
showFaceBlackPop();
break;
}
}
}
其中运行时权限的回调:
//6.0+运行时权限---写权限请求回掉函数(相机用)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case 1:
writeAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
break;
}
}
FaceBlackPopWindow源码:
package p.com.lubandemo.view;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AlertDialog;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import p.com.lubandemo.R;
import p.com.lubandemo.activity.MainActivity;
import p.com.lubandemo.rx.RxBus;
import p.com.lubandemo.rx.RxBusBaseMessage;
import p.com.lubandemo.rx.RxCode;
import rx.Subscription;
import rx.functions.Action1;
import static p.com.lubandemo.activity.MainActivity.PHOTO_REQUEST_GALLERY;
/**
* Created by ThinkPad on 2017/12/7.
*/
//人脸黑名单添加和修改 的popWindow
public class FaceBlackPopWindow extends PopupWindow implements View.OnClickListener{
private View contentView;
private Activity context;
private ImageView img_face;
private View anchorView;
//传过来的bean(修改的话传值,创建的话传null)
private Button btn_cancel;
private Button btn_sure;
private LinearLayout lin_update_icon;
private Handler uiHandler;
//头像修改完存储的路径
private String iconUrl = "";
public FaceBlackPopWindow(Activity _context, final Window window) {
this.context = _context;
uiHandler = new Handler();
//发起读写设备存储空间的权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
context.requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
contentView = LayoutInflater.from(context).inflate(R.layout.black_face_popwindow, null);
this.setContentView(contentView);
this.setTouchable(true);
this.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
img_face = contentView.findViewById(R.id.img_face);
btn_cancel = contentView.findViewById(R.id.btn_cancel);
btn_sure = contentView.findViewById(R.id.btn_sure);
lin_update_icon = contentView.findViewById(R.id.lin_update_icon);
this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
this.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
//背景阴影
WindowManager.LayoutParams lp = window.getAttributes();
lp.alpha = 0.5f;
window.setAttributes(lp);
// 在dismiss中恢复透明度
this.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss() {
WindowManager.LayoutParams lp = window.getAttributes();
lp.alpha = 1f;
window.setAttributes(lp);
}
});
this.setBackgroundDrawable(new BitmapDrawable());
this.setFocusable(false);// 如此设置,popwindow消失后不会弹出软键盘
//点击外部消失
this.setOutsideTouchable(true);
//设置可以点击
this.setTouchable(true);
//进入退出的动画
this.setAnimationStyle(R.style.mypopwindow_anim_style);
this.setBackgroundDrawable(new BitmapDrawable());
//软键盘不会挡着popupwindow
this.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
//设置PopuoWindow弹出窗体需要软键盘
this.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
btn_cancel.setOnClickListener(this);
btn_sure.setOnClickListener(this);
lin_update_icon.setOnClickListener(this);
Subscription subscription = RxBus.getDefault()
.toObservable(RxBusBaseMessage.class)
.subscribe(new Action1() {
@Override
public void call(final RxBusBaseMessage message) {
if (message.getCode() == RxCode.FACE_BLACK_IMAGE_CHANGE) {
uiHandler.post(new Runnable() {
@Override
public void run() {
/**
更新UI
*/
img_face.setBackgroundResource(0);
iconUrl = message.getObject().toString();
/** Glide 渲染 **/
//设置图片圆角角度
RoundedCorners roundedCorners = new RoundedCorners(30);
//通过RequestOptions扩展功能
RequestOptions options = RequestOptions.bitmapTransform(roundedCorners).override(300, 300)
//圆形
.circleCrop();
Glide.with(img_face.getContext())
.load(iconUrl).apply(options).into(img_face);
}
});
}
}
} );
}
public void showWindow(View view) {
if (!this.isShowing()) {
if (Build.VERSION.SDK_INT < 24) {
this.showAsDropDown(view);
} else {
// 获取控件的位置,安卓系统>7.0
int[] location = new int[2];
view.getLocationOnScreen(location);
this.showAtLocation(view, Gravity.CENTER, 0, 0);
}
}
}
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
return false;
}
/**
* 显示修改头像的对话框
*/
protected void showChoosePicDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("上传图片");
String[] items = { "拍照","相册" };
builder.setNegativeButton("取消", null);
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case MainActivity.TAKE_PICTURE: // 拍照
Intent openCameraIntent = new Intent(
MediaStore.ACTION_IMAGE_CAPTURE);
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M)
{
try {
//创建一个路径用于存生成的照片
MainActivity.phototUri = Uri.fromFile(createMediaFile(".png"));
} catch (IOException e) {
e.printStackTrace();
}
//系统拍照后,存到phototUri
openCameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, MainActivity.phototUri);
context.startActivityForResult(openCameraIntent, MainActivity.TAKE_PICTURE);
}else
{
//适配大于6.0的版本
File cameraPhoto = null;
try {
//创建一个路径用于存生成的照片
cameraPhoto = new File(createMediaFile(".png"),"");
} catch (IOException e) {
e.printStackTrace();
}
//生成照片的路径通过FileProvider创建一个content类型的Uri
MainActivity.phototUri = FileProvider.getUriForFile(
context,
context.getPackageName() + ".fileprovider",
cameraPhoto);
openCameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, MainActivity.phototUri);
context.startActivityForResult(openCameraIntent, MainActivity.TAKE_PICTURE);
}
break;
case PHOTO_REQUEST_GALLERY:
Album();
break;
}
}
});
builder.create().show();
}
/**
* 调起相册
*/
private void Album(){
Intent albumIntent = new Intent(Intent.ACTION_PICK);
albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
context.startActivityForResult(albumIntent,PHOTO_REQUEST_GALLERY);
}
private File createMediaFile(String suffixName) throws IOException {
if (MainActivity.writeAccepted||Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
//Android系统默认路径 Environment.DIRECTORY_MOVIES:电影保存的位置
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES), "CameraDemo");
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
return null;
}
}
// Create an image file name 时间戳处理命名
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "VID_" + timeStamp;
String suffix = suffixName;
File mediaFile = new File(mediaStorageDir + File.separator + imageFileName + suffix);
return mediaFile;
}
}
return null;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_cancel:
this.dismiss();
break;
case R.id.lin_update_icon:
//弹出拍照,照片弹窗
showChoosePicDialog();
break;
}
}
}
由上可知,针对7.0以上版本路径的变更,上面代码获取路径使用了FileProvider,FileProvider其实就是内容提供器 的一个子类,用于应用之间共享文件,使用FlieProvider需要2个先前操作:
1.AndroidManifest中注册
2. 定义xml文件,文件名随便起,名字和provider匹配
简单的说,xml文件的任务就是对具体文件路径的映射,指定了可以访问的文件存储的区域和路径。上面的写法的意思是,代码可以从外部存储根路径Environment.getExternalStorageDirectory()开始,使用当前路径文件夹和其子路径文件夹,如果我换了一种写法:
那么代码只能用Environment.getExternalStorageDirectory()/downloads开始的文件夹路径了,上层路径都做了限制不能映射。
然后代码中相机即时拍出照片的具体路径,通过FileProvider创建一个content类型的Uri,以供对外其他应用(我们写的应用)进行使用:
//生成照片的路径通过FileProvider创建一个content类型的Uri
//适配大于6.0的版本
File cameraPhoto = null;
try {
//创建一个路径用于存生成的照片
cameraPhoto = new File(createMediaFile(".png"),"");
} catch (IOException e) {
e.printStackTrace();
}
//生成照片的路径通过FileProvider创建一个content类型的Uri
MainActivity.phototUri = FileProvider.getUriForFile(
context,
context.getPackageName() + ".fileprovider",
cameraPhoto);
为了方便在手机上查找,相机照出来的照片文件夹我设置成了movie的系统路径下,关于压缩框架luban会另起一篇进行记录总结,Demo已上传:https://download.csdn.net/download/crystal_xing/10891256