不知不觉入职已经两个月了,到今天其实这个扫码项目V1.3.0已经开发完成了,正在测试中。有好一段时间没有作总结了,其实这段开发过程中也学习到了很多东西,封装了几个工具类,在这里一并总结出来。
一. 解析特殊格式二维码
直到V1.2.0上线了之后在发现,没有作二维码格式的解析,在公司前辈的指导下,才发现ZXing其实自带了二维码格式解析的类,只是我自己一直没有发现而已。
1.1什么是特殊格式的二维码?
首先明确一点,所有的二维码最终都是由一串字符串来生成的,有时候这串字符串像json数据那样符合了一些特殊的格式,可以根据它解析出一些字段内容,这时就是特殊格式二维码了。ZXing为我们枚举了一些特殊格式比如:WIFI,邮件,联系人,短信,地理位置,日程等等。
package com.google.zxing.client.result;
public enum ParsedResultType {
ADDRESSBOOK,//联系人
EMAIL_ADDRESS,//邮件
PRODUCT,//产品
URI,//链接
TEXT,//普通文本
GEO,//地理位置
TEL,//电话
SMS,//短信
CALENDAR,//日程
WIFI,//无线网络
ISBN,//书编号
VIN,//车架码
}
1.2 怎么解析这种特殊格式
既然二维码都是由字符串生成的,那么首先我们来看一下生成规则,比如有这样的WIFI二维码
WIFI名:test
WIFI密码:12345678
加密方式:WPA
那么它生成的二维码内容是这样的:(各位可以使用小米的分享wifi功能生成二维码再用支付宝来扫描)
WIFI:S:test;T:12345678;P:WPA;;
我没有系统的学习过这个生成规则,但是由字段内容不难看出,首先是一个“WIFI”标志符,然后后续的S,T,P标志分别给出名称,密码和加密方式,这种其实和json数据很类似。
现在问题来了,我们扫码拿到了这样的字符串该怎么解析呢?
其实很简单,刚刚提到了ZXing自带了二维码的解析方法:ResultParser.parseResult
用法:
val str="WIFI:S:test;T:12345678;P:WPA;;"
val parsedResult = ResultParser.parseResult(Result(str, null, null, null))
when (parsedResult.type) {
TEXT -> {//普通字符串
showTextContent()
}
EMAIL_ADDRESS -> {//邮件
parsedResult as EmailAddressParsedResult
showEmailContent(result)
}
TEL -> {//电话
parsedResult as TelParsedResult
showTelContent(result)
}
WIFI -> {//无线网络
parsedResult as WifiParsedResult
showWifiContent(result)
}
...//其他情况省略
}
将parsedResult 通过as关键字转换为对应对象后,比如WifiParsedResult,就可以通过:
result.ssid
WiFi名称
result.password
密码
result.networkEncryption
加密方式
这三种方法获取到想要的字段了
1.3 采用扩展函数组装出二维码展示内容
我们是不可能直接将WIFI:S:test;T:12345678;P:WPA;;
这样的内容展示给用户的,需要通过之前的方法解析过后再包装成格式比较好看的字符串再进行展示
WIFI:S:test;T:12345678;P:WPA;;
转换为:
WIFI名:test
WIFI密码:12345678
加密方式:WPA
当然,你可以把这写包装过程写在任何地方,但采用kotlin特有的扩展函数就是一个比较优雅的写法。
不了解扩展函数的同学可以看看我的关于kotlin的总结文章第8.3.1部分,其实非常简单
我们要给ParsedResult增加一个方法,首先新建一个kotlin文件(不创建类),命名就叫‘ParsedResult’,其中写法如下(注意非空判断):
//获取展示的结果 这个方法只负责将结果进行解析拼装 用于TextView中展示(展示前注意富文本处理)
fun ParsedResult.getShowText():String{
var showText=""
when(this.type){
ParsedResultType.TEXT -> {//普通字符串
showText=this.displayResult
}
ParsedResultType.EMAIL_ADDRESS -> {//邮件
val result=this as EmailAddressParsedResult
if (result.tos != null) {
showText = App.context.getString(R.string.addressee) + ": ${result.tos.joinToString(",")}\n"
}
showText = showText +
App.context.getString(R.string.email_title) + ": ${result.subject}\n" +
App.context.getString(R.string.email_body) + ": ${result.body}"
}
ParsedResultType.TEL -> {//电话
val result=this as TelParsedResult
showText = App.context.getString(R.string.phone_number) + ": ${result.number}"
}
ParsedResultType.WIFI -> {//无线网络
val result=this as WifiParsedResult
showText = App.context.getString(R.string.wifi_name) + ": ${result.ssid.replace("\"", "")}\n" +
App.context.getString(R.string.password) + ": ${result.password}\n" +
App.context.getString(R.string.wifi_encryption) + ": ${result.networkEncryption}"
}
ParsedResultType.CALENDAR -> {//日程
val result=this as CalendarParsedResult
showText = App.context.getString(R.string.summary) + ": ${result.summary}\n" +
App.context.getString(R.string.description) + ": ${result.description}\n" +
App.context.getString(R.string.location) + ": ${result.location}\n" +
App.context.getString(R.string.start_time) + ": ${TimeUnit.instance.formatTimestamp(
result.startTimestamp,
"yyyy-MM-dd HH:mm:ss"
)}\n" +
App.context.getString(R.string.end_time) + ": ${TimeUnit.instance.formatTimestamp(
result.endTimestamp,
"yyyy-MM-dd HH:mm:ss"
)}"
}
ParsedResultType.GEO -> {//地点 geo:12.111,104.3454
val result=this as GeoParsedResult
showText = App.context.getString(R.string.longitude) + ": ${result.longitude}\n" +
App.context.getString(R.string.latitude) + ": ${result.latitude}\n"
}
ParsedResultType.ADDRESSBOOK -> {//联系人信息二维码
val result=this as AddressBookParsedResult
var name = ""
var tel = ""
var address = ""
var email = ""
if (result.names != null) {
//name=result.names.joinToString(",")
name = result.names[0]
}
if (result.phoneNumbers != null) {
tel = result.phoneNumbers[0]
}
if (result.addresses != null) {
address = result.addresses[0]
}
if (result.emails != null) {
email = result.emails[0]
}
showText = App.context.getString(R.string.name) + ": ${name}\n" +
App.context.getString(R.string.tel) + ": ${tel}\n" +
App.context.getString(R.string.email) + ": ${email}\n" +
App.context.getString(R.string.address) + ": ${address}\n"
}
ParsedResultType.SMS -> {//短信
val result=this as SMSParsedResult
if (result.numbers != null) {
showText =
App.context.getString(R.string.addressee_number) + ": ${result.numbers.joinToString(",")}\n"
}
showText += App.context.getString(R.string.message_body) + ": ${result.body}"
}
ParsedResultType.URI -> {//网址 或者 第三方平台链接
val result=this as URIParsedResult
showText=this.displayResult
}
else -> {
showText=this.displayResult
}
}
return showText
}
用法就很优雅简洁了:
val text =result.getShowText()
1.4 采用富文本美化展示内容
参考:https://www.jianshu.com/p/e986bc1a6b62
本来通过扩展函数获得的showText也是一串字符串,但是我希望它的字段名和字段内容有一个区分,这个时候就需要用到富文本了,简单说一下思路:showText都是以回车“\n”结尾,那就以"\n"将字符串分为多个数组,对每个元素中,冒号之前的内容缩小为90%,并且变色处理,最后将所有内容连接起来,这是我简单封装的一个方法:
//将扫码展示字段转化为富文本 修改部分文字大小以及颜色
private fun formatShowText(string: String): SpannableStringBuilder {
val stringBuilder = SpannableStringBuilder()
val list = string.split("\n")
for (str in list) {
val spannableString =
if (str != list[list.size - 1]) {
SpannableString(str + "\n")
} else {//最后一行不加回车
SpannableString(str)
}
spannableString.setSpan(
RelativeSizeSpan(0.9f),
0,
spannableString.indexOf(":") + 1,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
spannableString.setSpan(
ForegroundColorSpan(this.resources.getColor(R.color.result_color_write)),
0,
spannableString.indexOf(":") + 1,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
stringBuilder.append(spannableString)
}
return stringBuilder
}
二. 根据不同格式的二维码采用不同的后续动作
2020.9.18更新:后续动作都是通过Intent唤醒不同的系统应用来完成,以下内容纯在不准确甚至错误和误导。仅作为参考。
关于Intent的用法官方文档上写的非常清楚明白,建议花20分钟认真学习研究以下
https://developer.android.com/guide/components/intents-common?hl=zh-cn
之前看到了zxing枚举出的几个不同的二维码类型,这些二维码类型扫描解析出来后都应该会有后续的动作,比如wifi类型的二维码,可以提供一个按钮给用户,直接连接。联系人的二维码,可以添加到通讯。邮件的二维码可以打开邮件并发送。
我开发完,发现这其中最难的点其实就是wifi的连接,因为android 10注重隐私,所以其wifi连接方式和之前的版本有些许不同。
那么就从最难的一部分开始:
2.1 应用内连接wifi
https://www.jianshu.com/p/d5176a57dacc
2.2 打开系统短信
action_btn.setOnClickListener {
//传入号码
val uri = Uri.parse("smsto:${numbers.joinToString(";")}")
val intent =
Intent(Intent.ACTION_SENDTO, uri)
//传入信息内容
intent.putExtra("sms_body", body)
startActivity(intent)
}
2.3 打开系统拨号
action_btn.setOnClickListener {
//传入电话号码
val uri = Uri.parse("tel:${number}")
val intent =
Intent(Intent.ACTION_DIAL, uri)
startActivity(intent)
}
2.4 打开系统通讯录 进行 添加/修改
参考: 使用 intent 修改联系人
action_btn.setOnClickListener {
// Creates a new Intent to insert a contact
val intent = Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
putExtra(ContactsContract.Intents.Insert.NAME, name)
// Inserts an email address
putExtra(ContactsContract.Intents.Insert.EMAIL, email)
// Inserts a phone number
putExtra(ContactsContract.Intents.Insert.PHONE, tel)
//住址
putExtra(ContactsContract.Intents.Insert.POSTAL, address)
}
startActivity(intent)
}
2.5 打开系统日历新建日程
action_btn.setOnClickListener {
val startMillis: Long = Calendar.getInstance().run {
timeInMillis = result.startTimestamp
timeInMillis
}
val endMillis: Long = Calendar.getInstance().run {
timeInMillis = result.endTimestamp
timeInMillis
}
val intent = Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis)
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis)
.putExtra(CalendarContract.Events.TITLE, result.summary)
.putExtra(CalendarContract.Events.DESCRIPTION, result.description)
.putExtra(CalendarContract.Events.EVENT_LOCATION, result.location)
startActivity(intent)
}
2.6 打开邮箱应用发送邮件
action_btn.setOnClickListener {
var intent = Intent(Intent.ACTION_SEND)
intent.type = "message/rfc822"
//收件人
intent.putExtra(
Intent.EXTRA_EMAIL,
result.tos
)
//主题
intent.putExtra(Intent.EXTRA_SUBJECT, result.subject);
//内容
intent.putExtra(Intent.EXTRA_TEXT, result.body);
intent =
Intent.createChooser(intent, getString(R.string.choose_app_to_send_email))
startActivity(intent)
}
2.7 打开地图应用 传入经纬度
action_btn.setOnClickListener {
try {
val uri = Uri.parse("geo:${result.latitude},${result.longitude}")
var intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
AnalyticsManager.instance.sendEvent(ActionEvent.OPEN_MAP)
} catch (e: Exception) {
Toast.makeText(this, getString(R.string.no_app_hint), Toast.LENGTH_SHORT).show()
}
}
2.8打开第三方应用 注意错误捕捉
action_btn.setOnClickListener {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(result))
startActivity(intent)
} catch (e: Exception) { //防止crash (如果手机上没有安装处理某个scheme开头的url的APP, 会导致crash)
//没有安装该app时
Toast.makeText(this, getString(R.string.no_app_hint), Toast.LENGTH_SHORT).show()
}
}
2.9打开系统浏览器
try {
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
customTabsIntent.launchUrl(this, Uri.parse(result.displayResult))
} catch (e: Exception) {
//url格式错误
}
2.10 调用系统分享 分享文本
share_btn.setOnClickListener {
var shareIntent = Intent()
shareIntent.action = Intent.ACTION_SEND
//确定要分享的类型为文本
shareIntent.type = "text/plain"
//传入分享内容scanResult
shareIntent.putExtra(Intent.EXTRA_TEXT, string)
//分享选择框标题
shareIntent =
Intent.createChooser(shareIntent, "选择应用进行分享")
startActivity(shareIntent)
}
2.11 将文本复制到剪贴板
copy_btn.setOnClickListener {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
//根据scanResult创建clipData
val clipData = ClipData.newPlainText("Lable", string)
clipboardManager.setPrimaryClip(clipData)
AnalyticsManager.instance.sendEvent(ActionEvent.COPY_RESULT)
Toast.makeText(this, "复制成功", Toast.LENGTH_SHORT).show()
}
三. Kotlin枚举的用法
掌握枚举的用法非常重要,这会为你精简非常多的代码,特别是switch判断(kotlin中是when),下面是一个比较全面的例子,一个朋友的枚举类(没有多少朋友,是我了)
Enum官方文档:https://developer.android.com/reference/java/lang/Enum?hl=en
//朋友的枚举
enum class Friend(private val sex: String) {
XIAOMING("男") {
override fun getHobby(): String {
return "唱歌"
}
//小明这个枚举的单独方法
fun getHeight():Int{
return 169
}
},
XIAOHONG("女") {
override fun getHobby(): String {
return "跳舞"
}
},
TOM("男") {
override fun getHobby(): String {
return "Rap"
}
},
JUDY("女") {
override fun getHobby(): String {
return "篮球"
}
},
BOB("男") {
override fun getHobby(): String {
return "Music"
}
};
//获取性别
fun getSex(): String = sex
//获取爱好(抽象方法 每个枚举中单独实现)
abstract fun getHobby():String
}
以上就是一个枚举类了,包含了枚举类日常用法。
每个枚举都包含了一条属性叫做sex
,这个属性值在创建枚举时需要给出来。可以通过下方getSex
获取。
并且Friend提供了一个抽象方法getHobby,然后在每个枚举类中都实现了这个抽象方法(这里就是说一个同样的方法,每个枚举需要执行某些不同的语句)
这是官方文档给出的枚举类通用方法:
四.Git小知识
遇到的问题:我在我的分支dev上进行开发,但开发过程中,master分支产生了非常非常重要的修改,需要将修改内容合并到我的dev分支,再继续开发。
解决办法:首先明确一点,我们不能操作master,但是可以在master现有的基础上开启一个新的分支future,然后将future分支合并到dev,这时可能会有冲突需要解决,解决完成后,可以删除掉future分支,直接继续在dev进行开发就行。
(这个问题其实用rebase就可以很好的解决,但是我不怎么熟练rebase操作,所以用的这个笨办法)
五 .Android 控件阴影效果
请参考链接:
https://www.jianshu.com/p/8e16a574abc0
简单来说就是,任意ViewGroup布局加入下边三条属性就可以很简单的完成(效果图可以看1.4中的截图)
android:elevation="3dp"
android:outlineProvider="background"
android:translationZ="3dp"
六 .复习并扩展一下视图动画
一开始我觉得视图动画很简单嘛,就是平移,伸缩,旋转,透明。最近再项目里用到挺多的,做一个扩展和总结。
看以下动画想象它是什么样子的:
我来描述一下:
这个动画总共3秒,其实主要就是一个简单的从上到下平移的动画
但是在最开始0.5秒内,view的透明度会从0变化到1,然后0.5~2秒内透明度继续从1变化到6(但是视觉效果不发生变化)
最后2.5秒到3秒,透明度从1再次变化到0,让view在视觉效果上消失。
这里一个关键属性就是startOffset
,调用startAnimation
后延迟一段时间再开始动画。
所以我们这个只用了一个动画就完成了原本需要用AnimationSet
才能完成的效果。
用法:
val upToDownAnim: Animation =
AnimationUtils.loadAnimation(this, R.anim.anim_up_down_translate)
view.startAnimation(upToDownAnim)
效果:
最后动画我推荐几个链接:
视图动画,补间动画
https://www.jianshu.com/p/16e0d4e92bb2
属性动画
https://www.jianshu.com/p/ce689b9d2d46
七 .一个单纯的定时器CountDownTimer
比如我要倒计时10秒,步长为1秒
val countDownTimer = object : CountDownTimer(10 * 1000, 1000) {
//倒计时完成
override fun onFinish() {
Log.d("倒计时", "倒计时完成")
}
//秒数改变
override fun onTick(millisUntilFinished: Long) {
Log.d("倒计时","倒计时${millisUntilFinished.toString()}")
}
}
countDownTimer.start()//开始计时
//countDownTimer.cancel()//取消计时