接入图片剪切库
图片剪切库我们使用的是Github上知名的uCrop
Github地址
具体的接入细节,和其他三方库一致。就不做过多介绍了,主要看一下一些需要注意的点。
个人的账户信息,我们是在AccountActivity中进行处理,更新用户信息我们采用Fragment,对应名称为UpdateInfoFragment,目前处理是在AccountActivity加载UpdateInfoFragment。而我们之前封装的GalleryView,也是通过Fragment来加载的,对应的为GalleryFragment。其中GalleryFragment继承自BottomSheetDialogFragment,当然如果说要实现类似微信的那种全屏的界面,也可以直接设置为全屏展示。
这里说一下这个GalleryFragment:
BottomDialogFragement是DialogFragment的子类,使用也是要覆写onCreateDialog、onCreateView方法等。
我们需要实现一个GalleryView的接口,也就是我们自定的
GalleryView.SelectedChangeListener
当选中的数量发生改变时,回调此方法,然后进行当前界面的逻辑处理。RecycleView的item点击逻辑,我们放在了GalleryView内部实现了,只是数量改变时,才反馈给上层。因为选择头像的时候,我们只需要一张图片,聊天发送图片的时候,我们可以至多选择九张,根据这个接口,我们就能分别进行相应的处理。这样也能体现出这个View的可复用性。
还有一点非常重要,通过接口回调的方式,我们实现了选择图片个数发生改变时,通知GalleryView进行相关业务逻辑处理。但是GalleryView拿到图片后,还需要返回给上层!
我们从两方面考虑,一方面选择头像,选择完头像,我们需要返回数据给UpdateInfoFragment,进行图片的裁剪等工作;另一方面,当我们聊天时发送图片的时候,我们还需要将1-9张图片渲染到当前的聊天界面中。
所以GalleryFragment也需要一个接口:
/**
* 选中图片的监听器
*/
public interface OnSelectedListener{
void onSelectedImage(String path);
}
调用时机,就是GalleryView反馈给GalleryFragment所用到的回调方法中:
@Override
public void onSelectedCountChanged(int count) {
//如果选中的一张图片
if (count>0){
//隐藏自己
dismiss();
}
if (mListener!=null){
//得到所有的选中的图片的路径
String[] paths=galleryView.getSelectedPath();
//返回第一张
mListener.onSelectedImage(paths[0]);
//取消和唤起者之间的应用,加快内存回收
mListener=null;
}
}
这样,我们在选择头像的入口处,也就是UpdateInfoFragment中实现这个接口:
@Override
public void onSelectedImage(String path) {
UCrop.Options options=new UCrop.Options();
//压缩格式设置为PNG
options.setCompressionFormat(Bitmap.CompressFormat.PNG);
options.setCompressionQuality(96);
//获取头像缓存地址
// TODO: 2019/12/28 把头像的地址生成从Application中抽离处理。放到FileUtils中去
File dPath= zApplication.getPortraitTmpFile();
//设置属性
UCrop.of(Uri.fromFile(new File(path)),Uri.fromFile(dPath))
//图像原地址,裁剪后图片保存地址
.withAspectRatio(1,1)//设置宽高比例
.withMaxResultSize(520,520)//设置裁剪后最大宽高
.withOptions(options)
.start(getActivity());
}
这个地方,我们把头像存储的地址生成放在了Application中,这样并不好。后续我们再抽离出来,先加个todo。
再点击头像选择的入口处展现我们的GalleryFragment即可。
void onPortraitClick(){
new GalleryFragment().setListener(this)
//show的时候建议使用getChildFragmentManager
.show(getChildFragmentManager(),GalleryFragment.class.getName());
}
当剪切成功后,UCrop会将回调返回给当前Actvity的onActivityResult里,我们在当前的Activity也就是AccountActivity里调用Fragment的onActivityResult,将处理完后的图片交由Fragment处理即可。:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mCurFragment.onActivityResult(requestCode,resultCode,data);
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
//收到从Activity传递过来的回调,然后取出其中的值进行图片加载
//如果是我能处理的类型
if (resultCode == RESULT_OK && requestCode == UCrop.REQUEST_CROP) {
final Uri resultUri = UCrop.getOutput(data);
if (resultUri!=null){
loadPortrait(resultUri);
}
} else if (resultCode == UCrop.RESULT_ERROR) {
final Throwable cropError = UCrop.getError(data);
}
}
/**
* 加载Uri到当前的View中
* @param uri
*/
private void loadPortrait(Uri uri){
RequestOptions options=new RequestOptions();
options.centerCrop();
Glide.with(getActivity())
.load(uri)
.apply(options)
.into(portraitView);
}
图片上传功能
这一块逻辑,主要是将以后我们用到的图片存储到阿里云OSS
平台上。
导入几个必须的库:
api "net.qiujuer.genius:kit-handler:$rootProject.ext.geniusVersion"
api "com.aliyun.dpa:oss-android-sdk:$rootProject.ext.ossVersion"
因为是在不同的module下导入的,可以全文搜索在哪个模块导入的,这些导入本身也没有什么难点,我们主要讲实现的逻辑。哦,对了,图片上传功能我们放到一个单独的module中,以后进行路优化改造也方便一些。
特别强调:这里有个史无前例的巨坑,如果不升级到androidX,就无法新建Module!!!虽然说项目最后肯定要迁徙到androidX,但没想到现在不迁徙连module都建不了!只能提前迁徙了,又得改一堆文件了。
迁徙到AndroidX后,我们来做一下权限适配。毕竟6.0之后,需要权限申请,这都10.0了。
权限申请我们使用的是Google官网推荐的EasyPermission
,Github上搜一下就可以看到官方文档。之前我用的是AndPermission框架,现在也暂时屏蔽,看一下EasyPermission在我们项目的实际使用。
先说一下权限申请的逻辑:
在我们的LaunchActivity的时候进行权限的申请,我们所需的权限通过Fragment来展示出来,申请成功的权限后面会给出一个√号的图标,没有授权的则不显示。点击底部授权请求图标后,如果权限都授权成功,就刷新当前的Fragment。否则会弹出谷歌内置的一个dialog,可以 跳到设置界面去授权。
关键代码:
1.
在PermissionFragment中我们只有公共方法,就是判断当前权限是否全部授权,这里的hasNetPer和hasReadPer等四个方法里内部逻辑一致,都是判断各项权限是否已经授权,如果没有,就展示我们当前申请的这个PermissionFragment,如果已经都授权了,就不展示这个Fragment了。我们抽一个hasNetPer方法看一下:
private static boolean hasNetPer(Context context){
String[] perms=new String[]{
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.ACCESS_WIFI_STATE
};
return EasyPermissions.hasPermissions(context,perms);
}
可以看出,就是通过EasyPermissions.hasPermissions,判断当前上下文有没有获得相关的权限。
/**
* 权限申请时候回调的方法,在这个方法中把对应的权限申请状态交给EasyPermission
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//传递对应参数,并且告知接受权限的处理者是自己
EasyPermissions.onRequestPermissionsResult(requestCode,permissions,grantResults,this);
}
复写一下当前Fragment的onRequestPermissionsResult方法,就是将权限处理的结果交给EasyPermission处理。
2.实现EasyPermission的接口:
EasyPermissions.PermissionCallbacks
就是处理权限全部申请成功后或者失败后的处理,就是通过上面那个方法,将处理结果交给这个Callback处理:
@Override
public void onPermissionsGranted(int requestCode, @NonNull List<String> perms) {
}
@Override
public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
/**
* 官网推荐---谷歌的,可以复写
*/
if (EasyPermissions.somePermissionPermanentlyDenied(this,perms)){
new AppSettingsDialog.Builder(this)
.build()
.show();
}
}
如果处理失败,就弹出EasyPermission封装的一个错误Dialog,这里面有按钮可以点到设置界面。
看一下申请权限按钮的逻辑处理;
root.findViewById(R.id.btn_submit).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//授权
requestPerm();
}
});
@AfterPermissionGranted(RC)//权限申请完会进入到当前界面
private void requestPerm(){
String[] perms=new String[]{
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO
};
if (EasyPermissions.hasPermissions(getContext(),perms)){//如果有权限
zApplication.showToast(R.string.label_permission_ok);
//getView必须再根布局之后
refreshState(getView());
}else{
EasyPermissions.requestPermissions(this,getString(R.string.title_assist_permissions),RC,perms);
}
}
可以看出权限申请调用EasyPermissions.requestPermissions方法即可。然后再LaunchActivity调用一下haveAll方法即可,没什么难的,就不说了。
上传OSS关键操作:
阿里云OSS存储文档
先看文档,毕竟老师的课程距离现在两年多了,很多东西都过时了。
主要就是加一下混淆,导入一下依赖,依赖上面也导入了。
OSS-Android集成使用文档
一、一切以官方文档为主。引入依赖后,我们就来初始化一下OSS:
private static final String TAG=UpLoadHelper.class.getSimpleName();
private static String endpoint = "http://oss-cn-beijing.aliyuncs.com";
//上传的仓库名
private static final String BUCKET_NAME="ryecatcher";
private static OSS getClient(){
// 在移动端建议使用STS的方式初始化OSSClient。-----少了个参数,先用老师这个把
OSSCredentialProvider credentialProvider = new OSSPlainTextAKSKCredentialProvider("AccessKey",
"AccessSecret");
ClientConfiguration conf = new ClientConfiguration();
conf.setConnectionTimeout(15 * 1000); // 连接超时,默认15秒。
conf.setSocketTimeout(15 * 1000); // socket超时,默认15秒。
conf.setMaxConcurrentRequest(5); // 最大并发请求书,默认5个。
conf.setMaxErrorRetry(2); // 失败后最大重试次数,默认2次。
return new OSSClient(Factory.app(), endpoint, credentialProvider, conf);
}
在UpLoadHelper进行一下初始化,这里有几个点需要注意下:
1.endpoint
这个要对应自己的服务器:
楼主用的是华北2,是北京的服务器,这个一定要对应自己OSS平台的地址。
2.BUCKET_NAME
也是换成自己的BUCKET_NAME;
3.
OSSPlainTextAKSKCredentialProvider("AccessKey",
"AccessSecret");
这里的AccessKey和AccessSecret也要换成自己的:
二、封装上传操作
private static String upload(String objKey,String uploadFilePath){
// 构造上传请求。
PutObjectRequest request = new PutObjectRequest(BUCKET_NAME, objKey, uploadFilePath);
try{//同步请求
OSS client=getClient();
//开始同步上传
PutObjectResult result= client.putObject(request);
//得到一个外网可访问的URL地址
String url=client.presignPublicObjectURL(BUCKET_NAME,objKey);
Log.d(TAG,"PublicUrl is:"+url);
return url;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
因为我们要上传头像/聊天图片/语音,如果全在一个文件夹下,查找或者分析起来都十分费劲,所以我们针对这三个分别封装了三个方法:
这三个方法基本一致,只不过是放在不同的文件夹下,我们抽一个看一下:
/**
* 上传头像
* @param path
* @return
*/
public static String uploadPortrait(String path){
String key=getPortraitObjKey(path);
return upload(key,path);
}
//201912/dsasdff.jpg -------------命名格式
private static String getPortraitObjKey(String path){
String fileMd5= HashUtil.getMD5String(new File(path));
String dateString=getDataString();
return String.format("portrait/%s/%s.jpg",dateString,fileMd5);
}
/**
* 分月存储,避免一个文件夹存储过多
* @return
*/
private static String getDataString(){
return DateFormat.format("yyyyMM",new Date()).toString();
}
上面逻辑没什么复杂的,就是给图片起个有格式的且互不相同的名字,这样服务端也好区分。毕竟三者要上传到不同的文件夹下,然后再根据月份不同进行二次划分。结构清晰明了。
三、新开线程池开启异步上传
因为为了监控流程,我们上面用了同步上传,这一块我们采取本地线程池的方式来处理这个问题。
在Factory类中,新开一个线程池:
public class Factory {
private static final Factory instance;
private final Executor executor;
static {
instance=new Factory();
}
private Factory(){
executor= Executors.newFixedThreadPool(4);
}
/**
* 返回全局的Application
* @return
*/
public static Application app(){
return zApplication.getInstance();
}
/**
* 异步运行的方法
* @param runnable
*/
public static void runOnAsync(Runnable runnable){
//拿到单例,拿到线程池,然后异步执行
instance.executor.execute(runnable);
}
}
然后在我们选择头像后,进行头像文件的上传(UpdateInfoFragment中):
/**
* 加载Uri到当前的View中
* @param uri
*/
private void loadPortrait(Uri uri){
RequestOptions options=new RequestOptions();
options.centerCrop();
Glide.with(getActivity())
.load(uri)
.apply(options)
.into(portraitView);
//拿到本地文件的地址
String localPath=uri.getPath();
Log.e("TAG","LocalFilePath:"+localPath);
//上传头像到OSS中去
Factory.runOnAsync(new Runnable() {
@Override
public void run() {
String url= UpLoadHelper.uploadPortrait(localPath);
Log.e("TAG","url"+url);
}
});
}
这样 ,头像处理完,我们的头像也会上传到OSS上,明天再贴OSS上传成功的图吧,已经两点多了,我怕 再出什么问题,有忍不住继续调试…太困了,今天就到这里。
成功了。至此,第四章关于前台搭建的初步工作算是已经完成了。接下来几章节会做后台相关的工作。请跳转至:IM-后台搭建(二).