修复 DialogManager 内存泄漏的问题 修复 Android 11 无法使用意图的问题 修复 Bugly 上报 Lottie 资源异常的问题 优化 SettingBar 自定义控件的代码逻辑master
* [为什么没有用 ButterKnife](#为什么没有用-butterknife) | * [为什么没有用 ButterKnife](#为什么没有用-butterknife) | ||||
* [为什么不用 ViewBinding](#为什么不用-viewbinding) | |||||
* [为什么没有用 ViewBinding](#为什么没有用-viewbinding) | |||||
* [为什么不用 DataBinding](#为什么不用-databinding) | |||||
* [为什么没有用 DataBinding](#为什么没有用-databinding) | |||||
* [为什么没有用组件化](#为什么没有用组件化) | * [为什么没有用组件化](#为什么没有用组件化) | ||||
* [为什么不用今日头条的适配方案](#为什么不用今日头条的适配方案) | |||||
* [为什么没有集成界面侧滑功能](#为什么没有集成界面侧滑功能) | |||||
* [为什么没有用今日头条的适配方案](#为什么没有用今日头条的适配方案) | |||||
* [字体大小为什么不用 dp 而用 sp](#字体大小为什么不用-dp-而用-sp) | * [字体大小为什么不用 dp 而用 sp](#字体大小为什么不用-dp-而用-sp) | ||||
* [为什么不用 DialogFragment 来防止内存泄漏](#为什么不用-dialogfragment-来防止内存泄漏) | |||||
* [为什么没有用 DialogFragment 来防止内存泄漏](#为什么没有用-dialogfragment-来防止内存泄漏) | |||||
* [为什么不用腾讯 X5 WebView](#为什么不用腾讯-x5-webview) | |||||
* [为什么没有用腾讯 X5 WebView](#为什么没有用腾讯-x5-webview) | |||||
* [为什么不用单 Activity 多 Fragment](#为什么不用单-activity-多-fragment) | |||||
* [为什么没有用单 Activity 多 Fragment](#为什么没有用单-activity-多-fragment) | |||||
* [为什么不用 ConstraintLayout 来写布局](#为什么不用-constraintlayout-来写布局) | |||||
* [为什么没有用 ConstraintLayout 来写布局](#为什么没有用-constraintlayout-来写布局) | |||||
* [为什么不拆成多个框架来做这件事](#为什么不拆成多个框架来做这件事) | * [为什么不拆成多个框架来做这件事](#为什么不拆成多个框架来做这件事) | ||||
* [为什么不加入 EventBus](#为什么不加入-eventbus) | * [为什么不加入 EventBus](#为什么不加入-eventbus) | ||||
* [为什么不用 Retrofit 和 RxJava](#为什么不用-retrofit-和-rxjava) | |||||
* [为什么没有用 Retrofit 和 RxJava](#为什么没有用-retrofit-和-rxjava) | |||||
* [为什么没有用 Jetpack 全家桶](#为什么没有用-jetpack-全家桶) | * [为什么没有用 Jetpack 全家桶](#为什么没有用-jetpack-全家桶) | ||||
* [为什么没有关于列表多 type 的封装](#为什么没有关于列表多-type-的封装) | * [为什么没有关于列表多 type 的封装](#为什么没有关于列表多-type-的封装) | ||||
* [为什么不用 Dagger 框架](#为什么不用-dagger-框架) | |||||
* [为什么没有用 Dagger 框架](#为什么没有用-dagger-框架) | |||||
* [这不就是一个模板工程换成我也能写一个](#这不就是一个模板工程换成我也能写一个) | * [这不就是一个模板工程换成我也能写一个](#这不就是一个模板工程换成我也能写一个) | ||||
* [轮子哥你怎么看待层出不穷的新技术](#轮子哥你怎么看待层出不穷的新技术) | * [轮子哥你怎么看待层出不穷的新技术](#轮子哥你怎么看待层出不穷的新技术) | ||||
* [为什么没有集成界面侧滑功能](#为什么没有集成界面侧滑功能) | |||||
#### 为什么没有用 MVP | #### 为什么没有用 MVP | ||||
 |  | ||||
* 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) | * 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) | ||||
#### 为什么不用 ViewBinding | |||||
#### 为什么没有用 ViewBinding | |||||
* 首先 ViewBinding 和 ButterKnife 有一个相同的毛病,就是自动生成一个类,然后在这个类里面进行 findViewById,但是有一个致命的缺点,每个 `Activity / Fragment / Dialog / Adapter` 都需要先初始化 ViewBinding 对象,因为每次生成的类名都是不固定的,所以无法在基类中封装处理,并且每次都要写 `binding.xxx` 才能操作控件。 | * 首先 ViewBinding 和 ButterKnife 有一个相同的毛病,就是自动生成一个类,然后在这个类里面进行 findViewById,但是有一个致命的缺点,每个 `Activity / Fragment / Dialog / Adapter` 都需要先初始化 ViewBinding 对象,因为每次生成的类名都是不固定的,所以无法在基类中封装处理,并且每次都要写 `binding.xxx` 才能操作控件。 | ||||
* 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) | * 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) | ||||
#### 为什么不用 DataBinding | |||||
#### 为什么没有用 DataBinding | |||||
* DataBinding 最大的优势在于,因为它可以在 xml 直接给 View 赋值,但它的优点正是它最致命的缺点,当业务逻辑简单时,会显得格外美好,但是一旦判断条件复杂起来,由于 xml 属性不能换行的特性,会导致无法在 xml 直接赋值又或者很长的一段代码堆在布局中,间接导致 CodeReview 时异常艰难,更别说在原有的基础上继续更新迭代,这对每一个开发者来讲无疑是一个巨大的灾难。 | * DataBinding 最大的优势在于,因为它可以在 xml 直接给 View 赋值,但它的优点正是它最致命的缺点,当业务逻辑简单时,会显得格外美好,但是一旦判断条件复杂起来,由于 xml 属性不能换行的特性,会导致无法在 xml 直接赋值又或者很长的一段代码堆在布局中,间接导致 CodeReview 时异常艰难,更别说在原有的基础上继续更新迭代,这对每一个开发者来讲无疑是一个巨大的灾难。 | ||||
* AndroidProject 面对的是大众开发者,所以更倾向中小型的项目代码的设计,虽然我没有做过大型的项目,但是在我看来是差不多的,最大的不同可能是代码分类方式的不同,该做的事情不会少,该写的代码也不会少,就是业务和代码的体量上比我们大,所以他们要处理体量大所带来的的问题。 | * AndroidProject 面对的是大众开发者,所以更倾向中小型的项目代码的设计,虽然我没有做过大型的项目,但是在我看来是差不多的,最大的不同可能是代码分类方式的不同,该做的事情不会少,该写的代码也不会少,就是业务和代码的体量上比我们大,所以他们要处理体量大所带来的的问题。 | ||||
#### 为什么不用今日头条的适配方案 | |||||
#### 为什么没有集成界面侧滑功能 | |||||
* AndroidProject 其实有加入过这个功能,但是在 [v9.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/9.0) 就移除了,原因是第三方侧滑框架 [BGASwipeBackLayout](https://github.com/bingoogolapple/BGASwipeBackLayout-Android) 在 Android 9.0 上面会[闪屏](https://github.com/bingoogolapple/BGASwipeBackLayout-Android/issues/173),并且还是 **100% 必现**,**用户体验极差**,我也跟作者反馈过这个问题,但结果不了了之,所以不得不移除。但是到了 [v10.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/10.0),我又加上界面侧滑功能了,不过这次我换成了 [SmartSwipe](https://github.com/luckybilly/SmartSwipe) 来做,但是我又再一次失望了,这个框架在 Android 11 上面,如果 Activity 上有 WindowManager 正在显示,然后使用界面侧滑,那么会出现闪屏的情况,具体效果如下图: | |||||
 | |||||
* 就这个情况我也联系过作者,并详细阐述了产生的原因和具体的复现步骤,但是我等了三天连个回复都没有,实属有些让我心寒,在等待的期间我看到 Github 的 issue 已经基本没有回复了,并且最后一次提交是在 13 个月前了,种种迹象都已经表明,所以经过慎重考虑,最终决定在 [v12.1 版本](https://github.com/getActivity/AndroidProject/releases/tag/12.1) 移除界面侧滑功能。 | |||||
#### 为什么没有用今日头条的适配方案 | |||||
* 关于屏幕适配方案,其实不能说头条的方案就是最好的,其实谷歌已经针对屏幕适配做了处理,就是 dp 和 sp ,而 dp 的计算转换是由屏幕的像素决定,系统只认 px 单位, dp 需要进行转换,比如 1dp 等于几个 px ,这个时候就需要基数进行转换,比如 1dp = 2px,这个基数就是 2。 | * 关于屏幕适配方案,其实不能说头条的方案就是最好的,其实谷歌已经针对屏幕适配做了处理,就是 dp 和 sp ,而 dp 的计算转换是由屏幕的像素决定,系统只认 px 单位, dp 需要进行转换,比如 1dp 等于几个 px ,这个时候就需要基数进行转换,比如 1dp = 2px,这个基数就是 2。 | ||||
* 显然这种方式是不合理的,也非常地不人性化。网上这种方案可能主要就是为了解决把控件宽高写死之后,在某些字体上显示比较大的机型会出现字显示不全的问题,而这种把控件宽高写死的方式本身也是不合理的,应该在不得已的情况下才把控件的宽高写死,一般情况下我们应当使用自适应的方式,让控件自己测量自己的宽高,特别是在有显示字体的控件下,就更不应该把宽高写死。 | * 显然这种方式是不合理的,也非常地不人性化。网上这种方案可能主要就是为了解决把控件宽高写死之后,在某些字体上显示比较大的机型会出现字显示不全的问题,而这种把控件宽高写死的方式本身也是不合理的,应该在不得已的情况下才把控件的宽高写死,一般情况下我们应当使用自适应的方式,让控件自己测量自己的宽高,特别是在有显示字体的控件下,就更不应该把宽高写死。 | ||||
#### 为什么不用 DialogFragment 来防止内存泄漏 | |||||
#### 为什么没有用 DialogFragment 来防止内存泄漏 | |||||
* DialogFragment 的出现就是为了解决 Dialog 和 Activity 生命周期不同步导致的内存泄漏问题,在 AndroidProject 曾经引入过,也经过了很多个版本的更新迭代,不过在 [10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 版本后就被移除了,原因是 Dialog 虽然有坑,但是 DialogFragment 也有坑,可以说解决了一个问题又引发了各种问题。先来细数 我在 DialogFragment 上踩过的各种坑: | * DialogFragment 的出现就是为了解决 Dialog 和 Activity 生命周期不同步导致的内存泄漏问题,在 AndroidProject 曾经引入过,也经过了很多个版本的更新迭代,不过在 [10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 版本后就被移除了,原因是 Dialog 虽然有坑,但是 DialogFragment 也有坑,可以说解决了一个问题又引发了各种问题。先来细数 我在 DialogFragment 上踩过的各种坑: | ||||
* 看过这些问题,你是不是和我一样,感觉这 DialogFragment 不是一般的坑,不过最终我放弃了使用 DialogFragment,并不是因为 DialogFragment 又出现了新问题,而是我想到了更好的方案来代替 DialogFragment,方案就是 Application.registerActivityLifecycleCallbacks,想必大家现在已经猜到我想干啥,和 DialogFragment 的作用一样,通过监听 Activity 的方式来管控 Dialog 的生命周期,但唯一不同的是,它不会出现刚刚说过 DialogFragment 的那些问题,这种方式在 AndroidProject 上迭代了几个版本过后,这期间没有发现新的问题,也没有收到别人反馈过这块的问题,证明这种方式是可行的。 | * 看过这些问题,你是不是和我一样,感觉这 DialogFragment 不是一般的坑,不过最终我放弃了使用 DialogFragment,并不是因为 DialogFragment 又出现了新问题,而是我想到了更好的方案来代替 DialogFragment,方案就是 Application.registerActivityLifecycleCallbacks,想必大家现在已经猜到我想干啥,和 DialogFragment 的作用一样,通过监听 Activity 的方式来管控 Dialog 的生命周期,但唯一不同的是,它不会出现刚刚说过 DialogFragment 的那些问题,这种方式在 AndroidProject 上迭代了几个版本过后,这期间没有发现新的问题,也没有收到别人反馈过这块的问题,证明这种方式是可行的。 | ||||
#### 为什么不用腾讯 X5 WebView | |||||
#### 为什么没有用腾讯 X5 WebView | |||||
* 首先我问大家一个问题,腾讯出品的 X5 WebView 就一定比原生 WebView 好吗?我觉得未必,我依稀记得 Android 9.0 还是 Android 10 刚出来的时候,我点了升级按钮,然后就发现微信和 QQ 的网页浏览卡得让我怀疑人生,不过后面突然某一天就变好了,从这件事可以得出两点结论: | * 首先我问大家一个问题,腾讯出品的 X5 WebView 就一定比原生 WebView 好吗?我觉得未必,我依稀记得 Android 9.0 还是 Android 10 刚出来的时候,我点了升级按钮,然后就发现微信和 QQ 的网页浏览卡得让我怀疑人生,不过后面突然某一天就变好了,从这件事可以得出两点结论: | ||||
* 基于以上两点,我的个人建议是优先使用原生 WebView,如果不满足需求了,可以自行替换成 X5 WebView,当然不是说 X5 WebView 一定不好,用原生 WebView 一定就好,而是 AndroidProject 的目标是稳中求胜,另外一个是 AndroidProject 中有针对 WebView 做统一封装,后续替换成 X5 WebView 的成本还算是相对较低的。 | * 基于以上两点,我的个人建议是优先使用原生 WebView,如果不满足需求了,可以自行替换成 X5 WebView,当然不是说 X5 WebView 一定不好,用原生 WebView 一定就好,而是 AndroidProject 的目标是稳中求胜,另外一个是 AndroidProject 中有针对 WebView 做统一封装,后续替换成 X5 WebView 的成本还算是相对较低的。 | ||||
#### 为什么不用单 Activity 多 Fragment | |||||
#### 为什么没有用单 Activity 多 Fragment | |||||
* 这个问题在前几年是一个比较火热的话题,我表示很能理解,因为新鲜的事物总是能勾起人的好奇,让人忍不住试一试,但是我先问大家一个问题,单 Activity 多 Fragment 和写多个 Activity 有什么优点?大家第一个反应应该是每写一个页面都不需要在清单文件中注册了,但是这个真的是优点吗?我可以很明确地告诉大家,我已经写了那么多句代码,不差那句在清单文件注册的代码。那么究竟什么才是对我们有价值的?我觉得就两点,一是减少前期开发的工作量,二是降低后续维护的难度。所以省那一两句有前途吗?我们是差那一两句代码的人吗?如果这种模式能够帮助我们写好代码,这个当然是有价值的,非常值得一试的,否则就是纯属瞎扯淡。不仅如此,我个人觉得这种模式有很大的弊端,会引发很多问题,例如: | * 这个问题在前几年是一个比较火热的话题,我表示很能理解,因为新鲜的事物总是能勾起人的好奇,让人忍不住试一试,但是我先问大家一个问题,单 Activity 多 Fragment 和写多个 Activity 有什么优点?大家第一个反应应该是每写一个页面都不需要在清单文件中注册了,但是这个真的是优点吗?我可以很明确地告诉大家,我已经写了那么多句代码,不差那句在清单文件注册的代码。那么究竟什么才是对我们有价值的?我觉得就两点,一是减少前期开发的工作量,二是降低后续维护的难度。所以省那一两句有前途吗?我们是差那一两句代码的人吗?如果这种模式能够帮助我们写好代码,这个当然是有价值的,非常值得一试的,否则就是纯属瞎扯淡。不仅如此,我个人觉得这种模式有很大的弊端,会引发很多问题,例如: | ||||
* 如果单 Activity 多 Fragment 不能为我们创造太大的价值时,这种模式根本就不值得我们去做,因为我们最终得到的,永远抵不上付出的。 | * 如果单 Activity 多 Fragment 不能为我们创造太大的价值时,这种模式根本就不值得我们去做,因为我们最终得到的,永远抵不上付出的。 | ||||
#### 为什么不用 ConstraintLayout 来写布局 | |||||
#### 为什么没有用 ConstraintLayout 来写布局 | |||||
* 大家如果有仔细观察的话,会发现 AndroidProject 其实没有用到 ConstraintLayout 布局,在这里谈谈我对这个布局的看法,约束布局有一个优点,没有布局嵌套,所以能减少测量次数,从而提升布局绘制的速度,但是优点也是它的缺点,正是因为没有布局嵌套,View 也就没有层级概念,所以它需要定义很多 ViewID 来约束相邻的 View 的位置,就算这个 View 我们在 Java 代码中没有用到,但是在约束布局中还是要定义。这样带来的弊端有几个: | * 大家如果有仔细观察的话,会发现 AndroidProject 其实没有用到 ConstraintLayout 布局,在这里谈谈我对这个布局的看法,约束布局有一个优点,没有布局嵌套,所以能减少测量次数,从而提升布局绘制的速度,但是优点也是它的缺点,正是因为没有布局嵌套,View 也就没有层级概念,所以它需要定义很多 ViewID 来约束相邻的 View 的位置,就算这个 View 我们在 Java 代码中没有用到,但是在约束布局中还是要定义。这样带来的弊端有几个: | ||||
* AndroidProject 其实一直有这样做,把很多组件都拆成了独立的框架,例如:权限请求框架 [XXPermissions](https://github.com/getActivity/XXPermissions),网络请求框架 [EasyHttp](https://github.com/getActivity/EasyHttp)、吐司框架 [ToastUtils](https://github.com/getActivity/ToastUtils) 等等,我都是将它抽离在 AndroidProject 之外,作为一个单独的开源项目进行开发和维护,至于说为什么还有一些代码没有抽取出来,主要原因有几点: | * AndroidProject 其实一直有这样做,把很多组件都拆成了独立的框架,例如:权限请求框架 [XXPermissions](https://github.com/getActivity/XXPermissions),网络请求框架 [EasyHttp](https://github.com/getActivity/EasyHttp)、吐司框架 [ToastUtils](https://github.com/getActivity/ToastUtils) 等等,我都是将它抽离在 AndroidProject 之外,作为一个单独的开源项目进行开发和维护,至于说为什么还有一些代码没有抽取出来,主要原因有几点: | ||||
1. 和业务的耦合性高,例如 Dialog 组件引用了很多基类(BaseDialog、BaseAdapter) | |||||
1. 和业务的耦合性高,例如 Dialog 组件引用了很多项目的基类,例如 **BaseDialog**、**BaseAdapter** 等 | |||||
2. 业务有定制化需求,因为 Dialog 的 UI 风格要跟随项目的设计走,所以你懂的 | |||||
2. 业务有定制化需求,因为 Dialog 的 UI 风格要跟随项目的设计走,所以代码如果在项目中,修改起来会非常方便,如果抽取到框架中,要怎么修改和统一 UI 风格呢?我个人认为框架不适合做 UI 定制化,因为每个产品的设计风格都不一样,就算开放再多的 API 给外部调用的人设置 UI 风格,也无法满足所有人的需求。 | |||||
* 基于以上几点,我并不认为所有的东西都适合抽取成框架给大家用,有些东西还是跟随 **AndroidProject** 一起更新比较好。当然像权限请求这种东西,我个人觉得抽成框架是比较合适的,因为它和业务的关联性不大,更重要的是,如果某一天你觉得 **XXPermissions** 做得不够好,你随时可以在 **AndroidProject** 替换掉它,并且整个过程不需要太大的改动。 | * 基于以上几点,我并不认为所有的东西都适合抽取成框架给大家用,有些东西还是跟随 **AndroidProject** 一起更新比较好。当然像权限请求这种东西,我个人觉得抽成框架是比较合适的,因为它和业务的关联性不大,更重要的是,如果某一天你觉得 **XXPermissions** 做得不够好,你随时可以在 **AndroidProject** 替换掉它,并且整个过程不需要太大的改动。 | ||||
* EventBus 我之前其实有加入过一版,只不过在 [v10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 版本上面移除了,原因很简单,它不是一个项目的必需品,我们用 EventBus 的初衷应该是,当需求在现有的基础上实现起来比较困难或者麻烦时,我们可以考虑用一用,但是到了实际项目中,会出现很多滥用的情况出现,在这里我建议大家,能用正常方式实现通讯的,尽量不要用 EventBus 实现。另外大家如果真的有需要,可以自行加入,集成也相对比较简单。 | * EventBus 我之前其实有加入过一版,只不过在 [v10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 版本上面移除了,原因很简单,它不是一个项目的必需品,我们用 EventBus 的初衷应该是,当需求在现有的基础上实现起来比较困难或者麻烦时,我们可以考虑用一用,但是到了实际项目中,会出现很多滥用的情况出现,在这里我建议大家,能用正常方式实现通讯的,尽量不要用 EventBus 实现。另外大家如果真的有需要,可以自行加入,集成也相对比较简单。 | ||||
#### 为什么不用 Retrofit 和 RxJava | |||||
#### 为什么没有用 Retrofit 和 RxJava | |||||
* 我想问大家一个问题,这两个框架搭配起来好用吗?可能大家的回答都不一致,但是我个人觉得不好用,接下来让我们分析一下 Retrofit 有什么问题: | * 我想问大家一个问题,这两个框架搭配起来好用吗?可能大家的回答都不一致,但是我个人觉得不好用,接下来让我们分析一下 Retrofit 有什么问题: | ||||
* 原生的 RecyclerView.Adapter 本身就支持多 type,只需要重写适配器的 getItemType 方法即可,具体用法不做过多介绍。 | * 原生的 RecyclerView.Adapter 本身就支持多 type,只需要重写适配器的 getItemType 方法即可,具体用法不做过多介绍。 | ||||
#### 为什么不用 Dagger 框架 | |||||
#### 为什么没有用 Dagger 框架 | |||||
* 框架的学习和使用成本极高,但总体收益不高,不适用于大部分人,所以不会考虑加入。 | * 框架的学习和使用成本极高,但总体收益不高,不适用于大部分人,所以不会考虑加入。 | ||||
* 谈谈我对新技术的看法,首先我会思考这种新技术能解决什么痛点,这点非常重要,再好的技术创新,也必须得创造价值,否则就是在扯淡。有人肯定会问,什么样的技术才算有价值?对于我们 Android 程序员来讲,无非就围绕两点,开发和维护。要么在前期开发上,能发挥很大的作用,要么在后续维护上面,能体现它的优势。 | * 谈谈我对新技术的看法,首先我会思考这种新技术能解决什么痛点,这点非常重要,再好的技术创新,也必须得创造价值,否则就是在扯淡。有人肯定会问,什么样的技术才算有价值?对于我们 Android 程序员来讲,无非就围绕两点,开发和维护。要么在前期开发上,能发挥很大的作用,要么在后续维护上面,能体现它的优势。 | ||||
* 还有谷歌的新技术不一定都是好的,也有一些是 KPI 的产物,别忘了,他们也是程序员,他们也有 KPI 考核,为了年终奖和晋升,他们不得不卖力宣传,纵使他们知道这个东西有硬伤,但是他们也会推出来看看市场反应。所以我们看待一种新技术,不要太看重是否是大公司出品的,也不要太看重是哪个行业名人写的,我们应该要重点关注的是,产品的质量以及能带给我们带来哪些帮助,这个才是正确的技术价值观。 | |||||
* 还有谷歌的新技术不一定都是好的,也有一些是 **KPI 产物**,别忘了,他们也是打工的,他们也有 **KPI 考核**,为了年终奖和晋升,他们不得不卖力宣传,纵使他们知道这个东西有硬伤,但是他们也会推出来看看市场反应。所以我们看待一种新技术,不要太看重是否是大公司出品的,也不要太看重是哪个行业名人写的,我们应该要重点关注的是,产品的质量以及能带给我们带来哪些帮助,还有会带来哪些不好的影响,这个才是正确的技术价值观。 |
implementation 'com.scwang.smart:refresh-layout-kernel:2.0.3' | implementation 'com.scwang.smart:refresh-layout-kernel:2.0.3' | ||||
implementation 'com.scwang.smart:refresh-header-material:2.0.3' | implementation 'com.scwang.smart:refresh-header-material:2.0.3' | ||||
// 侧滑框架:https://github.com/luckybilly/SmartSwipe | |||||
implementation 'com.billy.android:smart-swipe:1.1.2' | |||||
implementation 'com.billy.android:smart-swipe-x:1.1.0' | |||||
// 日志打印框架:https://github.com/JakeWharton/timber | // 日志打印框架:https://github.com/JakeWharton/timber | ||||
implementation 'com.jakewharton.timber:timber:4.7.1' | implementation 'com.jakewharton.timber:timber:4.7.1' | ||||
xmlns:tools="http://schemas.android.com/tools" | xmlns:tools="http://schemas.android.com/tools" | ||||
package="com.hjq.demo"> | package="com.hjq.demo"> | ||||
<!-- 联网权限 --> | |||||
<uses-permission android:name="android.permission.INTERNET" /> | |||||
<!-- 网络状态 --> | |||||
<!-- 网络相关 --> | |||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> | <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> | ||||
<uses-permission android:name="android.permission.INTERNET" /> | |||||
<!-- 外部存储 --> | <!-- 外部存储 --> | ||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
</application> | </application> | ||||
<!-- Android 11 软件包可见性适配:https://www.jianshu.com/p/d1ccd425c4ce --> | |||||
<queries> | |||||
<!-- 拍照意图:MediaStore.ACTION_IMAGE_CAPTURE --> | |||||
<intent> | |||||
<action android:name="android.media.action.IMAGE_CAPTURE" /> | |||||
</intent> | |||||
<!-- 拍摄意图:MediaStore.ACTION_VIDEO_CAPTURE --> | |||||
<intent> | |||||
<action android:name="android.media.action.VIDEO_CAPTURE" /> | |||||
</intent> | |||||
<!-- 图片裁剪意图 --> | |||||
<intent> | |||||
<action android:name="com.android.camera.action.CROP" /> | |||||
</intent> | |||||
<!-- 打电话意图:Intent.ACTION_DIAL --> | |||||
<intent> | |||||
<action android:name="android.intent.action.DIAL" /> | |||||
</intent> | |||||
<!-- 分享意图:Intent.ACTION_SEND --> | |||||
<intent> | |||||
<action android:name="android.intent.action.SEND" /> | |||||
</intent> | |||||
<!-- 调起其他页面意图:Intent.ACTION_VIEW --> | |||||
<intent> | |||||
<action android:name="android.intent.action.VIEW" /> | |||||
</intent> | |||||
</queries> | |||||
</manifest> | </manifest> |
*/ | */ | ||||
default void showComplete() { | default void showComplete() { | ||||
StatusLayout layout = getStatusLayout(); | StatusLayout layout = getStatusLayout(); | ||||
if (layout != null && layout.isShow()) { | |||||
layout.hide(); | |||||
if (layout == null || !layout.isShow()) { | |||||
return; | |||||
} | } | ||||
layout.hide(); | |||||
} | } | ||||
/** | /** |
package com.hjq.demo.action; | |||||
/** | |||||
* author : Android 轮子哥 | |||||
* github : https://github.com/getActivity/AndroidProject | |||||
* time : 2019/12/08 | |||||
* desc : 界面侧滑意图 | |||||
*/ | |||||
public interface SwipeAction { | |||||
/** | |||||
* 是否使用界面侧滑 | |||||
*/ | |||||
default boolean isSwipeEnable() { | |||||
// 默认开启 | |||||
return true; | |||||
} | |||||
} |
import com.hjq.base.BaseActivity; | import com.hjq.base.BaseActivity; | ||||
import com.hjq.base.BaseDialog; | import com.hjq.base.BaseDialog; | ||||
import com.hjq.demo.R; | import com.hjq.demo.R; | ||||
import com.hjq.demo.action.SwipeAction; | |||||
import com.hjq.demo.action.TitleBarAction; | import com.hjq.demo.action.TitleBarAction; | ||||
import com.hjq.demo.action.ToastAction; | import com.hjq.demo.action.ToastAction; | ||||
import com.hjq.demo.http.model.HttpData; | import com.hjq.demo.http.model.HttpData; | ||||
* desc : 业务 Activity 基类 | * desc : 业务 Activity 基类 | ||||
*/ | */ | ||||
public abstract class AppActivity extends BaseActivity | public abstract class AppActivity extends BaseActivity | ||||
implements ToastAction, TitleBarAction, | |||||
SwipeAction, OnHttpListener<Object> { | |||||
implements ToastAction, TitleBarAction, OnHttpListener<Object> { | |||||
/** 标题栏对象 */ | /** 标题栏对象 */ | ||||
private TitleBar mTitleBar; | private TitleBar mTitleBar; |
import androidx.lifecycle.Lifecycle; | import androidx.lifecycle.Lifecycle; | ||||
import androidx.lifecycle.LifecycleOwner; | import androidx.lifecycle.LifecycleOwner; | ||||
import com.billy.android.swipe.SmartSwipeBack; | |||||
import com.hjq.bar.TitleBar; | import com.hjq.bar.TitleBar; | ||||
import com.hjq.bar.initializer.LightBarInitializer; | import com.hjq.bar.initializer.LightBarInitializer; | ||||
import com.hjq.demo.R; | import com.hjq.demo.R; | ||||
import com.hjq.demo.action.SwipeAction; | |||||
import com.hjq.demo.aop.DebugLog; | import com.hjq.demo.aop.DebugLog; | ||||
import com.hjq.demo.http.glide.GlideApp; | import com.hjq.demo.http.glide.GlideApp; | ||||
import com.hjq.demo.http.model.RequestHandler; | import com.hjq.demo.http.model.RequestHandler; | ||||
* 初始化一些第三方框架 | * 初始化一些第三方框架 | ||||
*/ | */ | ||||
public static void initSdk(Application application) { | public static void initSdk(Application application) { | ||||
// 设置权限请求调试模式 | |||||
// 设置调试模式 | |||||
XXPermissions.setDebugMode(AppConfig.isDebug()); | XXPermissions.setDebugMode(AppConfig.isDebug()); | ||||
// 初始化吐司 | // 初始化吐司 | ||||
// 启用配置 | // 启用配置 | ||||
.into(); | .into(); | ||||
// Activity 侧滑返回 | |||||
SmartSwipeBack.activitySlidingBack(application, activity -> { | |||||
if (activity instanceof SwipeAction) { | |||||
return ((SwipeAction) activity).isSwipeEnable(); | |||||
} | |||||
return true; | |||||
}); | |||||
// 初始化日志打印 | // 初始化日志打印 | ||||
if (AppConfig.isLogEnable()) { | if (AppConfig.isLogEnable()) { | ||||
Timber.plant(new DebugLoggerTree()); | Timber.plant(new DebugLoggerTree()); |
*/ | */ | ||||
@Override | @Override | ||||
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { | |||||
public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, @NonNull Lifecycle.Event event) { | |||||
if (event != Lifecycle.Event.ON_DESTROY) { | if (event != Lifecycle.Event.ON_DESTROY) { | ||||
return; | return; | ||||
} | } | ||||
source.getLifecycle().removeObserver(this); | |||||
DIALOG_MANAGER.remove(lifecycleOwner); | |||||
lifecycleOwner.getLifecycle().removeObserver(this); | |||||
clearShow(); | clearShow(); | ||||
} | } | ||||
} | } |
/** | /** | ||||
* 输入发生了变化 | * 输入发生了变化 | ||||
* | |||||
* @return 返回按钮的 Enabled 状态 | * @return 返回按钮的 Enabled 状态 | ||||
*/ | */ | ||||
boolean onInputChange(InputTextManager helper); | |||||
boolean onInputChange(InputTextManager manager); | |||||
} | } | ||||
} | } |
} | } | ||||
if (XXPermissions.isGrantedPermission(this, new String[]{Permission.MANAGE_EXTERNAL_STORAGE, Permission.CAMERA}) | if (XXPermissions.isGrantedPermission(this, new String[]{Permission.MANAGE_EXTERNAL_STORAGE, Permission.CAMERA}) | ||||
&& intent.resolveActivity(getPackageManager()) != null) { | && intent.resolveActivity(getPackageManager()) != null) { | ||||
File file = getSerializable(IntentKey.FILE); | File file = getSerializable(IntentKey.FILE); | ||||
if (file == null) { | if (file == null) { | ||||
toast(R.string.camera_image_error); | toast(R.string.camera_image_error); |
} | } | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
@Override | @Override | ||||
public void onBackPressed() { | public void onBackPressed() { | ||||
// 按返回键重启应用 | // 按返回键重启应用 |
mIndicatorView.setViewPager(mViewPager); | mIndicatorView.setViewPager(mViewPager); | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
@SingleClick | @SingleClick | ||||
@Override | @Override | ||||
public void onClick(View view) { | public void onClick(View view) { |
mViewPager.setAdapter(null); | mViewPager.setAdapter(null); | ||||
mBottomNavigationView.setOnNavigationItemSelectedListener(null); | mBottomNavigationView.setOnNavigationItemSelectedListener(null); | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
} | } |
return false; | return false; | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
/** | /** | ||||
* {@link ViewPager.OnPageChangeListener} | * {@link ViewPager.OnPageChangeListener} | ||||
*/ | */ |
} | } | ||||
return false; | return false; | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
} | } |
return false; | return false; | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
/** | /** | ||||
* 注册监听 | * 注册监听 | ||||
*/ | */ |
//super.onBackPressed(); | //super.onBackPressed(); | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
@Override | @Override | ||||
protected void initActivity() { | protected void initActivity() { | ||||
// 问题及方案:https://www.cnblogs.com/net168/p/5722752.html | // 问题及方案:https://www.cnblogs.com/net168/p/5722752.html |
import com.gyf.immersionbar.BarHide; | import com.gyf.immersionbar.BarHide; | ||||
import com.gyf.immersionbar.ImmersionBar; | import com.gyf.immersionbar.ImmersionBar; | ||||
import com.hjq.demo.R; | import com.hjq.demo.R; | ||||
import com.hjq.demo.action.SwipeAction; | |||||
import com.hjq.demo.app.AppActivity; | import com.hjq.demo.app.AppActivity; | ||||
import com.hjq.demo.other.IntentKey; | import com.hjq.demo.other.IntentKey; | ||||
import com.hjq.demo.widget.PlayerView; | import com.hjq.demo.widget.PlayerView; | ||||
* desc : 视频播放界面 | * desc : 视频播放界面 | ||||
*/ | */ | ||||
public final class VideoPlayActivity extends AppActivity | public final class VideoPlayActivity extends AppActivity | ||||
implements SwipeAction, PlayerView.onPlayListener { | |||||
implements PlayerView.onPlayListener { | |||||
private PlayerView mPlayerView; | private PlayerView mPlayerView; | ||||
private VideoPlayActivity.Builder mBuilder; | private VideoPlayActivity.Builder mBuilder; | ||||
.hideBar(BarHide.FLAG_HIDE_BAR); | .hideBar(BarHide.FLAG_HIDE_BAR); | ||||
} | } | ||||
@Override | |||||
public boolean isSwipeEnable() { | |||||
return false; | |||||
} | |||||
/** | /** | ||||
* 播放参数构建 | * 播放参数构建 | ||||
*/ | */ |
String channelId = ""; | String channelId = ""; | ||||
// 适配 Android 8.0 通知渠道新特性 | // 适配 Android 8.0 通知渠道新特性 | ||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { | ||||
NotificationChannel channel = new NotificationChannel(getString(R.string.update_notification_channel_id), getString(R.string.update_notification_channel_name), NotificationManager.IMPORTANCE_HIGH); | |||||
NotificationChannel channel = new NotificationChannel(getString(R.string.update_notification_channel_id), getString(R.string.update_notification_channel_name), NotificationManager.IMPORTANCE_LOW); | |||||
channel.enableLights(false); | channel.enableLights(false); | ||||
channel.enableVibration(false); | channel.enableVibration(false); | ||||
channel.setVibrationPattern(new long[]{0}); | channel.setVibrationPattern(new long[]{0}); | ||||
} | } | ||||
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext(), channelId) | NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext(), channelId) | ||||
// 设置通知时间 | |||||
.setWhen(System.currentTimeMillis()) | .setWhen(System.currentTimeMillis()) | ||||
// 设置通知标题 | // 设置通知标题 | ||||
.setContentTitle(getString(R.string.app_name)) | .setContentTitle(getString(R.string.app_name)) | ||||
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.launcher_ic)) | .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.launcher_ic)) | ||||
// 设置通知静音 | // 设置通知静音 | ||||
.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) | .setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) | ||||
// 设置震动频率 | |||||
.setVibrate(new long[]{0}) | .setVibrate(new long[]{0}) | ||||
// 设置声音文件 | |||||
.setSound(null) | .setSound(null) | ||||
// 设置通知的优先级 | // 设置通知的优先级 | ||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT); | .setPriority(NotificationCompat.PRIORITY_DEFAULT); | ||||
@Override | @Override | ||||
public void onProgress(File file, int progress) { | public void onProgress(File file, int progress) { | ||||
mUpdateView.setText(String.format(getString(R.string.update_status_running), progress)); | |||||
mProgressView.setProgress(progress); | |||||
// 更新下载通知 | // 更新下载通知 | ||||
notificationManager.notify(notificationId, notificationBuilder | notificationManager.notify(notificationId, notificationBuilder | ||||
// 设置通知的文本 | // 设置通知的文本 | ||||
.setProgress(100, progress, false) | .setProgress(100, progress, false) | ||||
// 设置点击通知后是否自动消失 | // 设置点击通知后是否自动消失 | ||||
.setAutoCancel(false) | .setAutoCancel(false) | ||||
// 是否正在交互中 | |||||
.setOngoing(true) | |||||
// 重新创建新的通知对象 | // 重新创建新的通知对象 | ||||
.build()); | .build()); | ||||
mUpdateView.setText(String.format(getString(R.string.update_status_running), progress)); | |||||
mProgressView.setProgress(progress); | |||||
} | } | ||||
@Override | @Override | ||||
.setContentIntent(PendingIntent.getActivity(getContext(), 1, getInstallIntent(), Intent.FILL_IN_ACTION)) | .setContentIntent(PendingIntent.getActivity(getContext(), 1, getInstallIntent(), Intent.FILL_IN_ACTION)) | ||||
// 设置点击通知后是否自动消失 | // 设置点击通知后是否自动消失 | ||||
.setAutoCancel(true) | .setAutoCancel(true) | ||||
// 是否正在交互中 | |||||
.setOngoing(false) | |||||
.build()); | .build()); | ||||
mUpdateView.setText(R.string.update_status_successful); | mUpdateView.setText(R.string.update_status_successful); | ||||
// 标记成下载完成 | // 标记成下载完成 |
initLayout(); | initLayout(); | ||||
} | } | ||||
if (!isShow()) { | |||||
// 显示布局 | |||||
mMainLayout.setVisibility(VISIBLE); | |||||
if (isShow()) { | |||||
return; | |||||
} | } | ||||
// 显示布局 | |||||
mMainLayout.setVisibility(VISIBLE); | |||||
} | } | ||||
/** | /** | ||||
* 隐藏 | * 隐藏 | ||||
*/ | */ | ||||
public void hide() { | public void hide() { | ||||
if (mMainLayout != null && isShow()) { | |||||
//隐藏布局 | |||||
mMainLayout.setVisibility(INVISIBLE); | |||||
if (mMainLayout == null || !isShow()) { | |||||
return; | |||||
} | } | ||||
//隐藏布局 | |||||
mMainLayout.setVisibility(INVISIBLE); | |||||
} | } | ||||
/** | /** | ||||
} | } | ||||
public void setIcon(Drawable drawable) { | public void setIcon(Drawable drawable) { | ||||
if (mLottieView != null) { | |||||
// 这里需要先将 Lottie 动画禁用掉 | |||||
mLottieView.setAnimation(0); | |||||
if (mLottieView.isAnimating()) { | |||||
mLottieView.cancelAnimation(); | |||||
} | |||||
mLottieView.setImageDrawable(drawable); | |||||
if (mLottieView == null) { | |||||
return; | |||||
} | |||||
if (mLottieView.isAnimating()) { | |||||
mLottieView.cancelAnimation(); | |||||
} | } | ||||
mLottieView.setImageDrawable(drawable); | |||||
} | } | ||||
/** | /** | ||||
* 设置提示动画 | * 设置提示动画 | ||||
*/ | */ | ||||
public void setAnimResource(@RawRes int id) { | public void setAnimResource(@RawRes int id) { | ||||
if (mLottieView != null) { | |||||
mLottieView.setAnimation(id); | |||||
// 这里需要调用播放动画,否则会出现第一次显示动画效果正常,第二次显示动画会不动 | |||||
if (mLottieView == null) { | |||||
return; | |||||
} | |||||
mLottieView.setAnimation(id); | |||||
if (!mLottieView.isAnimating()) { | |||||
mLottieView.playAnimation(); | mLottieView.playAnimation(); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
public void setHint(CharSequence text) { | public void setHint(CharSequence text) { | ||||
if (mTextView != null && text != null) { | |||||
mTextView.setText(text); | |||||
if (mTextView == null) { | |||||
return; | |||||
} | |||||
if (text == null) { | |||||
text = ""; | |||||
} | } | ||||
mTextView.setText(text); | |||||
} | } | ||||
/** | /** |
<string name="common_network_error">当前网络不可用,请检查网络设置</string> | <string name="common_network_error">当前网络不可用,请检查网络设置</string> | ||||
<string name="common_phone_input_hint">输入手机号码</string> | |||||
<string name="common_phone_input_hint">请输入手机号码</string> | |||||
<string name="common_phone_input_error">手机号输入不正确</string> | <string name="common_phone_input_error">手机号输入不正确</string> | ||||
<string name="common_password_input_error">请输入密码</string> | <string name="common_password_input_error">请输入密码</string> | ||||
<string name="common_password_input_unlike">两次密码输入不一致,请重新输入</string> | <string name="common_password_input_unlike">两次密码输入不一致,请重新输入</string> | ||||
<string name="common_code_input_hint">输入验证码</string> | |||||
<string name="common_code_input_hint">请输入验证码</string> | |||||
<string name="common_code_send">发送验证码</string> | <string name="common_code_send">发送验证码</string> | ||||
<string name="common_code_send_hint">验证码已发送,请注意查收</string> | <string name="common_code_send_hint">验证码已发送,请注意查收</string> | ||||
<string name="common_code_error_hint">验证码错误,请检查输入</string> | <string name="common_code_error_hint">验证码错误,请检查输入</string> | ||||
<string name="image_select_max_hint">本次最多只能选择 %d 张图片</string> | <string name="image_select_max_hint">本次最多只能选择 %d 张图片</string> | ||||
<string name="image_select_error">无法选中,该图片已经被移除</string> | |||||
<string name="image_select_error">无法选中,该图片已经被删除</string> | |||||
<!-- 视频选择 --> | <!-- 视频选择 --> | ||||
<string name="video_select_title">视频选择</string> | <string name="video_select_title">视频选择</string> | ||||
<string name="video_select_max_hint">本次最多只能选择 %d 个视频</string> | <string name="video_select_max_hint">本次最多只能选择 %d 个视频</string> | ||||
<string name="video_select_error">无法选中,该视频已经被移除</string> | |||||
<string name="video_select_error">无法选中,该视频已经被删除</string> | |||||
<!-- 拍照 --> | <!-- 拍照 --> | ||||
<string name="camera_launch_fail">无法启动相机</string> | <string name="camera_launch_fail">无法启动相机</string> |
// AndroidProject 版本:v12.0 | |||||
// 发布日期:2021 年 02 月 22 日 | |||||
// AndroidProject 版本:v12.1 | |||||
// 发布日期:2021 年 02 月 27 日 | |||||
buildscript { | buildscript { | ||||
repositories { | repositories { |
import android.graphics.drawable.ColorDrawable; | import android.graphics.drawable.ColorDrawable; | ||||
import android.graphics.drawable.Drawable; | import android.graphics.drawable.Drawable; | ||||
import android.graphics.drawable.StateListDrawable; | import android.graphics.drawable.StateListDrawable; | ||||
import android.text.TextUtils; | |||||
import android.util.AttributeSet; | import android.util.AttributeSet; | ||||
import android.util.TypedValue; | import android.util.TypedValue; | ||||
import android.view.Gravity; | import android.view.Gravity; | ||||
mLeftView.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); | mLeftView.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); | ||||
mRightView.setGravity(Gravity.END | Gravity.CENTER_VERTICAL); | mRightView.setGravity(Gravity.END | Gravity.CENTER_VERTICAL); | ||||
mLeftView.setSingleLine(true); | |||||
mRightView.setSingleLine(true); | |||||
mLeftView.setEllipsize(TextUtils.TruncateAt.END); | |||||
mRightView.setEllipsize(TextUtils.TruncateAt.END); | |||||
mLeftView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mLeftView.getLineSpacingMultiplier()); | mLeftView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mLeftView.getLineSpacingMultiplier()); | ||||
mRightView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mRightView.getLineSpacingMultiplier()); | mRightView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mRightView.getLineSpacingMultiplier()); | ||||