美文网首页简化开发
扫码 项目总结-v1.1.0

扫码 项目总结-v1.1.0

作者: 番茄tomato | 来源:发表于2020-07-07 14:59 被阅读0次
    总结

    一. 选择本地图片进行二维码扫描

    不得不说BGAQRCode真的是功能强大,直接有一个方法就是扫描本地图片或者bitmap,

    decodeQRCode
    也就是说只要传入本地图片的地址,或者生成的bitmap对象,就可以进行解析,回调onScanQRCodeSuccess方法。
    所以需要做的就是选择本地图片得到图片地址或者生成bitmap
    第一步: selectLocalPic()函数 调用系统相册选择图片,明确需要选择的文件是image类型,然后使用startActivityForResult函数传入intent和requsetCode启动选择图片的activity(不同系统界面会有区别)
        //选择本地图片
        private fun selectLocalPic() {
            val intent = Intent("android.intent.action.GET_CONTENT")
            intent.type = "image/*"
            startActivityForResult(intent, SELECT_LOCAL_IMAGE)
        }
    

    第二步:onActivityResult()中获取选择图片的uri(注意 这个uri并非图片地址)

        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            when (requestCode) {
                SELECT_LOCAL_IMAGE -> {
                    if (resultCode == Activity.RESULT_OK) {
                        val selectedImage = data?.data
                        selectedImage?.let {
                            scanLocalPic(it)
                        }
                    }
                }
            }
        }
    

    第三步:根据图片uri解析图片地址,并进行扫描:
    (难点)

        //扫描本地图片
        private fun scanLocalPic(uri: Uri) {
            val path = GetPhotoFromAlbum.getRealPathFromUri(this, uri)
            Log.d("图片path", "$path")
            scanView.decodeQRCode(path)
        }
    

    这里是一个难点,首先java和kotlin的处理方式不同,使用java的解析uri方式,在kotlin中为null,并且对于不同api设备,处理方式也不同。所以我在网上找到了一个工具类GetPhotoFromAlbum
    原地址在这里:
    https://blog.csdn.net/qq_37216116/article/details/88045161
    用于kotlin开发中将图片uri解析为绝对路径:

    import android.annotation.SuppressLint
    import android.content.ContentUris
    import android.content.Context
    import android.database.Cursor
    import android.net.Uri
    import android.os.Build
    import android.provider.DocumentsContract
    import android.provider.MediaStore
    
    object GetPhotoFromAlbum {
    
    
        /**
         * 根据Uri获取图片的绝对路径
         * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
         */
        fun getRealPathFromUri(context: Context, uri: Uri): String? {
    
            val sdkVersion = Build.VERSION.SDK_INT
    
            return if (sdkVersion >= 19) { // api >= 19
    
                getRealPathFromUriAboveApi19(context, uri)
    
            } else { // api < 19
    
                getRealPathFromUriBelowAPI19(context, uri)
    
            }
    
        }
    
    
        /**
         * 适配api19以下(不包括api19),根据uri获取图片的绝对路径
         *
         * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
         */
    
        private fun getRealPathFromUriBelowAPI19(context: Context, uri: Uri): String? {
            return getDataColumn(context, uri, null, null)
        }
    
    
        /**
         * 适配api19及以上,根据uri获取图片的绝对路径
         *
         * @return 如果Uri对应的图片存在, 那么返回该图片的绝对路径, 否则返回null
         */
    
        @SuppressLint("NewApi")
        private fun getRealPathFromUriAboveApi19(context: Context, uri: Uri): String? {
    
            var filePath: String? = null
    
            if (DocumentsContract.isDocumentUri(context, uri)) {
    
                // 如果是document类型的 uri, 则通过document id来进行处理
                val documentId = DocumentsContract.getDocumentId(uri)
    
                if (isMediaDocument(uri)) { // MediaProvider
                    // 使用':'分割
                    val id = documentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
                    val selection = MediaStore.Images.Media._ID + "=?"
                    val selectionArgs = arrayOf(id)
                    filePath =
                        getDataColumn(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, selectionArgs)
    
                } else if (isDownloadsDocument(uri)) { // DownloadsProvider
    
                    val contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"),
                        java.lang.Long.valueOf(documentId)
                    )
                    filePath = getDataColumn(context, contentUri, null, null)
                }
    
            } else if ("content".equals(uri.scheme!!, ignoreCase = true)) {
    
                // 如果是 content 类型的 Uri
    
                filePath = getDataColumn(context, uri, null, null)
    
            } else if ("file" == uri.scheme) {
    
                // 如果是 file 类型的 Uri,直接获取图片对应的路径
                filePath = uri.path
            }
            return filePath
        }
    
    
        /**
         * 获取数据库表中的 _data 列,即返回Uri对应的文件路径
         */
    
        private fun getDataColumn(context: Context, uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
    
            var path: String? = null
            val projection = arrayOf(MediaStore.Images.Media.DATA)
            var cursor: Cursor? = null
    
            try {
                cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
                if (cursor != null && cursor.moveToFirst()) {
                    val columnIndex = cursor.getColumnIndexOrThrow(projection[0])
                    path = cursor.getString(columnIndex)
                }
    
            } catch (e: Exception) {
                cursor?.close()
            }
    
            finally {
                cursor?.close()
            }
            return path
        }
    
    
        /**
         * @param uri the Uri to check
         *
         * @return Whether the Uri authority is MediaProvider
         */
    
        private fun isMediaDocument(uri: Uri): Boolean {
            return "com.android.providers.media.documents" == uri.authority
        }
    
        /**
         * @param uri the Uri to check
         *
         * @return Whether the Uri authority is DownloadsProvider
         */
    
        private fun isDownloadsDocument(uri: Uri): Boolean {
            return "com.android.providers.downloads.documents" == uri.authority
        }
    }
    

    第四步:onScanQRCodeSuccess中做好判断,因为选择图片扫码的话,结果可能是空的,提示“未识别到二维码”

    二. 选择本地图片进行扫码所需要的权限请求

    在1.0的开发中也提到了权限请求,当时只有一个相机权限,但是现在增加了文件读写权限,这里也作一下修改,使用官方权限请求框架EasyPermissions
    参考文章:https://www.jianshu.com/p/886a1ffb02b9
    第一步:在Activity中实现EasyPermissions.PermissionCallbacks接口,并且在onRequestPermissionsResult中添加一句话EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);

     @Override
        public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    
            // Forward results to EasyPermissions
            EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
        }
    
        //成功
        @Override
        public void onPermissionsGranted(int requestCode, List<String> list) {
            // Some permissions have been granted
            // ...
        }
    
        //失败
        @Override
        public void onPermissionsDenied(int requestCode, List<String> list) {
            // Some permissions have been denied
            // ...
        }
    

    第二步:准备好需要请求的权限列表:

       private val perms = arrayOf(
            //外部文件读写权限和相机权限
            Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE
        )
    

    第三步:检查并请求权限(onCreate中调用checkAndRequestPermission):

        private fun checkAndRequestPermission() {
            //检查是否有权限
            if (EasyPermissions.hasPermissions(this, *perms)) {
                isPermissionAuth = true
            } else {
                //请求权限 四个参数:回调对像实现接口的activity 权限请求对话框标题提示 权限请求码 需要请求的权限
                EasyPermissions.requestPermissions(this, "", PERMS_REQUEST_CODE, *perms)
            }
        }
    

    第四步onPermissionsGranted(申请通过)/onPermissionsDenied(权限拒绝)回调中完成相应操作

    第五步 以上过程都完成了 读写权限请求成功 但是选择图片时还是报错说没有权限?
    原因:AndroidQ以下,还是使用老的文件存储方式。Android Q仍然使用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE作为存储相关运行时权限,但现在即使获取了这些权限,访问外部存储也受到了限制。
    解决办法:在manifest的applicaiton标签添加 android:requestLegacyExternalStorage="true"
    参考:https://blog.csdn.net/MarketAndTechnology/article/details/105955441

    三. 使用Android本地数据库保存扫码记录

    这个需求涉及到数据库的使用了,原本打算用SQLite的,但是这玩意封装起来很烦,突然发现公司的框架中用到的是Anko
    文档地址:https://github.com/Kotlin/anko/wiki/Anko-SQLite
    我总结的文章:
    https://www.jianshu.com/p/f4f42950ec69

    四. 项目多语言

    项目默认语言为英语,现在要添加日语,操作步骤如下:
    1.右键res->New->Android Resouce File


    image.png
    image.png
    image.png
    image.png

    5.点击OK后就可以在res目录中看到对应的文件了


    image.png

    需要注意的是,程序中用到的字段都需要在Strings中声明,并且特定地区的Strings也需要有同样的字段不同语言的翻译。如果某个字段在默认的String中有,而特点地区中没有,IDE会给出警告并使用默认内容。相反,如果特定地区中有的字段,而默认String中没有 则会报错。

    补充点:如果想根据国家和地区显示不同图片怎么操作?
    比入我这里有两张gif,分别是英文和日文,这个gif需要在启动页一开始进行播放

    1
    2
    同理:英文gif作为默认选择,放在res下的drawable中命名为start_page_gif.gif。那么此时需要新建日本地区的drawable文件夹,按以下步骤:
    res右键--->new--->Android Resource Directory
    image.png
    确定资源文件夹类型
    image.png
    选择好国家或地区后文件夹会自动命名
    image.png
    点击ok,将同名的日文gif资源文件放在这个文件夹下,系统会自动根据当前语言地区选择对应资源文件:
    image.png
    当然也要注意默认的资源必须存在

    五. 状态栏全黑问题

    为了隐藏掉每个界面自带的appbar,使用了以下主题:

        <style name="AppTheme.FullScreen" parent="Theme.AppCompat.Light.NoActionBar">
            <item name="windowNoTitle">true</item>
            <item name="android:windowFullscreen">true</item>
        </style>
    

    <item name="windowNoTitle">true</item> 去掉标题栏
    <item name="android:windowFullscreen">true</item>隐藏状态栏

    当添加了隐藏状态栏的代码后,状态栏全黑,无内容。所以把这句话删了,但是此时出现一个问题:状态栏内容正常显示,但是却挡住了界面内容

    image.png
    其实只需要再在界面根布局中加一句话就行了:
    android:fitsSystemWindows="true"
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:animateLayoutChanges="true"
        android:fitsSystemWindows="true">
    

    六. 其他细节点

    6.1 约束布局形成链之后,链头约束的定位改变时,各个控件也会改变,会有一个很舒服的过渡动画,自己看图,再吹一波约束布局
    约束布局
    这里主界面根布局中加了android:animateLayoutChanges="true"这句话,所以控件的消失会渐入渐出
    6.2 xml完成卡片背景

    https://blog.csdn.net/qq1171574843/article/details/89386733
    关于xml生成背景比如圆角 阴影这些 待学习(和动画比起来应该不难吧,花1小时掌握 2020-7-9)

    6.3 修改项目包名

    不要傻乎乎的去修改项目的文件夹路径名称,吃力不讨好,包名就像是项目的身份证号码,在module的build.gradle文件的defaultConfig下修改applicationId

    image.png
    6.4 关于报错

    Error: Cannot fit requested classes in a single dex file (# methods: 66190 > 65536)
    原因是引入SDK后 方法太多大于限制65536,解决方法:
    https://blog.csdn.net/hanshiying007/article/details/105219378/
    gradle文件的defaultConfig默认配置里面增加:
    multiDexEnabled true
    在dependencies中添加:
    implementation 'com.android.support:multidex:1.0.3'
    或者(开启androidx的话)
    implementation 'androidx.multidex:multidex:2.0.1'
    自定义的Application的onCreate()中添加:
    MultiDex.install(this);

    相关文章

      网友评论

        本文标题:扫码 项目总结-v1.1.0

        本文链接:https://www.haomeiwen.com/subject/beaoqktx.html