IM-前台搭建(二)

接入图片剪切库

图片剪切库我们使用的是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-后台搭建(二).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值