diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d342ae5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.gradle +.idea +.cxx +.externalNativeBuild +build +captures + +._* +*.iml +.DS_Store +local.properties \ No newline at end of file diff --git a/HelpDoc.md b/HelpDoc.md new file mode 100644 index 0000000..f6d9b40 --- /dev/null +++ b/HelpDoc.md @@ -0,0 +1,398 @@ +#### 目录 + +* [为什么没有用 MVP](#为什么没有用-mvp) + +* [为什么没有用 ButterKnife](#为什么没有用-butterknife) + +* [为什么没有用 ViewBinding](#为什么没有用-viewbinding) + +* [为什么没有用 DataBinding](#为什么没有用-databinding) + +* [为什么没有用组件化](#为什么没有用组件化) + +* [为什么没有集成界面侧滑功能](#为什么没有集成界面侧滑功能) + +* [为什么没有用今日头条的适配方案](#为什么没有用今日头条的适配方案) + +* [字体大小为什么不用 dp 而用 sp](#字体大小为什么不用-dp-而用-sp) + +* [为什么没有用 DialogFragment 来防止内存泄漏](#为什么没有用-dialogfragment-来防止内存泄漏) + +* [为什么没有用腾讯 X5 WebView](#为什么没有用腾讯-x5-webview) + +* [为什么没有用单 Activity 多 Fragment](#为什么没有用单-activity-多-fragment) + +* [为什么没有用 ConstraintLayout 来写布局](#为什么没有用-constraintlayout-来写布局) + +* [为什么不拆成多个框架来做这件事](#为什么不拆成多个框架来做这件事) + +* [为什么最低兼容到 Android 5](#为什么最低兼容到-android-5) + +* [为什么不加入扫描二维码功能](#为什么不加入扫描二维码功能) + +* [为什么不加入 EventBus](#为什么不加入-eventbus) + +* [为什么没有用 Retrofit 和 RxJava](#为什么没有用-retrofit-和-rxjava) + +* [为什么没有用 Jetpack 全家桶](#为什么没有用-jetpack-全家桶) + +* [为什么不对图片加载框架进行再次封装](#为什么不对图片加载框架进行再次封装) + +* [模板 架构 技术中台有什么区别](#模板-架构-技术中台有什么区别) + +* [为什么不按业务来划分包名](#为什么不按业务来划分包名) + +* [为什么没有关于列表多 type 的封装](#为什么没有关于列表多-type-的封装) + +* [这不就是一个模板工程换成我也能写一个](#这不就是一个模板工程换成我也能写一个) + +* [假设 AndroidProject 更新了该怎么升级它](#假设-androidproject-更新了该怎么升级它) + +* [为什么不用谷歌 ActivityResultContracts](#为什么不用谷歌-activityresultcontracts) + +* [轮子哥你怎么看待层出不穷的新技术](#轮子哥你怎么看待层出不穷的新技术) + +#### 为什么没有用 MVP + +![](picture/help/mvp1.jpg) + +![](picture/help/mvp2.jpg) + +![](picture/help/mvp3.jpg) + +* AndroidProject 舍弃 MVP 的最大一个原因,需要写各种类,各种回调,如果这个页面比较简单的话,使用 MVP 会让原本简单的代码变复杂,导致后续开发和维护成本是非常高,前期付出的代价和后期的维护不成正比关系,当然这种说法只适用于各种中小型项目,大型的项目我还没有经历过,不过我觉得,无论是 MVC、MVP、MVVM,它们出现的目的是为了解决代码多并且乱的问题,作用就是给代码做分类,但是可以跟大家分享我的心得,我并不看好 MVP,因为它让我开发和维护都很痛苦,所以我就直接将它从 AndroidProject 移除,目的也很简单,不推荐大家使用,因为 MVP 不适合大多数项目的开发和维护。我更推荐大家直接将代码写在 Activity,但是有一个前提条件需要大家遵守,大家要做好代码封装和重复代码的抽取,尽量让 Activity 成为只有业务代码的类,这样一个项目里面的大多数 Activity 代码量都能很好控制在 1000 行代码以内。但是这种看似简单的操作,但是实际要做到是一件不容易的事情,这里面不仅要解决代码带来的问题,还要解决带来的各种人性矛盾,困难重重,这种想法经过很长一段时间的思考,虽然写法在开发和维护中效率是非常高的,但是不被大多数人认可,大家更愿意相信 MVC、MVP、MVVM,而很少有人理解这三种模式的本质是什么,就是为了给代码做分类,但这三种模式都不够灵活,很生硬,像是一套套规则,而这样的代码分类,只会让大多数人的开发越来越头疼。 + +#### 为什么没有用 ButterKnife + +* 随着 AndroidProject 的不断优化,ButterKnife 功能很强大,但是实际开发中,大多数人只用到了 BindView 和 OnClick 注解,在 OnClick 注解在我的项目中发现一个 Bug,就是有时候不会响应点击事件,这个问题并不是必现的。还有 BindView 注解只是在视觉上面将 View 和 ID 的关系更明显了,它其实不能为我们简化代码,因为使用 BindView 和 findViewById 的代码量是一样的。 + +* ButterKnife 最大的缺点是还会自动生成 ViewBinding 类,就算在类中只使用了一个 BindView,它也会生成这个类,其实这样是不太好的。 + +* 另外一个点,将 Android Studio 升级到 4.1 之后,会出现以下提示,这个是因为 Gradle 在 5.0 之后的版本,View ID 将不会以常量的形式存在,所以不能将其定义在 `BindView` 注解或者在 `switch case` 块中。 + +```text +Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them as annotation +Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them in switch case statements +``` + +* 考虑到这些情况,我在新版的 AndroidProject 上面移除了 ButterKnife 框架,其实 findViewById 一直挺好,只是我们没有认真思考过而已。 + +* 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) + +#### 为什么没有用 ViewBinding + +* 首先 ViewBinding 和 ButterKnife 有一个相同的毛病,就是自动生成一个类,然后在这个类里面进行 findViewById,但是有一个致命的缺点,每个 `Activity / Fragment / Dialog / Adapter` 都需要先初始化 ViewBinding 对象,因为每次生成的类名都是不固定的,所以无法在基类中封装处理,并且每次都要写 `binding.xxx` 才能操作控件。 + +```java +ActivityXxxxBinding.inflate binding = ActivityXxxxBinding.inflate(getLayoutInflater()); +binding.tv_data_name.setText("字符串"); +``` + +* 另外一个它会根据控件 id 作为属性的名称,这样会导致一个代码不规范的问题,如果在 xml 中控件的 id 命名符合规范了,会导致在 Java 代码中的命名不规范,如果在 Java 代码中的命名规范了,又会导致 xml 的 id 不符合规范了。而代码规范关系到后续的代码维护,是一个很重要的问题,不容忽视。 + +* 虽然 ViewBinding 是谷歌官方推荐的,但是我觉得并不完美,解决了 findViewById 的同时又带来了其他的问题,在关键问题上有问题和矛盾,直白点说这些问题都是硬伤。 + +* 正如我上面所说的,findViewById 一直挺好,只是我们没有认真思考过而已。 + +* 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) + +#### 为什么没有用 DataBinding + +* DataBinding 最大的优势在于,因为它可以在 xml 直接给 View 赋值,但它的优点正是它最致命的缺点,当业务逻辑简单时,会显得格外美好,但是一旦判断条件复杂起来,由于 xml 属性不能换行的特性,会导致无法在 xml 直接赋值又或者很长的一段代码堆在布局中,间接导致 CodeReview 时异常艰难,更别说在原有的基础上继续更新迭代,这对每一个开发者来讲无疑是一个巨大的灾难。 + +* 还有一个是 DataBinding 的学习成本比较高,其次成本也挺高,使用前需要做很多封装,另外每次使用时都需要添加 `layout` 和 `data` 节点,然后在 Java 代码中初始化 DataBinding 对象,无法在基类中封装处理,每次都要写 `binding.xxx` 才能操作控件,和 ViewBinding 的问题差不多。 + +```java +ActivityXxxxBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_xxxx); +``` + +#### 为什么没有用组件化 + +* 先来说说组件化的优点,能够把不同的业务代码进行隔离,达到完全解耦的效果,同时也能提升编译和打包速度。但是这两个优点只有项目业务变得大并且复杂的情况下才能产生价值,否则价值并不大,在我看来,代码解耦其实是把双刃剑,解耦的过程相对比较麻烦,这会直接加大前期开发的工作量,并且一些解耦的方式可能会导致代码重复,例如 AndroidManifest 清单文件,需要同时配置两份文件,后期改动也需要改两遍,另外一个是路由跳转,现在大多数路由框架都是通过 APT 生成一张映射表,这个需要我们每写一个 Activity 都要写一个路径的注解,这个不仅写起来麻烦,管理起来也会很麻烦,另外对每个业务模块的 SDK 初始化操作和数据存储交互上又该如何处理和解耦?这些都是组件化所存在的问题,矛和盾又该如何抉择? + +* 组件化其实是一个很好的思想,但是它并不适用于中小型项目,因为这些项目并没有那么复杂,大部分业务模块都很小,大的业务模块其实也不多,当然我个人建议可以将大点的模业务进行模块化,但是没有必要做组件化,因为一旦涉及,从组件化中得到弊会大于利。而在一些大型的项目中,大大小小的模块非常多,一次打包编译可能要半个小时甚至更久(请注意大厂的电脑基本都是高配或者顶配),相比较这种情况之下,组件化的优点就已经大于了它的缺点,同时他们也有充足的人力和过硬的技术,并且能长期投入巨大的时间和精力来做这件事。 + +* 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 正在显示,然后使用界面侧滑,那么会出现闪屏的情况,具体效果如下图: + +![](picture/help/Swipe.jpg) + +* 就这个情况我也联系过作者,并详细阐述了产生的原因和具体的复现步骤,但是我等了三天连个回复都没有,实属有些让我心寒,在等待的期间我看到 Github 的 issue 已经基本没有回复了,并且最后一次提交是在 13 个月前了,种种迹象都已经表明,所以经过慎重考虑,最终决定在 [v12.1 版本](https://github.com/getActivity/AndroidProject/releases/tag/12.1) 移除界面侧滑功能。 + +#### 为什么没有用今日头条的适配方案 + +* 关于屏幕适配方案,其实不能说头条的方案就是最好的,其实谷歌已经针对屏幕适配做了处理,就是 dp 和 sp ,而 dp 的计算转换是由屏幕的像素决定,系统只认 px 单位, dp 需要进行转换,比如 1dp 等于几个 px ,这个时候就需要基数进行转换,比如 1dp = 2px,这个基数就是 2。 + +```text +ldpi:1dp=0.75px + +mdpi:1dp=1px + +hdpi:1dp=1.5px + +xhdpi:1dp=2px + +xxhdpi:1dp=3px + +xxxhdpi:1dp=4px +``` + +* 这个是谷歌对屏幕适配的一种默认方式,厂商也可以根据需要去修改默认的基数,从而达到最优的显示效果。 + +* 谷歌的屏幕适配方案也不是百分之一百完美的,其实会存在一些需求不能满足的问题。谷歌的设计理念是屏幕越大显示的东西越多,这种想法并没有错,但有些 App 可能对这块会有要求,希望根据屏幕大小对控件进行百分比压缩。这个时候谷歌那套适配方案的设计已经和需求完全不一致了。 + +* 那什么样的 App 才会有那样的需求呢?现在手机的屏幕大多在 5 - 6寸,而平板大多在 8 - 10 寸,也就是说我们只适配手机的话,只需要针对 5 - 6 寸的,并且它们的分辨率都差不多,其实用谷歌那种方案是最优的,如果我们需要适配平板的话,一般都会要求对控件进行百分比压缩,这个时候谷歌那套方案会把原先在手机显示的控件在平板上面变大一点,这样就会导致屏幕剩余的空间过大,导致控件显示出来的效果比较小,而如果采用百分比对控件压缩的方式,能比较好地控制 App 在不同屏幕下显示的效果。 + +* 另外谈谈我的经历,我自己之前的公司主要是做平板上面的应用,所以也用过 [AutoSize 框架](https://github.com/JessYanCoding/AndroidAutoSize),一年多的使用体验下来,发现这个框架 Bug 还算是比较多的,例如框架会偶尔出现机型适配失效,重写了 **getResources** 方法情况之后出现的情况少了一些,但是仍然还有一些奇奇怪怪的问题,这里就不一一举例了,最后总结下来就是框架还不够成熟,但是框架的思想还是很不错的。我后面换了一家公司,也是做平板应用,项目用的是用[通配符的适配方案](https://github.com/wildma/ScreenAdaptation),跟 AutoSize 相对比,没有了那些奇奇怪怪的问题,但是代码的侵入性比较高。这两种方案各有优缺点,大家看着选择。 + +![](picture/help/vote2.jpg) + +* 在这块我也发起过群投票,相比谷歌的适配方案,大多数人更认同那种百分比适配方案,秉承着少数服从多数的理念,我在 AndroidProject [v13.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/13.0) 加入了通配符的适配方案。虽然有一部分人不认同,但是我想跟这些人说的是:我的每一个决定都是十分谨慎的,因为这其中涉及到许多人的利益,AndroidProject 虽然是我创造的,但是它早就不是我一个人的了,而是大家的,每个重要的决定我都会考虑再三才会去做,在做决定的时候我会把大众的利益放在第一位,把自己的利益放在最后一位,所以大家唯一能做的是,相信我的选择。或许你可能觉得这样不太对,也随时欢迎你提出不同的意见给到我,我不认为自己做的决定一定都是对的,但是我会一直朝着对的方向前进。 + +#### 字体大小为什么不用 dp 而用 sp + +* 首先我们先回顾一下谷歌原生的写法,将控件大小的单位定成了 dp,而字体大小的单位定成了 sp,而无论是 dp 还是 sp 作为单位,最终还是会转成 px 的单位。 + +* 谷歌这样做也有一定目的,dp 是根据屏幕的密度来计算的,而 sp 是根据手机设置的字体大小来计算的,如果用 dp 来代替 sp 会有一个问题,那么就是无论用户在手机里面怎么设置字体大小,应用的字体大小不会产生任何变化。这种场景对年轻人来讲没有太大的影响,而对一些老龄用户,例如我们的爸妈,他们一般会把手机的字体调大,这样才能看清楚里面的字,如果我们采用 dp 来代替 sp 的方案,会对这类用户造成不便,换位思考,我们终有一天也会变老,变得老眼昏花,我们会如何看待这个事情? + +* 显然这种方式是不合理的,也非常地不人性化。网上这种方案可能主要就是为了解决把控件宽高写死之后,在某些字体上显示比较大的机型会出现字显示不全的问题,而这种把控件宽高写死的方式本身也是不合理的,应该在不得已的情况下才把控件的宽高写死,一般情况下我们应当使用自适应的方式,让控件自己测量自己的宽高,特别是在有显示字体的控件下,就更不应该把宽高写死。 + +#### 为什么没有用 DialogFragment 来防止内存泄漏 + +* DialogFragment 的出现就是为了解决 Dialog 和 Activity 生命周期不同步导致的内存泄漏问题,在 AndroidProject 曾经引入过,也经过了很多个版本的更新迭代,不过在 [10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 版本后就被移除了,原因是 Dialog 虽然有坑,但是 DialogFragment 也有坑,可以说解决了一个问题又引发了各种问题。先来细数我在 DialogFragment 上踩过的各种坑: + + 1. DialogFragment 会占用 Dialog 的 Cancel 和 Dismiss 监听,为了就是在 Dialog 消失之后将自己(Fragment)从 Activity 上移除,这样的操作看起很合理,但是会引发一个问题,那么就是会导致我们原先给 Dialog 设置的 Cancel 和 Dismiss 监听被覆盖掉,间接导致我们无法使用这个监听,因为 Dialog 的监听器只能有一个观察者,而 AndroidProject 前期解决这个问题的方式是:将 Dialog 的监听器使用的观察者模式,从一对一改造成一对多,也就是一个被观察者可以有很多个观察者,由此来解决这个问题。 + + 2. DialogFragment 的显示和隐藏操作都不能在后台中进行,否则会出现一个报错 `java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState`,这个是因为 DialogFragment 的 show 和 dismiss 方法使用了 FragmentTransaction.commit 方法,这个 commit 方法会触发对 Activity 状态的检查,如果 Activity 的状态已经保存了(即已经调用了 onSaveInstanceState 方法),这个时候把 Fragment commit 到 Activity 上会抛出异常,这种场景在执行异步操作(例如请求网络)未结束前,用户手动将 App 返回到桌面,然后异步操作执行完毕,下一步就是回调异步监听器,这个时候我们的 App 已经处于后台状态,那么我们如果在监听回调中 show 或 dismiss DialogFragment,那么就会触发这个异常。AndroidProject 前期对于这个问题的解决方案是重写 DialogFragment.show 方法,加一个对 Activity 的状态判断,如果 Activity 处于后台状态,那么不去调用 super.show(),但是这样会导致一个问题,虽然解决了崩溃的问题,但是又会导致 Dialog 没显示出来,而重写 DialogFragment.dismiss 方法,直接调用 dismissAllowingStateLoss 方法,因为这个方法不会去检查 Activity 的状态。虽然这种解决方式不够完美,但却是我那个时候能想到的最好方法。 + + 3. 最后一个问题是关于 DialogFragment 屏幕旋转的问题,首先 DialogFragment 是通过自身 onCreateDialog 方法来获取 Dialog 对象的,但是如果我们直接通过外层给 DialogFragment 传入 Dialog 的对象时,这样的代码逻辑貌似没有问题,但是在用户进行屏幕旋转,而刚好我们的应用没有固定屏幕方向时,DialogFragment 对象会跟随 Activity 销毁重建,因为它本身就是一个 Fragment,但是会导致之前的外层传入 Dialog 对象被回收并置空,然后再调用到 onCreateDialog 方法时,返回的是一个空对象的 Dialog,那么就会直接 DialogFragment 内部引发空指针异常,而 AndroidProject 前期解决这个问题的方案是,重写 onActivityCreated,赶在 onCreateDialog 方法调用之前,先判断 DialogFragment 对象内部持有的 Dialog 是否为空,如果是一个空对象,那么就将自己 dismissAllowingStateLoss 掉。 + +* 看过这些问题,你是不是和我一样,感觉这 DialogFragment 不是一般的坑,不过最终我放弃了使用 DialogFragment,并不是因为 DialogFragment 又出现了新问题,而是我想到了更好的方案来代替 DialogFragment,方案就是 Application.registerActivityLifecycleCallbacks,想必大家现在已经猜到我想干啥,和 DialogFragment 的作用一样,通过监听 Activity 的方式来管控 Dialog 的生命周期,但唯一不同的是,它不会出现刚刚说过 DialogFragment 的那些问题,这种方式在 AndroidProject 上迭代了几个版本过后,这期间没有发现新的问题,也没有收到别人反馈过这块的问题,证明这种方式是可行的。 + +#### 为什么没有用腾讯 X5 WebView + +* 首先我问大家一个问题,腾讯出品的 X5 WebView 就一定比原生 WebView 好吗?我觉得未必,我依稀记得 Android 9.0 还是 Android 10 刚出来的时候,我点了升级按钮,然后就发现微信和 QQ 的网页浏览卡得让我怀疑人生,不过后面突然某一天就变好了,从这件事可以得出两点结论: + + 1. 第一个 SDK 有自我更新功能,意味着 WebView 掌控权握在腾讯公司手中 + + 2. 第二个是 SDK 需要腾讯来持续维护,意味着这个项目的生命周期会跟随腾讯公司的发展和决策 + +* 基于以上两点,我的个人建议是优先使用原生 WebView,如果不满足需求了,可以自行替换成 X5 WebView,当然不是说 X5 WebView 一定不好,用原生 WebView 一定就好,而是 AndroidProject 的目标是稳中求胜,另外一个是 AndroidProject 中有针对 WebView 做统一封装,后续替换成 X5 WebView 的成本还算是相对较低的。 + +#### 为什么没有用单 Activity 多 Fragment + +* 这个问题在前几年是一个比较火热的话题,我表示很能理解,因为新鲜的事物总是能勾起人的好奇,让人忍不住试一试,但是我先问大家一个问题,单 Activity 多 Fragment 和写多个 Activity 有什么优点?大家第一个反应应该是每写一个页面都不需要在清单文件中注册了,但是这个真的是优点吗?我可以很明确地告诉大家,我已经写了那么多句代码,不差那句在清单文件注册的代码。那么究竟什么才是对我们有价值的?我觉得就两点,一是减少前期开发的工作量,二是降低后续维护的难度。所以省那一两句有前途吗?我们是差那一两句代码的人吗?如果这种模式能够帮助我们写好代码,这个当然是有价值的,非常值得一试的,否则就是纯属瞎扯淡。不仅如此,我个人觉得这种模式有很大的弊端,会引发很多问题,例如: + + 1. 有的页面是全屏有的页面是非全屏,有的页面是固定竖屏有的页面是横屏,进入时怎么切换?返回时怎么切换回来?然后又该怎么去做统一的封装? + + 2. 不同 Fragment 之间应该怎样通讯?Activity 有 onActivityResult 方法可以用,但是 Fragment 有什么方法可以用?还是全用 EventBus 来处理?如果是这样做会不会太低效了?每次都要写一个 Event 类,并且在代码中找起来是不是也不太好找? + + 3. 如何保证这个 Activity 被系统回收之后,然后引发重建操作,又该如何保证这个 Activity 中的多个 Fragment 之间的回退栈是否正常?假设这个 Activity 里面有 10 个Fragment,一下子引发 10 个 Fragment 创建是否会对内存和性能造成影响呢? + +* 如果单 Activity 多 Fragment 不能为我们创造太大的价值时,这种模式根本就不值得我们去做,因为我们最终得到的,永远抵不上付出的。 + +#### 为什么没有用 ConstraintLayout 来写布局 + +* 大家如果有仔细观察的话,会发现 AndroidProject 其实没有用到 ConstraintLayout 布局,在这里谈谈我对这个布局的看法,约束布局有一个优点,没有布局嵌套,所以能减少测量次数,从而提升布局绘制的速度,但是优点也是它的缺点,正是因为没有布局嵌套,View 也就没有层级概念,所以它需要定义很多 ViewID 来约束相邻的 View 的位置,就算这个 View 我们在 Java 代码中没有用到,但是在约束布局中还是要定义。这样带来的弊端有几个: + + 1. 我们每次都要想好这个 ViewID 的名称叫什么,这个就有点烧脑筋了,既要符合代码规范,也要明确和突出其作用。 + + 2. 要考虑好每个 View 上下左右之间的约束关系,否则就会容易出现越界的情况,例如一个 TextView 设计图上有 5 个字,但是后台返回了 10 个字,这个时候 TextView 的控件宽度会被拉长,如果没有设置好右边的约束,极有可能出现遮盖右边 View 的情况。 + + 3. View 之间的关系会变得复杂起来,具体表现为布局一旦发生变更,例如删除或增加某一个 View,都会影响整个 ConstraintLayout 布局,因为很多约束关系会因此发生改变,并且在布局预览中就会变得错乱起来,简单通俗点来讲就是,你拆了一块瓦,很可能会导致房倒屋塌。 + + 4. 是我们无法直接在布局中无法直接预判这个 View 在 Java 代码中是否有使用到,因为每个 View 几乎都有定义 ID,要想知道这个 View 有没有用到,还是得在 Java 代码中找一找。 + +* 我的想法是:项目里面大多数页面还是比较简单的,可以结合 LinearLayout 和 FrameLayout 布局来写,并且不需要嵌套得太深,我觉得合理的嵌套是 2~3 层,如果超过 5 层可以考虑用 ConstraintLayout 布局来写,当然这种情况在实际项目中还是比较少的。 + +* 另外一个问题,就是我发现有些人写布局喜欢嵌套很多层,但是真正的情况并不是真的就需要嵌套那么多,而是这个人对这个布局的特性和属性不太熟悉而导致,正确的方式是深入学习,这样才能用好每一个布局。 + +#### 为什么不拆成多个框架来做这件事 + +* AndroidProject 其实一直有这样做,把很多组件都拆成了独立的框架,例如:权限请求框架 [XXPermissions](https://github.com/getActivity/XXPermissions),网络请求框架 [EasyHttp](https://github.com/getActivity/EasyHttp)、吐司框架 [ToastUtils](https://github.com/getActivity/ToastUtils) 等等,我都是将它抽离在 AndroidProject 之外,作为一个单独的开源项目进行开发和维护,至于说为什么还有一些代码没有抽取出来,主要原因有几点: + + 1. 和业务的耦合性高,例如 Dialog 组件引用了很多项目的基类,例如 **BaseDialog**、**BaseAdapter** 等 + + 2. 业务有定制化需求,因为 Dialog 的 UI 风格要跟随项目的设计走,所以代码如果在项目中,修改起来会非常方便,如果抽取到框架中,要怎么修改和统一 UI 风格呢?我个人认为框架不适合做 UI 定制化,因为每个产品的设计风格都不一样,就算开放再多的 API 给外部调用的人设置 UI 风格,也无法满足所有人的需求。 + +* 基于以上几点,我并不认为所有的东西都适合抽取成框架给大家用,有些东西还是跟随 **AndroidProject** 一起更新比较好。当然像权限请求这种东西,我个人觉得抽成框架是比较合适的,因为它和业务的关联性不大,更重要的是,如果某一天你觉得 **XXPermissions** 做得不够好,你随时可以在 **AndroidProject** 替换掉它,并且整个过程不需要太大的改动。 + +#### 为什么最低兼容到 Android 5 + +* AndroidProject 从 [v11.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/11.0),已经将 minSdkVersion 从 19 升级到 21,原因也很简单,我不推荐大家兼容 Android 4.4 版本,因为这个版本兼容性的问题太多,对 **dex 分包**、**矢量图**的支持不是特别好,这个我们开发者处理不了,除此之外还有很多 API 要做高低版本兼容,这个我们开发者能做,但是我觉得没什么必要性,因为这个版本的机型会越来越少,会逐步退出历史舞台,而 AndroidProject 一旦投入到项目中使用,minSdkVersion 基本不会有变动,所以我的想法是,不如在一开始就不兼容这个版本,免得后面给大家带来一些不必要的麻烦,Android 4.4 有些问题是**真硬伤**,这是一个非常**令人头疼**的问题。 + +#### 为什么不加入扫描二维码功能 + +* AndroidProject 的定位是做一个技术架构,不是什么都做的 Demo 工程,如果只是解决大家的需求问题,那样在我看来意义其实并不大,当然实现需求固然很重要,但并不是所有的技术点在不同项目都会用到,AndroidProject 只是在做架构的同时顺道把模板做了,如果说架构是理论,那么模板就是实践,代码写得再好,如果不实践,那么也只是纸上谈兵,又或者中看不中用。 + +* AndroidProject 并不会为个人做定制,包括我自己,我可以给大家举个栗子,AndroidProject 集成了我很多自己的框架,但并不是所有我写的框架都会加入到里面去,例如[多语种框架](https://github.com/getActivity/MultiLanguages),主要原因是 App 国际化的场景并不多,大部分国内的 App 没有上架 GooglePlay,少数服从多数的原则,所以我没有加入这个框架到 AndroidProject 中,并不是框架做得不好,虽然加入会对这个框架有利,会有推广作用,但是不符合大部分人的利益,于是在大我和小我之间,我还是选择大我。这无疑是一个艰难的抉择,但是我必须得这么做。 + +#### 为什么不加入 EventBus + +* EventBus 我之前其实有加入过一版,只不过在 [v10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 版本上面移除了,原因很简单,它不是一个项目的必需品,我们用 EventBus 的初衷应该是,当需求在现有的基础上实现起来比较困难或者麻烦时,我们可以考虑用一用,但是到了实际项目中,会出现很多滥用的情况出现,在这里我建议大家,能用正常方式实现通讯的,尽量不要用 EventBus 实现。另外大家如果真的有需要,可以自行加入,集成也相对比较简单。 + +#### 为什么没有用 Retrofit 和 RxJava + +* 我想问大家一个问题,这两个框架搭配起来好用吗?可能大家的回答都不一致,但是我个人觉得不好用,接下来让我们分析一下 Retrofit 有什么问题: + + 1. 功能太少:如果你用过 Retrofit,一说到功能这个词,我相信你的脑海中第一个想到能不能用 OkHttp 的拦截器来实现,没错常用的功能 Retrofit 都没有封装,例如最常用的功能有:添加通用请求头和参数、请求和响应的日志打印、动态 Host 及多域名配置,Retrofit 统统都没有,需要自己去实现。有时候我在想,如果 Retrofit 没有 **Square 公司背书**,现在应该估计不会有多少人用吧。 + + 2. 不够灵活:Retrofit 其实是支持上传的,但是有一个缺点,不能获取进度监听,只能获取到成功和失败。当然网上也有一些解决方案,例如通过设置拦截器,来对 RequestBody 进行二次包装来获取上传进度,但是整个实现的过程十分地麻烦,在 Retrofit 上也没有给出一个好的方案,明明可以由 Retrofit 来做的事,为什么要分发到每个开发者上面。 + + 3. 学习成本高:Retrofit 主要的学习成本来源于它的注解,我现在把它里面所有注解罗列出来:@Url、@Body、@Field、@FieldMap、@FormUrlEncoded、@Header、@HeaderMap、@Headers、@HTTP、@Multipart、@Part、@PartMap、@Path、@Query、@QueryMap、@QueryName、@Streaming。我们了解过多少个注解的作用?这个时候大多数人肯定会说,我都是按照别人的写法复制一遍,具体有什么作用我还真的不知道。其实这个是学习成本高带来的弊端,人们往往只会记住最常用的那几个。 + +* 我感觉,大家用的不一定就是最好的,盲目地从众不是件好事,谈谈我的看法,在选用一个框架之前,我会分析它在项目实战中的优缺点,如果缺点大于优点,那是肯定不能接受的,如果优点过多,同时现有的缺点还能接受,还是可以考虑投入到项目中使用的。 + +* AndroidProject 在很长的时间内都没有加入网络请求框架,是因为我还没有找到合适的网络请求框架,如果一旦加入 Retrofit,我就不得不面对它带来的各种各样的问题,例如有很多人会问你这个功能怎么实现?那个功能怎么实现?与其这样,那我为什么不自己做一个呢? + +* 但我深知做好一个网络请求框架不是一件简单的事情,从 [OkGo](https://github.com/jeasonlzy/okhttp-OkGo) 作者弃更的事件来看,我大概就知道了这块领域一入坑深似海,但是网络请求是一个项目必不可少的部分,想要做好 AndroidProject,那网络请求这块一定不能少。终于在经过了半年多的设计和开发,[EasyHttp](https://github.com/getActivity/EasyHttp) 在 2019 年 12 月 7 日面世了,当我兴高采烈地发布时,却发现基本没有什么热度,有很多人都说我用 Retrofit + RxJava 它难道不香吗? + +* EasyHttp 在被备受冷落的期间,我也很难受,难道半年的心血要付之东流?我重新分析了 EasyHttp 的设计,它确实是块好料,但是要做到大部分人认可还需要一段时间的打磨,所以我选择了坚守,因为我相信是金子终有一天会发光,我愿意付出大量的时间和精力来维护它。最近有一个好消息可以跟大家分享,我渐渐收到了很多关于 EasyHttp 的夸赞,都是说 EasyHttp 很好用、灵活性很高,这让我越发觉得自己做的是对的,如果没有这些肯定,我可能早就坚持不下去了。 + +#### 为什么没有用 Jetpack 全家桶 + +* AndroidProject 里面其实有运用到和 Jetpack 相关的技术,例如 Lifecycle 特性,在 BaseDialog 加入了此特性,不仅如此,里面引入的 EasyHttp 网络请求框架也采用了 Lifecycle 特性来管控网络请求,Lifecycle 是一个好东西,把组件的生命周期抽象化了,这样我们无需要关心这个组件是 Activity 或 Fragment,又或者是其他类型的组件。 + +* 但是除了 Lifecycle 组件之外,LiveData 和 ViewModel 组件在 AndroidProject 基本没有用到,这个是因为 AndroidProject 有自己的代码设计思想,只会集成一些合适的代码库,不会一味地去追求什么全家桶,框架选型是要综合考虑很多方面的因素,并没有大家想得那么简单。 + +#### 为什么不对图片加载框架进行再次封装 + +* 常用的图片加载框架无非就两种,最常用的是 Glide,其次是 Fresco。我曾做过一个技术调研: + +![](picture/help/vote1.jpg) + +* 无疑 Glide 已成大家最喜爱的图片加载框架,当然也有人使用 Fresco,但是占比极少。 + +* 那既然萝卜白菜各有所爱,那么为什么不对图片加载框架抽取成接口呢?这样不就把所有的问题都解决了? + +* 其实 [AndroidProject 10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 之前的版本有做过这块的内容,但是移除的原因是,抽取接口其实不难,难的是后续的扩展,例如 Glide 给我们开放了很多 API,我们最常用的是加载网络图片、加载圆角图片、加载圆形图片,但是如果是其他形状的图片呢?那就要涉及到 Glide 图形变换的 API 了,还有一个就是加载监听的事件,也需要涉及到 Glide 的 API,缓存策略,不止如此,还有很多 API 都涉及到 Glide 的 API,如果直接用 Glide 来做,我们可以轻松实现,但是如果经过一层的代码封装,那么会把框架本身的灵活性给扼杀掉。但并不是不可以实现,而是没有这个必要,就算做了付出和收益也会远远不成正比,同时也会给大家带来一定的学习成本。 + +#### 模板 架构 技术中台有什么区别 + +* AndroidProject 正式从 **安卓架构** 更名为 **安卓技术中台**,因为它符合技术中台的特性,既能够做到快速开发,同时又能保证后续维护也能快速迭代。大家可以也将技术中台理解为:模板+架构,一般写模板代码的人做不了架构设计,而做架构设计的人又不想写模板代码,那么技术中台的概念便出现了,并且结合了这两种的优点,开发和维护都兼顾到位。 + +#### 为什么不按业务来划分包名 + +* 有一些业务职责不明确,无法限定属于哪一个业务模块,并且大多数模块的类都是比较少,只有少部分的模块拥有一定数量的类,所以在一般的中小项目开发中,我更推荐以类的作用来划分包名。 + +#### 为什么没有关于列表多 type 的封装 + +* 原生的 RecyclerView.Adapter 本身就支持多 type,只需要重写适配器的 getItemType 方法即可,具体用法不做过多介绍。 + +#### 这不就是一个模板工程换成我也能写一个 + +* 想把 AndroidProject 做出来并不难,我当时只花了一两个星期,而做好它需要无限的时间和精力,我花了两年多的时间仍然还在半路之上,尽管有很多人认为它很好用,没有任何 Bug,但是在我看来还不够,因为每个人衡量标准的程度不同,我的标准是随着时间的推移和技术的提升而不断提高。具体付出了多少努力,[可以先让我们看一组数据](https://github.com/getActivity/AndroidProject/graphs/contributors): + +![](picture/help/contributors.jpg) + +* 与其说 AndroidProject 做的是模板工程,但实际我在架构设计上花费的时间和精力会更多,其实这两者我都有在做,因为我的目的只有一个,能够帮助大家更好地开发和维护项目。具体 AndroidProject 在代码设计上有什么亮点,这里我建议你看一下里面的代码,我相信你看完后会有收获的,后面我可能也会出一篇文章具体讲述 AndroidProject 的亮点。 + +#### 假设 AndroidProject 更新了该怎么升级它 + +* 原因和解释:首先纠正一点,AndroidProject 严格意义上来说,不是框架一种,而属于架构一种,架构升级本身就是一件大事,并且存在很多未知的风险点,我不推荐已使用 AndroidProject 开发的项目去做升级,因为开发和测试的成本极其高,间接能为业务带来价值其实很低,很多时候我知道大家很喜欢 AndroidProject 的代码,想用到公司项目中去,但是我仍然不推荐你那么做,假设这是你的个人项目可以那么做,但是公司项目最好不要,因为公司和你都是要靠这个项目赚钱,谁也不希望项目出现问题,如果是公司要开发人员重构公司项目,也可以考虑那么做,毕竟这个时候的风险公司已经承担了大部分了,接下来的话只需要服从公司安排即可。 + +* 更新的方式:由于 AndroidProject 不是一个单独的框架那么简单,无法通过更新远程依赖的方式进行升级,所以只能通过替换代码的形式进行更新,需要注意的是,代码覆盖完需要经过严格的自测及测试,测试是做这件事情的关键流程,需要重视起来,对每一处功能进行详细测试,一定要详细,特别涉及到主流程的功能。 + +#### 为什么不用谷歌 ActivityResultContracts + +* ActivityResultContract是 Activity 1.2.0-alpha02 和 Fragment 1.3.0-alpha02 中新追加的新 API,但是在此之前 AndroidProject 早已经对 onActivityResult 回调进行了封装,详情请见 BaseActivity + +```java +public abstract class BaseActivity extends AppCompatActivity { + + /** Activity 回调集合 */ + private SparseArray mActivityCallbacks; + + /** + * startActivityForResult 方法优化 + */ + + public void startActivityForResult(Class clazz, OnActivityCallback callback) { + startActivityForResult(new Intent(this, clazz), null, callback); + } + + public void startActivityForResult(Intent intent, OnActivityCallback callback) { + startActivityForResult(intent, null, callback); + } + + public void startActivityForResult(Intent intent, @Nullable Bundle options, OnActivityCallback callback) { + if (mActivityCallbacks == null) { + mActivityCallbacks = new SparseArray<>(1); + } + // 请求码必须在 2 的 16 次方以内 + int requestCode = new Random().nextInt((int) Math.pow(2, 16)); + mActivityCallbacks.put(requestCode, callback); + startActivityForResult(intent, requestCode, options); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + OnActivityCallback callback; + if (mActivityCallbacks != null && (callback = mActivityCallbacks.get(requestCode)) != null) { + callback.onActivityResult(resultCode, data); + mActivityCallbacks.remove(requestCode); + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + public interface OnActivityCallback { + + /** + * 结果回调 + * + * @param resultCode 结果码 + * @param data 数据 + */ + void onActivityResult(int resultCode, @Nullable Intent data); + } +} +``` + +* 至于要不要换成谷歌出的那种呢?我们先来对比这两种的方式的用法 + +```java +// Google 的用法 +registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult result) { + Intent data = result.getData(); + int resultCode = result.getResultCode(); + } +}).launch(new Intent(this, HomeActivity.class)); +``` + +--- + +```java +// AndroidProject 的用法 +startActivityForResult(HomeActivity.class, new OnActivityCallback() { + @Override + public void onActivityResult(int resultCode, @Nullable Intent data) { + + } +}); +``` + +* 对这两种经过对比,得出结论如下: + + 1. 谷歌原生的没有 AndroidProject 封装得那么人性化,谷歌那种方式调用稍微麻烦一点 + + 2. 谷歌那种方式直接集成进 AndroidX 包的,要比直接在 BaseActivity 中封装要好 + + 3. AndroidProject 封装 onActivityResult 回调至少要比谷歌要早一两年,并非谷歌之后的产物 + + 4. 之前使用 AndroidProject 的人群已经习惯和记忆了那种方式,所以 API 不能删也不能改 + +* 所以并不是我不想用,而是谷歌封装得还不够好,至少在我看来还不够好,抛去 AndroidProject 封装的时间早不说,谷歌封装出来的效果也是强差人意,我感觉谷歌工程师的封装得越来越敷衍了,看起来像是在完成任务,而不是在做好一件事。 + +#### 轮子哥你怎么看待层出不穷的新技术 + +* 新东西的出现总能引起别人的好奇和尝试,但是我建议有兴趣的人可以学一下,但是如果要应用到项目中,我个人建议还是要慎重,因为纵观历史,我们不难发现,技术创新虽然很受欢迎,但是大多数都经不住时间的考验,最终一个个气尽倒下,这是因为很多新技术,表面看起来很美好,但实际上一入坑深似海。当然也有一些优秀的技术创新活了下来,但是毕竟占的是少数。 + +* 谈谈我对新技术的看法,首先我会思考这种新技术能解决什么痛点,这点非常重要,再好的技术创新,也必须得创造价值,否则就是在扯淡。有人肯定会问,什么样的技术才算有价值?对于我们 Android 程序员来讲,无非就围绕两点,开发和维护。要么在前期开发上,能发挥很大的作用,要么在后续维护上面,能体现它的优势。 + +* 还有谷歌的新技术不一定都是好的,也有一些是 **KPI 产物**,别忘了,他们也是打工的,他们也有 **KPI 考核**,为了年终奖和晋升,他们不得不卖力宣传,纵使他们知道这个东西有硬伤,但是他们也会推出来看看市场反应。所以我们看待一种新技术,不要太看重是否是大公司出品的,也不要太看重是哪个行业名人写的,我们应该要重点关注的是,产品的质量以及能带给我们带来哪些帮助,还有会带来哪些不好的影响,这个才是正确的技术价值观。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4b7bce0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, October 2018 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Huang JinQun + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index ebf3db5..8f5c438 100644 --- a/README.md +++ b/README.md @@ -1 +1,161 @@ -# 开发中,敬请期待,Java 版本:[AndroidProject](https://github.com/getActivity/AndroidProject) \ No newline at end of file +# 安卓技术中台 Kotlin 版 + +* 项目地址:[Github](https://github.com/getActivity/AndroidProject-Kotlin)、[码云](https://gitee.com/getActivity/AndroidProject-Kotlin) + +* Java 版本:[AndroidProject](https://github.com/getActivity/AndroidProject) + +* 博客地址:[但愿人长久,搬砖不再有](https://www.jianshu.com/p/77dd326f21dc) + +* 当我们日复一日年复一年的搬砖的时候,你是否曾想过提升一下开发效率,如果一个通用的架构摆在你的面前,你还会选择自己搭架构么,但是搭建出一个好的架构并非易事,有多少人愿意选择去做,还有多少人选择努力去做好,可能寥寥无几,但是你今天看到的,正是你所想要的,一个真正能解决你开发新项目时最大痛点的架构工程,你不需要再麻木 Copy 原有旧项目的代码,只需改动少量代码就能得到想要的效果,你会发现开发新项目其实是一件很快乐的事。 + +* AndroidProject 已维护三年多的时间,几乎耗尽我所有的业余时间,里面的代码改了再改,改了又改,不断 Review、不断创新、不断改进、不断测试、不断优化,每天都在重复这些枯燥的步骤,但是只有这样才能把这件事做好,因为我相信把同样一件事重复做,迟早有一天可以做好。 + +* 已经正式投入到多个公司项目实践中,暂时没有发现任何问题或者 Bug,[点击下载 Apk 体验](https://github.com/getActivity/AndroidProject-Kotlin/releases/download/13.1/AndroidProject-Kotlin.apk),又或者扫码下载 + +![](picture/demo_code.png) + +#### 常用界面 + +![](picture/activity/1.jpg) ![](picture/activity/2.jpg) ![](picture/activity/3.jpg) + +![](picture/activity/4.jpg) ![](picture/activity/5.jpg) ![](picture/activity/6.jpg) + +![](picture/activity/7.jpg) ![](picture/activity/8.jpg) ![](picture/activity/9.jpg) + +![](picture/activity/10.jpg) ![](picture/activity/11.jpg) ![](picture/activity/12.jpg) + +![](picture/activity/13.jpg) ![](picture/activity/14.jpg) ![](picture/activity/15.jpg) + +![](picture/activity/16.jpg) ![](picture/activity/17.jpg) ![](picture/activity/18.jpg) + +![](picture/activity/19.jpg) ![](picture/activity/20.jpg) ![](picture/activity/21.jpg) + +![](picture/activity/22.jpg) ![](picture/activity/23.jpg) ![](picture/activity/24.jpg) + +![](picture/activity/25.jpg) ![](picture/activity/26.jpg) ![](picture/activity/27.jpg) + +------ + +![](picture/activity/28.jpg) + +![](picture/activity/29.jpg) + +![](picture/activity/30.jpg) + +![](picture/activity/31.jpg) + +![](picture/activity/32.jpg) + +![](picture/activity/33.jpg) + +![](picture/activity/34.jpg) + +![](picture/activity/35.jpg) + +![](picture/activity/36.jpg) + +#### 常用对话框 + +![](picture/dialog/1.jpg) ![](picture/dialog/2.jpg) ![](picture/dialog/3.jpg) + +![](picture/dialog/4.jpg) ![](picture/dialog/5.jpg) ![](picture/dialog/6.jpg) + +![](picture/dialog/7.jpg) ![](picture/dialog/8.jpg) ![](picture/dialog/9.jpg) + +![](picture/dialog/10.jpg) ![](picture/dialog/11.jpg) ![](picture/dialog/12.jpg) + +![](picture/dialog/13.jpg) ![](picture/dialog/14.jpg) ![](picture/dialog/15.jpg) + +![](picture/dialog/16.jpg) ![](picture/dialog/17.jpg) ![](picture/dialog/18.jpg) + +#### 动图欣赏 + +![](picture/gif/1.gif) ![](picture/gif/2.gif) ![](picture/gif/3.gif) + +![](picture/gif/4.gif) ![](picture/gif/5.gif) ![](picture/gif/6.gif) + +![](picture/gif/7.gif) ![](picture/gif/8.gif) ![](picture/gif/9.gif) + +![](picture/gif/10.gif) ![](picture/gif/11.gif) ![](picture/gif/12.gif) + +#### 项目亮点 + +* App 优化:已经进行了全面的内存优化、布局优化、代码优化、瘦身优化,并且对结果进行了严格的长久测试。 + +* 代码规范:参照 Android SDK 、Support 源码和参考阿里巴巴的代码规范文档对代码进行命名,并对难点代码进行了注释,对重点代码进行了说明。 + +* 代码统一:对项目中常见的代码进行了封装,或是封装到基类中、或是封装到工具类中、或者封装到框架中,不追求过度封装,根据实际场景和代码维护性考虑,尽量保证同一个功能的代码在项目中不重复。 + +* 敏捷开发:一个 App 大概率会出现的功能已经写好,对项目的敏捷开发起到了至关重要的作用,可用于新项目开发或者旧项目重构,可将开发周期缩短近一半的时间,并且后续不会因为前期的快速开发而留下成堆的技术遗留问题,万丈高楼平地起,AndroidProject 属于基建工程,而在软件行业我们称之为技术中台。 + +* 无任何瑕疵:对小屏手机、全面屏手机、带虚拟按键手机进行了适配和优化,确保每一个界面细节都能处理到位、每一个功能细节都能符合大众的需求、乃至每一行代码都能贴合 Android 程序员的审美观。 + +* 兼容性优良:在此感谢开源道路上给予我支持和帮助的小伙伴,一个人一台机在兼容性面前无能为力,而在几百人几百台机面前却不是问题。如果没有这些的测试,有些问题我一个人可能这辈子都发现不了,纵使代码写得再好,逻辑再严谨,没有经过大众的验证,无异于纸上谈兵。 + +* 优秀的代码设计:AndroidProject 对 startActivityForResult 的设计进行了改良,使得可以直接在方法上传入监听对象,这样我们就不需要重写 onActivityResult 方法来拿到回调,另外原生的 startActivityForResult 还需要传 requestCode 参数,而 AndroidProject 会自动帮你生成这个 requestCode 码,并在 onActivityResult 进行判断,如果满足条件,那么就会回调外层传入的监听对象。然而这只是冰山一角,更多优秀的代码设计还需要你通过阅读 AndroidProject 源码的形式来发掘,在这里不再细说。 + +#### [代码规范文档请点击这里查看](https://github.com/getActivity/AndroidCodeStandard) + +#### [版本适配文档请点击这里查看](https://github.com/getActivity/AndroidVersionAdapter) + +#### [常见问题解答请点击这里查看](HelpDoc.md) + +#### 作者的其他开源项目 + +* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) + +* 吐司框架:[ToastUtils](https://github.com/getActivity/ToastUtils) ![](https://img.shields.io/github/stars/getActivity/ToastUtils.svg) ![](https://img.shields.io/github/forks/getActivity/ToastUtils.svg) + +* 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) ![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg) ![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) + +* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) ![](https://img.shields.io/github/stars/getActivity/TitleBar.svg) ![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) + +* 悬浮窗框架:[XToast](https://github.com/getActivity/XToast) ![](https://img.shields.io/github/stars/getActivity/XToast.svg) ![](https://img.shields.io/github/forks/getActivity/XToast.svg) + +* Shape 框架:[ShapeView](https://github.com/getActivity/ShapeView) ![](https://img.shields.io/github/stars/getActivity/ShapeView.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) + +* 语种切换框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) ![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg) ![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) + +* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) ![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg) ![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) + +* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) ![](https://img.shields.io/github/stars/getActivity/Logcat.svg) ![](https://img.shields.io/github/forks/getActivity/Logcat.svg) + +* Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) ![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) + +* Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) + +* Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) + +* 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) + +* 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson) ![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg) ![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) + +#### 微信公众号:Android轮子哥 + +![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png) + +#### Android 技术分享 QQ 群:78797078 + +#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat: + +![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) + +#### [点击查看捐赠列表](https://github.com/getActivity/Donate) + +## License + +```text +Copyright 2018 Huang JinQun + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` \ No newline at end of file diff --git a/app/AppSignature.jks b/app/AppSignature.jks new file mode 100644 index 0000000..82bab2f Binary files /dev/null and b/app/AppSignature.jks differ diff --git a/app/AppSignature.jpg b/app/AppSignature.jpg new file mode 100644 index 0000000..8db4077 Binary files /dev/null and b/app/AppSignature.jpg differ diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..7856824 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,217 @@ +apply plugin : 'com.android.application' +apply plugin : 'android-aspectjx' +apply plugin : 'kotlin-kapt' +apply from : '../common.gradle' + +// Android 代码规范文档:https://github.com/getActivity/AndroidCodeStandard +android { + + // 资源目录存放指引:https://developer.android.google.cn/guide/topics/resources/providing-resources + defaultConfig { + + // 无痛修改包名:https://www.jianshu.com/p/17327e191d2e + applicationId 'com.hjq.demo' + + // 仅保留中文语种的资源 + resConfigs 'zh' + + // 仅保留 xxhdpi 图片资源(目前主流分辨率 1920 * 1080) + resConfigs 'xxhdpi' + + // 混淆配置 + proguardFiles 'proguard-sdk.pro', 'proguard-app.pro' + + // 日志打印开关 + buildConfigField('boolean', 'LOG_ENABLE', '' + LOG_ENABLE + '') + // 测试包下的 BuglyId + buildConfigField('String', 'BUGLY_ID', '"' + BUGLY_ID + '"') + // 测试服务器的主机地址 + buildConfigField('String', 'HOST_URL', '"' + HOST_URL + '"') + } + + // Apk 签名的那些事:https://www.jianshu.com/p/a1f8e5896aa2 + signingConfigs { + config { + storeFile file(StoreFile) + storePassword StorePassword + keyAlias KeyAlias + keyPassword KeyPassword + } + } + + // 构建配置:https://developer.android.google.cn/studio/build/build-variants + buildTypes { + + debug { + // 给包名添加后缀 + applicationIdSuffix '.debug' + // 调试模式开关 + debuggable true + jniDebuggable true + // 压缩对齐开关 + zipAlignEnabled false + // 移除无用的资源 + shrinkResources false + // 代码混淆开关 + minifyEnabled false + // 签名信息配置 + signingConfig signingConfigs.config + // 添加清单占位符 + addManifestPlaceholders([ + 'app_name' : '安卓技术中台 Debug 版' + ]) + // 调试模式下只保留一种架构的 so 库,提升打包速度 + ndk { + abiFilters 'armeabi-v7a' + } + } + + preview.initWith(debug) + preview { + applicationIdSuffix '' + // 添加清单占位符 + addManifestPlaceholders([ + 'app_name' : '安卓技术中台 Preview 版' + ]) + } + + release { + // 调试模式开关 + debuggable false + jniDebuggable false + // 压缩对齐开关 + zipAlignEnabled true + // 移除无用的资源 + shrinkResources true + // 代码混淆开关 + minifyEnabled true + // 签名信息配置 + signingConfig signingConfigs.config + // 添加清单占位符 + addManifestPlaceholders([ + 'app_name' : '@string/app_name' + ]) + // 仅保留两种架构的 so 库,根据 Bugly 统计得出 + ndk { + // armeabi:万金油架构平台(占用率:0%) + // armeabi-v7a:曾经主流的架构平台(占用率:10%) + // arm64-v8a:目前主流架构平台(占用率:95%) + abiFilters 'armeabi-v7a', 'arm64-v8a' + } + } + } + + packagingOptions { + // 剔除这个包下的所有文件(不会移除签名信息) + exclude 'META-INF/*******' + } + + // AOP 配置(exclude 和 include 二选一) + // 需要进行配置,否则就会引发冲突,具体表现为: + // 第一种:编译不过去,报错:java.util.zip.ZipException:Cause: zip file is empty + // 第二种:编译能过去,但运行时报错:ClassNotFoundException: Didn't find class on path: DexPathList + aspectjx { + // 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突) + // exclude 'androidx', 'com.google', 'com.squareup', 'org.apache', 'com.alipay', 'com.taobao', 'versions.9' + // 只对以下包名做 AOP 处理 + include android.defaultConfig.applicationId + } + + applicationVariants.all { variant -> + // apk 输出文件名配置 + variant.outputs.all { output -> + outputFileName = rootProject.getName() + '_v' + variant.versionName + '_' + variant.buildType.name + if (variant.buildType.name == buildTypes.release.getName()) { + outputFileName += '_' + new Date().format('MMdd') + } + outputFileName += '.apk' + } + } +} + +// 添加构建依赖项:https://developer.android.google.cn/studio/build/dependencies +// api 与 implementation 的区别:https://www.jianshu.com/p/8962d6ba936e +dependencies { + // 基类封装 + implementation project(':library:base') + // 控件封装 + implementation project(':library:widget') + // 友盟封装 + implementation project(':library:umeng') + + // 权限请求框架:https://github.com/getActivity/XXPermissions + implementation 'com.github.getActivity:XXPermissions:12.3' + + // 标题栏框架:https://github.com/getActivity/TitleBar + implementation 'com.github.getActivity:TitleBar:9.2' + + // 吐司框架:https://github.com/getActivity/ToastUtils + implementation 'com.github.getActivity:ToastUtils:9.5' + + // 网络请求框架:https://github.com/getActivity/EasyHttp + implementation 'com.github.getActivity:EasyHttp:10.2' + // OkHttp 框架:https://github.com/square/okhttp + // noinspection GradleDependency + implementation 'com.squareup.okhttp3:okhttp:3.12.13' + + // Json 解析框架:https://github.com/google/gson + implementation 'com.google.code.gson:gson:2.8.8' + // Gson 解析容错:https://github.com/getActivity/GsonFactory + implementation 'com.github.getActivity:GsonFactory:5.2' + + // Shape 框架:https://github.com/getActivity/ShapeView + implementation 'com.github.getActivity:ShapeView:6.0' + + // AOP 插件库:https://mvnrepository.com/artifact/org.aspectj/aspectjrt + implementation 'org.aspectj:aspectjrt:1.9.6' + + // 图片加载框架:https://github.com/bumptech/glide + // 官方使用文档:https://github.com/Muyangmin/glide-docs-cn + implementation 'com.github.bumptech.glide:glide:4.12.0' + kapt 'com.github.bumptech.glide:compiler:4.12.0' + + // 沉浸式框架:https://github.com/gyf-dev/ImmersionBar + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + + // 手势 ImageView:https://github.com/Baseflow/PhotoView + implementation 'com.github.Baseflow:PhotoView:2.3.0' + + // Bugly 异常捕捉:https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20190418140644 + implementation 'com.tencent.bugly:crashreport:3.4.4' + implementation 'com.tencent.bugly:nativecrashreport:3.9.2' + + // 动画解析库:https://github.com/airbnb/lottie-android + // 动画资源:https://lottiefiles.com、https://icons8.com/animated-icons + implementation 'com.airbnb.android:lottie:4.1.0' + + // 上拉刷新下拉加载框架:https://github.com/scwang90/SmartRefreshLayout + implementation 'com.scwang.smart:refresh-layout-kernel:2.0.3' + implementation 'com.scwang.smart:refresh-header-material:2.0.3' + + // 日志打印框架:https://github.com/JakeWharton/timber + implementation 'com.jakewharton.timber:timber:4.7.1' + + // 指示器框架:https://github.com/ongakuer/CircleIndicator + implementation 'me.relex:circleindicator:2.1.6' + + // 腾讯 MMKV:https://github.com/Tencent/MMKV + implementation 'com.tencent:mmkv-static:1.2.10' + + // 内存泄漏监测框架:https://github.com/square/leakcanary + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' + previewImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' + + // 多语种:https://github.com/getActivity/MultiLanguages + // 悬浮窗:https://github.com/getActivity/XToast + // 日志输出:https://github.com/getActivity/Logcat + // 工具类:https://github.com/Blankj/AndroidUtilCode + // 轮播图:https://github.com/bingoogolapple/BGABanner-Android + // 二维码:https://github.com/bingoogolapple/BGAQRCode-Android + // 跑马灯:https://github.com/sunfusheng/MarqueeView + // 对象注解:https://www.jianshu.com/p/f1f888e4a35f + // 对象存储:https://github.com/leavesC/DoKV + // 多渠道打包:https://github.com/Meituan-Dianping/walle + // 设备唯一标识:http://msa-alliance.cn/col.jsp?id=120 + // 嵌套滚动容器:https://github.com/donkingliang/ConsecutiveScroller + // 隐私调用监控:https://github.com/huage2580/PermissionMonitor +} \ No newline at end of file diff --git a/app/gradle.properties b/app/gradle.properties new file mode 100644 index 0000000..97eeed7 --- /dev/null +++ b/app/gradle.properties @@ -0,0 +1,4 @@ +StoreFile = AppSignature.jks +StorePassword = AndroidProject +KeyAlias = AndroidProject +KeyPassword = AndroidProject \ No newline at end of file diff --git a/app/proguard-app.pro b/app/proguard-app.pro new file mode 100644 index 0000000..58761e4 --- /dev/null +++ b/app/proguard-app.pro @@ -0,0 +1,21 @@ +# 忽略警告 +#-ignorewarning + +# 混淆保护自己项目的部分代码以及引用的第三方jar包 +#-libraryjars libs/xxxxxxxxx.jar + +# 不混淆这个包下的类 +-keep class com.hjq.demo.http.api.** { + ; +} +-keep class com.hjq.demo.http.response.** { + ; +} +-keep class com.hjq.demo.http.model.** { + ; +} + +# 不混淆被 Log 注解的方法信息 +-keepclassmembernames class ** { + @com.hjq.demo.aop.Log ; +} \ No newline at end of file diff --git a/app/proguard-sdk.pro b/app/proguard-sdk.pro new file mode 100644 index 0000000..5c39db9 --- /dev/null +++ b/app/proguard-sdk.pro @@ -0,0 +1,36 @@ +# Glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep class * extends com.bumptech.glide.module.AppGlideModule { + (...); +} +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} +-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { + *** rewind(); +} + +# for DexGuard only +#-keepresourcexmlelements manifest/application/meta-data@value=GlideModule + +# Bugly +-dontwarn com.tencent.bugly.** +-keep public class com.tencent.bugly.**{*;} + +# AOP +-adaptclassstrings +-keepattributes InnerClasses, EnclosingMethod, Signature, *Annotation* + +-keepnames @org.aspectj.lang.annotation.Aspect class * { + public ; +} + +# OkHttp3 +-keepattributes Signature +-keepattributes *Annotation* +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** +-dontwarn okio.** +-dontwarn org.conscrypt.** \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d4b70b4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/StatusAction.kt b/app/src/main/java/com/hjq/demo/action/StatusAction.kt new file mode 100644 index 0000000..44cc0bb --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/StatusAction.kt @@ -0,0 +1,93 @@ +package com.hjq.demo.action + +import android.graphics.drawable.Drawable +import android.net.ConnectivityManager +import android.net.NetworkInfo +import androidx.annotation.DrawableRes +import androidx.annotation.RawRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import com.hjq.demo.R +import com.hjq.demo.widget.StatusLayout +import com.hjq.demo.widget.StatusLayout.OnRetryListener + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/08 + * desc : 状态布局意图 + */ +interface StatusAction { + + /** + * 获取状态布局 + */ + fun getStatusLayout(): StatusLayout? + + /** + * 显示加载中 + */ + fun showLoading(@RawRes id: Int = R.raw.loading) { + getStatusLayout()?.let { + it.show() + it.setAnimResource(id) + it.setHint("") + it.setOnRetryListener(null) + } + } + + /** + * 显示加载完成 + */ + fun showComplete() { + getStatusLayout()?.let { + if (!it.isShow()) { + return + } + it.hide() + } + } + + /** + * 显示空提示 + */ + fun showEmpty() { + showLayout(R.drawable.status_empty_ic, R.string.status_layout_no_data, null) + } + + /** + * 显示错误提示 + */ + fun showError(listener: OnRetryListener?) { + getStatusLayout()?.let { + val manager: ConnectivityManager? = ContextCompat.getSystemService(it.context, ConnectivityManager::class.java) + if (manager != null) { + val info: NetworkInfo? = manager.activeNetworkInfo + // 判断网络是否连接 + if (info == null || !info.isConnected) { + showLayout(R.drawable.status_network_ic, R.string.status_layout_error_network, listener) + return + } + } + showLayout(R.drawable.status_error_ic, R.string.status_layout_error_request, listener) + } + } + + /** + * 显示自定义提示 + */ + fun showLayout(@DrawableRes drawableId: Int, @StringRes stringId: Int, listener: OnRetryListener?) { + getStatusLayout()?.let { + showLayout(ContextCompat.getDrawable(it.context, drawableId), it.context.getString(stringId), listener) + } + } + + fun showLayout(drawable: Drawable?, hint: CharSequence?, listener: OnRetryListener?) { + getStatusLayout()?.let { + it.show() + it.setIcon(drawable) + it.setHint(hint) + it.setOnRetryListener(listener) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/TitleBarAction.kt b/app/src/main/java/com/hjq/demo/action/TitleBarAction.kt new file mode 100644 index 0000000..487a1d7 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/TitleBarAction.kt @@ -0,0 +1,139 @@ +package com.hjq.demo.action + +import android.graphics.drawable.Drawable +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import com.hjq.bar.OnTitleBarListener +import com.hjq.bar.TitleBar + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/08 + * desc : 标题栏意图 + */ +interface TitleBarAction : OnTitleBarListener { + + /** + * 获取标题栏对象 + */ + fun getTitleBar(): TitleBar? + + /** + * 左项被点击 + * + * @param view 被点击的左项View + */ + override fun onLeftClick(view: View) {} + + /** + * 标题被点击 + * + * @param view 被点击的标题View + */ + override fun onTitleClick(view: View) {} + + /** + * 右项被点击 + * + * @param view 被点击的右项View + */ + override fun onRightClick(view: View) {} + + /** + * 设置标题栏的标题 + */ + fun setTitle(@StringRes id: Int) { + getTitleBar()?.setTitle(id) + } + + /** + * 设置标题栏的标题 + */ + fun setTitle(title: CharSequence?) { + getTitleBar()?.title = title + } + + /** + * 设置标题栏的左标题 + */ + fun setLeftTitle(id: Int) { + getTitleBar()?.setLeftTitle(id) + } + + fun setLeftTitle(text: CharSequence?) { + getTitleBar()?.leftTitle = text + } + + fun getLeftTitle(): CharSequence? { + return getTitleBar()?.leftTitle + } + + /** + * 设置标题栏的右标题 + */ + fun setRightTitle(id: Int) { + getTitleBar()?.setRightTitle(id) + } + + fun setRightTitle(text: CharSequence?) { + getTitleBar()?.rightTitle = text + } + + fun getRightTitle(): CharSequence? { + return getTitleBar()?.rightTitle + } + + /** + * 设置标题栏的左图标 + */ + fun setLeftIcon(id: Int) { + getTitleBar()?.setLeftIcon(id) + } + + fun setLeftIcon(drawable: Drawable?) { + getTitleBar()?.leftIcon = drawable + } + + fun getLeftIcon(): Drawable? { + return getTitleBar()?.leftIcon + } + + /** + * 设置标题栏的右图标 + */ + fun setRightIcon(id: Int) { + getTitleBar()?.setRightIcon(id) + } + + fun setRightIcon(drawable: Drawable?) { + getTitleBar()?.rightIcon = drawable + } + + fun getRightIcon(): Drawable? { + return getTitleBar()?.rightIcon + } + + /** + * 递归获取 ViewGroup 中的 TitleBar 对象 + */ + fun obtainTitleBar(group: ViewGroup?): TitleBar? { + if (group == null) { + return null + } + for (i in 0 until group.childCount) { + val view = group.getChildAt(i) + if (view is TitleBar) { + return view + } + if (view is ViewGroup) { + val titleBar = obtainTitleBar(view) + if (titleBar != null) { + return titleBar + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/ToastAction.kt b/app/src/main/java/com/hjq/demo/action/ToastAction.kt new file mode 100644 index 0000000..a5451fa --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/ToastAction.kt @@ -0,0 +1,25 @@ +package com.hjq.demo.action + +import androidx.annotation.StringRes +import com.hjq.toast.ToastUtils + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/08 + * desc : 吐司意图 + */ +interface ToastAction { + + fun toast(text: CharSequence?) { + ToastUtils.show(text) + } + + fun toast(@StringRes id: Int) { + ToastUtils.show(id) + } + + fun toast(`object`: Any?) { + ToastUtils.show(`object`) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/CheckNet.kt b/app/src/main/java/com/hjq/demo/aop/CheckNet.kt new file mode 100644 index 0000000..3c74dca --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/CheckNet.kt @@ -0,0 +1,13 @@ +package com.hjq.demo.aop + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/01/11 + * desc : 网络检测注解 + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER) +annotation class CheckNet \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/CheckNetAspect.kt b/app/src/main/java/com/hjq/demo/aop/CheckNetAspect.kt new file mode 100644 index 0000000..f8d263c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/CheckNetAspect.kt @@ -0,0 +1,50 @@ +package com.hjq.demo.aop + +import android.app.* +import android.net.ConnectivityManager +import android.net.NetworkInfo +import androidx.core.content.ContextCompat +import com.hjq.demo.R +import com.hjq.demo.manager.ActivityManager +import com.hjq.toast.ToastUtils +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/01/11 + * desc : 网络检测切面 + */ +@Suppress("unused") +@Aspect +class CheckNetAspect { + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.CheckNet * *(..))") + fun method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("method() && @annotation(checkNet)") + @Throws(Throwable::class) + fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, checkNet: CheckNet) { + val application: Application = ActivityManager.getInstance().getApplication() + val manager: ConnectivityManager? = ContextCompat.getSystemService(application, ConnectivityManager::class.java) + if (manager != null) { + val info: NetworkInfo? = manager.activeNetworkInfo + // 判断网络是否连接 + if (info == null || !info.isConnected) { + ToastUtils.show(R.string.common_network_hint) + return + } + } + //执行原方法 + joinPoint.proceed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/Log.kt b/app/src/main/java/com/hjq/demo/aop/Log.kt new file mode 100644 index 0000000..92ae491 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/Log.kt @@ -0,0 +1,14 @@ +package com.hjq.demo.aop + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/06 + * desc : Debug 日志注解 + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.CONSTRUCTOR) +annotation class Log constructor(val value: String = "AppLog") \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/LogAspect.kt b/app/src/main/java/com/hjq/demo/aop/LogAspect.kt new file mode 100644 index 0000000..1fc37c3 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/LogAspect.kt @@ -0,0 +1,133 @@ +package com.hjq.demo.aop + +import android.os.Looper +import android.os.Trace +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.Signature +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut +import org.aspectj.lang.reflect.CodeSignature +import org.aspectj.lang.reflect.MethodSignature +import timber.log.Timber +import java.util.concurrent.TimeUnit + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/06 + * desc : Debug 日志切面 + */ +@Suppress("unused") +@Aspect +class LogAspect { + + /** + * 构造方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.Log *.new(..))") + fun constructor() {} + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.Log * *(..))") + fun method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("(method() || constructor()) && @annotation(log)") + @Throws(Throwable::class) + fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, log: Log): Any? { + enterMethod(joinPoint, log) + val startNanos: Long = System.nanoTime() + val result: Any? = joinPoint.proceed() + val stopNanos: Long = System.nanoTime() + exitMethod(joinPoint, log, result, TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos)) + return result + } + + /** + * 方法执行前切入 + */ + private fun enterMethod(joinPoint: ProceedingJoinPoint, log: Log) { + val codeSignature: CodeSignature = joinPoint.signature as CodeSignature + + // 方法所在类 + val className: String = codeSignature.declaringType.name + // 方法名 + val methodName: String = codeSignature.name + // 方法参数名集合 + val parameterNames: Array = codeSignature.parameterNames + // 方法参数值集合 + val parameterValues: Array = joinPoint.args + + //记录并打印方法的信息 + val builder: StringBuilder = + getMethodLogInfo(className, methodName, parameterNames, parameterValues) + log(log.value, builder.toString()) + val section: String = builder.substring(2) + Trace.beginSection(section) + } + + /** + * 获取方法的日志信息 + * + * @param className 类名 + * @param methodName 方法名 + * @param parameterNames 方法参数名集合 + * @param parameterValues 方法参数值集合 + */ + private fun getMethodLogInfo(className: String, methodName: String, parameterNames: Array, parameterValues: Array): StringBuilder { + val builder: StringBuilder = StringBuilder("\u21E2 ") + builder.append(className) + .append(".") + .append(methodName) + .append('(') + for (i in parameterValues.indices) { + if (i > 0) { + builder.append(", ") + } + builder.append(parameterNames[i]).append('=') + builder.append(parameterValues[i].toString()) + } + builder.append(')') + if (Looper.myLooper() != Looper.getMainLooper()) { + builder.append(" [Thread:\"").append(Thread.currentThread().name).append("\"]") + } + return builder + } + + /** + * 方法执行完毕,切出 + * + * @param result 方法执行后的结果 + * @param lengthMillis 执行方法所需要的时间 + */ + private fun exitMethod(joinPoint: ProceedingJoinPoint, log: Log, result: Any?, lengthMillis: Long) { + Trace.endSection() + val signature: Signature = joinPoint.signature + val className: String? = signature.declaringType.name + val methodName: String? = signature.name + val builder: StringBuilder = StringBuilder("\u21E0 ") + .append(className) + .append(".") + .append(methodName) + .append(" [") + .append(lengthMillis) + .append("ms]") + + // 判断方法是否有返回值 + if (signature is MethodSignature && signature.returnType != Void.TYPE) { + builder.append(" = ") + builder.append(result.toString()) + } + log(log.value, builder.toString()) + } + + private fun log(tag: String?, msg: String?) { + Timber.tag(tag) + Timber.d(msg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/Permissions.kt b/app/src/main/java/com/hjq/demo/aop/Permissions.kt new file mode 100644 index 0000000..c4977ad --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/Permissions.kt @@ -0,0 +1,18 @@ +package com.hjq.demo.aop + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/06 + * desc : 权限申请注解 + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER) +annotation class Permissions constructor( + /** + * 需要申请权限的集合 + */ + vararg val value: String +) \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/PermissionsAspect.kt b/app/src/main/java/com/hjq/demo/aop/PermissionsAspect.kt new file mode 100644 index 0000000..918c1e0 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/PermissionsAspect.kt @@ -0,0 +1,72 @@ +package com.hjq.demo.aop + +import android.app.Activity +import com.hjq.demo.manager.* +import com.hjq.demo.other.PermissionCallback +import com.hjq.permissions.XXPermissions +import com.tencent.bugly.crashreport.CrashReport +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut +import timber.log.Timber + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/06 + * desc : 权限申请切面 + */ +@Suppress("unused") +@Aspect +class PermissionsAspect { + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.Permissions * *(..))") + fun method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("method() && @annotation(permissions)") + fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, permissions: Permissions) { + var activity: Activity? = null + + // 方法参数值集合 + val parameterValues: Array = joinPoint.args + for (arg: Any? in parameterValues) { + if (arg !is Activity) { + continue + } + activity = arg + break + } + if ((activity == null) || activity.isFinishing || activity.isDestroyed) { + activity = ActivityManager.getInstance().getTopActivity() + } + if ((activity == null) || activity.isFinishing || activity.isDestroyed) { + Timber.e("The activity has been destroyed and permission requests cannot be made") + return + } + requestPermissions(joinPoint, activity, permissions.value) + } + + private fun requestPermissions(joinPoint: ProceedingJoinPoint, activity: Activity, permissions: Array) { + XXPermissions.with(activity) + .permission(*permissions) + .request(object : PermissionCallback() { + override fun onGranted(permissions: MutableList?, all: Boolean) { + if (all) { + try { + // 获得权限,执行原方法 + joinPoint.proceed() + } catch (e: Throwable) { + CrashReport.postCatchedException(e) + } + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/SingleClick.kt b/app/src/main/java/com/hjq/demo/aop/SingleClick.kt new file mode 100644 index 0000000..f2134a2 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/SingleClick.kt @@ -0,0 +1,18 @@ +package com.hjq.demo.aop + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/06 + * desc : 防重复点击注解 + */ +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER) +annotation class SingleClick constructor( + /** + * 快速点击的间隔 + */ + val value: Long = 1000 +) \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/SingleClickAspect.kt b/app/src/main/java/com/hjq/demo/aop/SingleClickAspect.kt new file mode 100644 index 0000000..e0072db --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/SingleClickAspect.kt @@ -0,0 +1,69 @@ +package com.hjq.demo.aop + +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut +import org.aspectj.lang.reflect.CodeSignature +import timber.log.Timber + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/06 + * desc : 防重复点击切面 + */ +@Suppress("unused") +@Aspect +class SingleClickAspect { + + /** 最近一次点击的时间 */ + private var lastTime: Long = 0 + + /** 最近一次点击的标记 */ + private var lastTag: String? = null + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.SingleClick * *(..))") + fun method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("method() && @annotation(singleClick)") + @Throws(Throwable::class) + fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, singleClick: SingleClick) { + val codeSignature: CodeSignature = joinPoint.signature as CodeSignature + // 方法所在类 + val className: String = codeSignature.declaringType.name + // 方法名 + val methodName: String = codeSignature.name + // 构建方法 TAG + val builder: StringBuilder = StringBuilder("$className.$methodName") + builder.append("(") + val parameterValues: Array = joinPoint.args + for (i in parameterValues.indices) { + val arg: Any? = parameterValues[i] + if (i == 0) { + builder.append(arg) + } else { + builder.append(", ") + .append(arg) + } + } + builder.append(")") + val tag: String = builder.toString() + val currentTimeMillis: Long = System.currentTimeMillis() + if (currentTimeMillis - lastTime < singleClick.value && (tag == lastTag)) { + Timber.tag("SingleClick") + Timber.i("%s 毫秒内发生快速点击:%s", singleClick.value, tag) + return + } + lastTime = currentTimeMillis + lastTag = tag + // 执行原方法 + joinPoint.proceed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppActivity.kt b/app/src/main/java/com/hjq/demo/app/AppActivity.kt new file mode 100644 index 0000000..47c0e28 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppActivity.kt @@ -0,0 +1,201 @@ +package com.hjq.demo.app + +import android.content.Intent +import android.os.Bundle +import android.view.* +import androidx.annotation.StringRes +import com.gyf.immersionbar.ImmersionBar +import com.hjq.bar.TitleBar +import com.hjq.base.BaseActivity +import com.hjq.base.BaseDialog +import com.hjq.demo.R +import com.hjq.demo.action.TitleBarAction +import com.hjq.demo.action.ToastAction +import com.hjq.demo.http.model.HttpData +import com.hjq.demo.ui.dialog.WaitDialog +import com.hjq.http.listener.OnHttpListener +import okhttp3.Call + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : Activity 业务基类 + */ +abstract class AppActivity : BaseActivity(), + ToastAction, TitleBarAction, OnHttpListener { + + /** 标题栏对象 */ + private var titleBar: TitleBar? = null + + /** 状态栏沉浸 */ + private var immersionBar: ImmersionBar? = null + + /** 加载对话框 */ + private var dialog: BaseDialog? = null + + /** 对话框数量 */ + private var dialogCount: Int = 0 + + /** + * 当前加载对话框是否在显示中 + */ + open fun isShowDialog(): Boolean { + return dialog != null && dialog!!.isShowing + } + + /** + * 显示加载对话框 + */ + open fun showDialog() { + if (isFinishing || isDestroyed) { + return + } + dialogCount++ + postDelayed(Runnable { + if ((dialogCount <= 0) || isFinishing || isDestroyed) { + return@Runnable + } + if (dialog == null) { + dialog = WaitDialog.Builder(this) + .setCancelable(false) + .create() + } + if (!dialog!!.isShowing) { + dialog!!.show() + } + }, 300) + } + + /** + * 隐藏加载对话框 + */ + open fun hideDialog() { + if (isFinishing || isDestroyed) { + return + } + if (dialogCount > 0) { + dialogCount-- + } + if ((dialogCount != 0) || (dialog == null) || !dialog!!.isShowing) { + return + } + dialog?.dismiss() + } + + override fun initLayout() { + super.initLayout() + + val titleBar = getTitleBar() + titleBar?.setOnTitleBarListener(this) + + // 初始化沉浸式状态栏 + if (isStatusBarEnabled()) { + getStatusBarConfig().init() + + // 设置标题栏沉浸 + if (titleBar != null) { + ImmersionBar.setTitleBar(this, titleBar) + } + } + } + + /** + * 是否使用沉浸式状态栏 + */ + protected open fun isStatusBarEnabled(): Boolean { + return true + } + + /** + * 状态栏字体深色模式 + */ + open fun isStatusBarDarkFont(): Boolean { + return true + } + + /** + * 获取状态栏沉浸的配置对象 + */ + open fun getStatusBarConfig(): ImmersionBar { + if (immersionBar == null) { + immersionBar = createStatusBarConfig() + } + return immersionBar!! + } + + /** + * 初始化沉浸式状态栏 + */ + protected open fun createStatusBarConfig(): ImmersionBar { + return ImmersionBar.with(this) // 默认状态栏字体颜色为黑色 + .statusBarDarkFont(isStatusBarDarkFont()) // 指定导航栏背景颜色 + .navigationBarColor(R.color.white) // 状态栏字体和导航栏内容自动变色,必须指定状态栏颜色和导航栏颜色才可以自动变色 + .autoDarkModeEnable(true, 0.2f) + } + + /** + * 设置标题栏的标题 + */ + override fun setTitle(@StringRes id: Int) { + title = getString(id) + } + + /** + * 设置标题栏的标题 + */ + override fun setTitle(title: CharSequence?) { + super.setTitle(title) + getTitleBar()?.title = title + } + + override fun getTitleBar(): TitleBar? { + if (titleBar == null) { + titleBar = obtainTitleBar(getContentView()) + } + return titleBar + } + + override fun onLeftClick(view: View) { + onBackPressed() + } + + override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) { + super.startActivityForResult(intent, requestCode, options) + overridePendingTransition(R.anim.right_in_activity, R.anim.right_out_activity) + } + + override fun finish() { + super.finish() + overridePendingTransition(R.anim.left_in_activity, R.anim.left_out_activity) + } + + /** + * [OnHttpListener] + */ + override fun onStart(call: Call) { + showDialog() + } + + override fun onSucceed(result: Any) { + if (result is HttpData<*>) { + toast(result.getMessage()) + } + } + + override fun onFail(e: Exception) { + toast(e.message) + } + + override fun onEnd(call: Call) { + hideDialog() + } + + override fun onDestroy() { + super.onDestroy() + if (isShowDialog()) { + hideDialog() + } + dialog = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppAdapter.kt b/app/src/main/java/com/hjq/demo/app/AppAdapter.kt new file mode 100644 index 0000000..50bd57f --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppAdapter.kt @@ -0,0 +1,201 @@ +package com.hjq.demo.app + +import android.content.Context +import android.view.View +import androidx.annotation.IntRange +import androidx.annotation.LayoutRes +import com.hjq.base.BaseAdapter +import java.util.* + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/12/19 + * desc : RecyclerView 适配器业务基类 + */ +abstract class AppAdapter constructor(context: Context) : + BaseAdapter.AppViewHolder>(context) { + + /** 列表数据 */ + private var dataSet: MutableList = ArrayList() + + /** 当前列表的页码,默认为第一页,用于分页加载功能 */ + private var pageNumber = 1 + + /** 是否是最后一页,默认为false,用于分页加载功能 */ + private var lastPage = false + + /** 标记对象 */ + private var tag: Any? = null + + override fun getItemCount(): Int { + return getCount() + } + + /** + * 获取数据总数 + */ + open fun getCount(): Int { + return dataSet.size + } + + /** + * 设置新的数据 + */ + open fun setData(data: MutableList?) { + if (data == null) { + dataSet.clear() + } else { + dataSet = data + } + notifyDataSetChanged() + } + + /** + * 获取当前数据 + */ + open fun getData(): MutableList { + return dataSet + } + + /** + * 追加一些数据 + */ + open fun addData(data: MutableList?) { + if (data == null || data.isEmpty()) { + return + } + dataSet.addAll(data) + notifyItemRangeInserted(dataSet.size - data.size, data.size) + } + + /** + * 清空当前数据 + */ + open fun clearData() { + dataSet.clear() + notifyDataSetChanged() + } + + /** + * 是否包含了某个位置上的条目数据 + */ + open fun containsItem(@IntRange(from = 0) position: Int): Boolean { + return containsItem(getItem(position)) + } + + /** + * 是否包含某个条目数据 + */ + open fun containsItem(item: T?): Boolean { + return if (item == null) { + false + } else dataSet.contains(item) + } + + /** + * 获取某个位置上的数据 + */ + open fun getItem(@IntRange(from = 0) position: Int): T { + return dataSet[position] + } + + /** + * 更新某个位置上的数据 + */ + open fun setItem(@IntRange(from = 0) position: Int, item: T) { + dataSet[position] = item + notifyItemChanged(position) + } + + /** + * 添加单条数据 + */ + open fun addItem(item: T) { + addItem(dataSet.size, item) + } + + open fun addItem(@IntRange(from = 0) position: Int, item: T) { + var finalPosition = position + if (finalPosition < dataSet.size) { + dataSet.add(finalPosition, item) + } else { + dataSet.add(item) + finalPosition = dataSet.size - 1 + } + notifyItemInserted(finalPosition) + } + + /** + * 删除单条数据 + */ + open fun removeItem(item: T) { + val index = dataSet.indexOf(item) + if (index != -1) { + removeItem(index) + } + } + + open fun removeItem(@IntRange(from = 0) position: Int) { + dataSet.removeAt(position) + notifyItemRemoved(position) + } + + /** + * 获取当前的页码 + */ + open fun getPageNumber(): Int { + return pageNumber + } + + /** + * 设置当前的页码 + */ + open fun setPageNumber(@IntRange(from = 0) number: Int) { + pageNumber = number + } + + /** + * 当前是否为最后一页 + */ + open fun isLastPage(): Boolean { + return lastPage + } + + /** + * 设置是否为最后一页 + */ + open fun setLastPage(last: Boolean) { + lastPage = last + } + + /** + * 获取标记 + */ + open fun getTag(): Any? { + return tag + } + + /** + * 设置标记 + */ + open fun setTag(tag: Any) { + this.tag = tag + } + + abstract inner class AppViewHolder : BaseViewHolder { + + constructor(@LayoutRes id: Int) : super(id) + + constructor(itemView: View) : super(itemView) + } + + inner class SimpleViewHolder : AppViewHolder { + + constructor(@LayoutRes id: Int) : super(id) + + constructor(itemView: View) : super(itemView) + + override fun onBindView(position: Int) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppApplication.kt b/app/src/main/java/com/hjq/demo/app/AppApplication.kt new file mode 100644 index 0000000..499f866 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppApplication.kt @@ -0,0 +1,167 @@ +package com.hjq.demo.app + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.os.Build +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonToken +import com.hjq.bar.TitleBar +import com.hjq.demo.R +import com.hjq.demo.aop.Log +import com.hjq.demo.http.glide.GlideApp +import com.hjq.demo.http.model.RequestHandler +import com.hjq.demo.http.model.RequestServer +import com.hjq.demo.manager.ActivityManager +import com.hjq.demo.other.* +import com.hjq.gson.factory.GsonFactory +import com.hjq.http.EasyConfig +import com.hjq.http.config.IRequestApi +import com.hjq.http.model.HttpHeaders +import com.hjq.http.model.HttpParams +import com.hjq.toast.ToastUtils +import com.hjq.umeng.UmengClient +import com.scwang.smart.refresh.layout.SmartRefreshLayout +import com.scwang.smart.refresh.layout.api.RefreshLayout +import com.tencent.bugly.crashreport.CrashReport +import com.tencent.mmkv.MMKV +import okhttp3.OkHttpClient +import timber.log.Timber + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : 应用入口 + */ +class AppApplication : Application() { + + @Log("启动耗时") + override fun onCreate() { + super.onCreate() + initSdk(this) + } + + override fun onLowMemory() { + super.onLowMemory() + // 清理所有图片内存缓存 + GlideApp.get(this).onLowMemory() + } + + override fun onTrimMemory(level: Int) { + super.onTrimMemory(level) + // 根据手机内存剩余情况清理图片内存缓存 + GlideApp.get(this).onTrimMemory(level) + } + + companion object { + + /** + * 初始化一些第三方框架 + */ + fun initSdk(application: Application) { + // 设置标题栏初始化器 + TitleBar.setDefaultStyle(TitleBarStyle()) + + // 设置全局的 Header 构建器 + SmartRefreshLayout.setDefaultRefreshHeaderCreator{ context: Context, layout: RefreshLayout -> + MaterialHeader(context).setColorSchemeColors(ContextCompat.getColor(context, R.color.common_accent_color)) + } + // 设置全局的 Footer 构建器 + SmartRefreshLayout.setDefaultRefreshFooterCreator{ context: Context, layout: RefreshLayout -> + SmartBallPulseFooter(context) + } + // 设置全局初始化器 + SmartRefreshLayout.setDefaultRefreshInitializer { context: Context, layout: RefreshLayout -> + // 刷新头部是否跟随内容偏移 + layout.setEnableHeaderTranslationContent(true) + // 刷新尾部是否跟随内容偏移 + .setEnableFooterTranslationContent(true) + // 加载更多是否跟随内容偏移 + .setEnableFooterFollowWhenNoMoreData(true) + // 内容不满一页时是否可以上拉加载更多 + .setEnableLoadMoreWhenContentNotFull(false) + // 仿苹果越界效果开关 + .setEnableOverScrollDrag(false) + } + + // 初始化吐司 + ToastUtils.init(application, ToastStyle()) + // 设置调试模式 + ToastUtils.setDebugMode(AppConfig.isDebug()) + // 设置 Toast 拦截器 + ToastUtils.setInterceptor(ToastLogInterceptor()) + + // 本地异常捕捉 + CrashHandler.register(application) + + // 友盟统计、登录、分享 SDK + UmengClient.init(application, AppConfig.isLogEnable()) + + // Bugly 异常捕捉 + CrashReport.initCrashReport(application, AppConfig.getBuglyId(), AppConfig.isDebug()) + + // Activity 栈管理初始化 + ActivityManager.getInstance().init(application) + + // MMKV 初始化 + MMKV.initialize(application) + + // 网络请求框架初始化 + val okHttpClient: OkHttpClient = OkHttpClient.Builder() + .build() + + EasyConfig.with(okHttpClient) + // 是否打印日志 + .setLogEnabled(AppConfig.isLogEnable()) + // 设置服务器配置 + .setServer(RequestServer()) + // 设置请求处理策略 + .setHandler(RequestHandler(application)) + // 设置请求重试次数 + .setRetryCount(1) + .setInterceptor { api: IRequestApi, params: HttpParams, headers: HttpHeaders -> + // 添加全局请求头 + headers.put("token", "66666666666") + headers.put("deviceOaid", UmengClient.getDeviceOaid()) + headers.put("versionName", AppConfig.getVersionName()) + headers.put("versionCode", AppConfig.getVersionCode().toString()) + } + .into() + + // 设置 Json 解析容错监听 + GsonFactory.setJsonCallback { typeToken: TypeToken<*>, fieldName: String?, jsonToken: JsonToken -> + // 上报到 Bugly 错误列表 + CrashReport.postCatchedException(IllegalArgumentException("类型解析异常:$typeToken#$fieldName,后台返回的类型为:$jsonToken")) + } + + // 初始化日志打印 + if (AppConfig.isLogEnable()) { + Timber.plant(DebugLoggerTree()) + } + + // 注册网络状态变化监听 + val connectivityManager: ConnectivityManager? = ContextCompat.getSystemService(application, ConnectivityManager::class.java) + if (connectivityManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onLost(network: Network) { + val topActivity: Activity? = ActivityManager.getInstance().getTopActivity() + if (topActivity !is LifecycleOwner) { + return + } + val lifecycleOwner: LifecycleOwner = topActivity + if (lifecycleOwner.lifecycle.currentState != Lifecycle.State.RESUMED) { + return + } + ToastUtils.show(R.string.common_network_error) + } + }) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppFragment.kt b/app/src/main/java/com/hjq/demo/app/AppFragment.kt new file mode 100644 index 0000000..f8a3f2a --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppFragment.kt @@ -0,0 +1,61 @@ +package com.hjq.demo.app + +import com.hjq.base.BaseFragment +import com.hjq.demo.action.ToastAction +import com.hjq.demo.http.model.HttpData +import com.hjq.http.listener.OnHttpListener +import okhttp3.Call + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : Fragment 业务基类 + */ +abstract class AppFragment : BaseFragment(), + ToastAction, OnHttpListener { + + /** + * 当前加载对话框是否在显示中 + */ + open fun isShowDialog(): Boolean { + val activity: A = getAttachActivity() ?: return false + return activity.isShowDialog() + } + + /** + * 显示加载对话框 + */ + open fun showDialog() { + getAttachActivity()?.showDialog() + } + + /** + * 隐藏加载对话框 + */ + open fun hideDialog() { + getAttachActivity()?.hideDialog() + } + + /** + * [OnHttpListener] + */ + override fun onStart(call: Call) { + showDialog() + } + + override fun onSucceed(result: Any) { + if (result !is HttpData<*>) { + return + } + toast(result.getMessage()) + } + + override fun onFail(e: Exception) { + toast(e.message) + } + + override fun onEnd(call: Call) { + hideDialog() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/TitleBarFragment.kt b/app/src/main/java/com/hjq/demo/app/TitleBarFragment.kt new file mode 100644 index 0000000..1b1726d --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/TitleBarFragment.kt @@ -0,0 +1,93 @@ +package com.hjq.demo.app + +import android.os.Bundle +import android.view.* +import com.gyf.immersionbar.ImmersionBar +import com.hjq.bar.TitleBar +import com.hjq.demo.R +import com.hjq.demo.action.TitleBarAction + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/10/31 + * desc : 带标题栏的 Fragment 业务基类 + */ +abstract class TitleBarFragment : AppFragment(), TitleBarAction { + + /** 标题栏对象 */ + private var titleBar: TitleBar? = null + + /** 状态栏沉浸 */ + private var immersionBar: ImmersionBar? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val titleBar = getTitleBar() + // 设置标题栏点击监听 + titleBar?.setOnTitleBarListener(this) + + if (isStatusBarEnabled()) { + // 初始化沉浸式状态栏 + getStatusBarConfig().init() + if (titleBar != null) { + // 设置标题栏沉浸 + ImmersionBar.setTitleBar(this, titleBar) + } + } + } + + override fun onResume() { + super.onResume() + if (isStatusBarEnabled()) { + // 重新初始化状态栏 + getStatusBarConfig().init() + } + } + + /** + * 是否在 Fragment 使用沉浸式 + */ + open fun isStatusBarEnabled(): Boolean { + return false + } + + /** + * 获取状态栏沉浸的配置对象 + */ + protected fun getStatusBarConfig(): ImmersionBar { + if (immersionBar == null) { + immersionBar = createStatusBarConfig() + } + return immersionBar!! + } + + /** + * 初始化沉浸式 + */ + protected fun createStatusBarConfig(): ImmersionBar { + return ImmersionBar.with(this) + // 默认状态栏字体颜色为黑色 + .statusBarDarkFont(isStatusBarDarkFont()) + // 指定导航栏背景颜色 + .navigationBarColor(R.color.white) + // 状态栏字体和导航栏内容自动变色,必须指定状态栏颜色和导航栏颜色才可以自动变色 + .autoDarkModeEnable(true, 0.2f) + } + + /** + * 获取状态栏字体颜色 + */ + protected open fun isStatusBarDarkFont(): Boolean { + // 返回真表示黑色字体 + return getAttachActivity()!!.isStatusBarDarkFont() + } + + override fun getTitleBar(): TitleBar? { + if (titleBar == null || !isLoading()) { + titleBar = obtainTitleBar(view as ViewGroup) + } + return titleBar + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/CopyApi.kt b/app/src/main/java/com/hjq/demo/http/api/CopyApi.kt new file mode 100644 index 0000000..97d7943 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/CopyApi.kt @@ -0,0 +1,20 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 可进行拷贝的副本 + */ +class CopyApi : IRequestApi { + + override fun getApi(): String { + return "" + } + + class Bean { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.kt b/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.kt new file mode 100644 index 0000000..dc86c80 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.kt @@ -0,0 +1,23 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 获取验证码 + */ +class GetCodeApi : IRequestApi { + + override fun getApi(): String { + return "code/get" + } + + /** 手机号 */ + private var phone: String? = null + + fun setPhone(phone: String?): GetCodeApi = apply { + this.phone = phone + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/LoginApi.kt b/app/src/main/java/com/hjq/demo/http/api/LoginApi.kt new file mode 100644 index 0000000..9a240e3 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/LoginApi.kt @@ -0,0 +1,39 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 用户登录 + */ +class LoginApi : IRequestApi { + + override fun getApi(): String { + return "user/login" + } + + /** 手机号 */ + private var phone: String? = null + + /** 登录密码 */ + private var password: String? = null + + fun setPhone(phone: String?): LoginApi = apply { + this.phone = phone + } + + fun setPassword(password: String?): LoginApi = apply { + this.password = password + } + + class Bean { + + private val token: String? = null + + fun getToken(): String? { + return token + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/LogoutApi.kt b/app/src/main/java/com/hjq/demo/http/api/LogoutApi.kt new file mode 100644 index 0000000..9041d68 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/LogoutApi.kt @@ -0,0 +1,16 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 退出登录 + */ +class LogoutApi : IRequestApi { + + override fun getApi(): String { + return "user/logout" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/PasswordApi.kt b/app/src/main/java/com/hjq/demo/http/api/PasswordApi.kt new file mode 100644 index 0000000..34b0058 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/PasswordApi.kt @@ -0,0 +1,37 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 修改密码 + */ +class PasswordApi : IRequestApi { + + override fun getApi(): String { + return "user/password" + } + + /** 手机号(已登录可不传) */ + private var phone: String? = null + + /** 验证码 */ + private var code: String? = null + + /** 密码 */ + private var password: String? = null + + fun setPhone(phone: String?): PasswordApi = apply { + this.phone = phone + } + + fun setCode(code: String?): PasswordApi = apply { + this.code = code + } + + fun setPassword(password: String?): PasswordApi = apply { + this.password = password + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/PhoneApi.kt b/app/src/main/java/com/hjq/demo/http/api/PhoneApi.kt new file mode 100644 index 0000000..0361864 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/PhoneApi.kt @@ -0,0 +1,37 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 修改手机 + */ +class PhoneApi : IRequestApi { + + override fun getApi(): String { + return "user/phone" + } + + /** 旧手机号验证码(没有绑定情况下可不传) */ + private var preCode: String? = null + + /** 新手机号 */ + private var phone: String? = null + + /** 新手机号验证码 */ + private var code: String? = null + + fun setPreCode(preCode: String?): PhoneApi = apply { + this.preCode = preCode + } + + fun setPhone(phone: String?): PhoneApi = apply { + this.phone = phone + } + + fun setCode(code: String?): PhoneApi = apply { + this.code = code + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/RegisterApi.kt b/app/src/main/java/com/hjq/demo/http/api/RegisterApi.kt new file mode 100644 index 0000000..8c8cb56 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/RegisterApi.kt @@ -0,0 +1,41 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 用户注册 + */ +class RegisterApi : IRequestApi { + + override fun getApi(): String { + return "user/register" + } + + /** 手机号 */ + private var phone: String? = null + + /** 验证码 */ + private var code: String? = null + + /** 密码 */ + private var password: String? = null + + fun setPhone(phone: String?): RegisterApi = apply { + this.phone = phone + } + + fun setCode(code: String?): RegisterApi = apply { + this.code = code + } + + fun setPassword(password: String?): RegisterApi = apply { + this.password = password + } + + class Bean { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.kt b/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.kt new file mode 100644 index 0000000..c7fdca1 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.kt @@ -0,0 +1,24 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi +import java.io.File + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 上传图片 + */ +class UpdateImageApi : IRequestApi { + + override fun getApi(): String { + return "update/image" + } + + /** 图片文件 */ + private var image: File? = null + + fun setImage(image: File?): UpdateImageApi = apply { + this.image = image + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.kt b/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.kt new file mode 100644 index 0000000..f454b28 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.kt @@ -0,0 +1,20 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 获取用户信息 + */ +class UserInfoApi : IRequestApi { + + override fun getApi(): String { + return "user/info" + } + + class Bean { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.kt b/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.kt new file mode 100644 index 0000000..51e83bb --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.kt @@ -0,0 +1,30 @@ +package com.hjq.demo.http.api + +import com.hjq.http.config.IRequestApi + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 验证码校验 + */ +class VerifyCodeApi : IRequestApi { + + override fun getApi(): String { + return "code/checkout" + } + + /** 手机号 */ + private var phone: String? = null + + /** 验证码 */ + private var code: String? = null + + fun setPhone(phone: String?): VerifyCodeApi = apply { + this.phone = phone + } + + fun setCode(code: String?): VerifyCodeApi = apply { + this.code = code + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.kt b/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.kt new file mode 100644 index 0000000..bf8f081 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.kt @@ -0,0 +1,75 @@ +package com.hjq.demo.http.glide + +import android.content.Context +import com.bumptech.glide.Glide +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool +import com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper +import com.bumptech.glide.load.engine.cache.LruResourceCache +import com.bumptech.glide.load.engine.cache.MemorySizeCalculator +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.module.AppGlideModule +import com.bumptech.glide.request.RequestOptions +import com.hjq.demo.R +import com.hjq.http.EasyConfig +import java.io.File +import java.io.InputStream + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/15 + * desc : Glide 全局配置 + */ +@GlideModule +class GlideConfig : AppGlideModule() { + + companion object { + + /** 本地图片缓存文件最大值 */ + private const val IMAGE_DISK_CACHE_MAX_SIZE: Int = 500 * 1024 * 1024 + } + + override fun applyOptions(context: Context, builder: GlideBuilder) { + // 读写外部缓存目录不需要申请存储权限 + val diskCacheFile = File(context.cacheDir, "glide") + // 如果这个路径是一个文件 + if (diskCacheFile.exists() && diskCacheFile.isFile) { + // 执行删除操作 + diskCacheFile.delete() + } + // 如果这个路径不存在 + if (!diskCacheFile.exists()) { + // 创建多级目录 + diskCacheFile.mkdirs() + } + builder.setDiskCache { + DiskLruCacheWrapper.create(diskCacheFile, IMAGE_DISK_CACHE_MAX_SIZE.toLong()) + } + val calculator: MemorySizeCalculator = MemorySizeCalculator.Builder(context).build() + val defaultMemoryCacheSize: Int = calculator.memoryCacheSize + val defaultBitmapPoolSize: Int = calculator.bitmapPoolSize + val customMemoryCacheSize: Long = (1.2 * defaultMemoryCacheSize).toLong() + val customBitmapPoolSize: Long = (1.2 * defaultBitmapPoolSize).toLong() + builder.setMemoryCache(LruResourceCache(customMemoryCacheSize)) + builder.setBitmapPool(LruBitmapPool(customBitmapPoolSize)) + builder.setDefaultRequestOptions( + RequestOptions() + // 设置默认加载中占位图 + .placeholder(R.drawable.image_loading_ic) + // 设置默认加载出错占位图 + .error(R.drawable.image_error_ic) + ) + } + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + // Glide 默认使用的是 HttpURLConnection 来做网络请求,这里切换成更高效的 OkHttp + registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpLoader.Factory(EasyConfig.getInstance().client)) + } + + override fun isManifestParsingEnabled(): Boolean { + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.kt b/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.kt new file mode 100644 index 0000000..ec16e91 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.kt @@ -0,0 +1,81 @@ +package com.hjq.demo.http.glide + +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.HttpException +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.util.ContentLengthInputStream +import okhttp3.* +import java.io.IOException +import java.io.InputStream + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/15 + * desc : OkHttp 加载器 + */ +class OkHttpFetcher internal constructor( + private val callFactory: Call.Factory, + private val glideUrl: GlideUrl) : + DataFetcher, Callback { + + private var inputStream: InputStream? = null + private var responseBody: ResponseBody? = null + private var dataCallback: DataFetcher.DataCallback? = null + + @Volatile + private var call: Call? = null + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + val requestBuilder: Request.Builder = Request.Builder().url(glideUrl.toStringUrl()) + for (headerEntry: MutableMap.MutableEntry in glideUrl.headers.entries) { + val key: String = headerEntry.key + requestBuilder.addHeader(key, headerEntry.value) + } + val request: Request = requestBuilder.build() + dataCallback = callback + call = callFactory.newCall(request) + call?.enqueue(this) + } + + override fun onFailure(call: Call, e: IOException) { + dataCallback?.onLoadFailed(e) + } + + override fun onResponse(call: Call, response: Response) { + responseBody = response.body() + if (response.isSuccessful) { + responseBody?.let { + val contentLength: Long = it.contentLength() + inputStream = ContentLengthInputStream.obtain(it.byteStream(), contentLength) + } + dataCallback?.onDataReady(inputStream) + } else { + dataCallback?.onLoadFailed(HttpException(response.message(), response.code())) + } + } + + override fun cleanup() { + try { + inputStream?.close() + } catch (e: IOException) { + e.printStackTrace() + } + responseBody?.close() + dataCallback = null + } + + override fun cancel() { + call?.cancel() + } + + override fun getDataClass(): Class { + return InputStream::class.java + } + + override fun getDataSource(): DataSource { + return DataSource.REMOTE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.kt b/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.kt new file mode 100644 index 0000000..ca4654d --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.kt @@ -0,0 +1,37 @@ +package com.hjq.demo.http.glide + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoader.LoadData +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import okhttp3.Call +import java.io.InputStream + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/15 + * desc : OkHttp 加载模型 + */ +class OkHttpLoader private constructor(private val factory: Call.Factory) : + ModelLoader { + + override fun handles(url: GlideUrl): Boolean { + return true + } + + override fun buildLoadData(model: GlideUrl, width: Int, height: Int, options: Options): LoadData { + return LoadData(model, OkHttpFetcher(factory, model)) + } + + class Factory constructor(private val factory: Call.Factory) : ModelLoaderFactory { + + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return OkHttpLoader(factory) + } + + override fun teardown() {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpData.kt b/app/src/main/java/com/hjq/demo/http/model/HttpData.kt new file mode 100644 index 0000000..0e2195b --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpData.kt @@ -0,0 +1,45 @@ +package com.hjq.demo.http.model + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 统一接口数据结构 + */ +open class HttpData { + + /** 返回码 */ + private val code: Int = 0 + + /** 提示语 */ + private val msg: String? = null + + /** 数据 */ + private val data: T? = null + + fun getCode(): Int { + return code + } + + fun getMessage(): String? { + return msg + } + + fun getData(): T? { + return data + } + + /** + * 是否请求成功 + */ + fun isRequestSucceed(): Boolean { + return code == 200 + } + + /** + * 是否 Token 失效 + */ + fun isTokenFailure(): Boolean { + return code == 1001 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpListData.kt b/app/src/main/java/com/hjq/demo/http/model/HttpListData.kt new file mode 100644 index 0000000..6edf78d --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpListData.kt @@ -0,0 +1,51 @@ +package com.hjq.demo.http.model + +import com.hjq.demo.http.model.HttpListData.ListBean +import kotlin.math.ceil + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/10/07 + * desc : 统一接口列表数据结构 + */ +class HttpListData : HttpData?>() { + + class ListBean { + + /** 当前页码 */ + private val pageIndex: Int = 0 + + /** 页大小 */ + private val pageSize: Int = 0 + + /** 总数量 */ + private val totalNumber: Int = 0 + + /** 数据 */ + private val items: MutableList? = null + + /** + * 判断是否是最后一页 + */ + fun isLastPage(): Boolean { + return ceil((totalNumber.toFloat() / pageSize.toFloat())) <= pageIndex + } + + fun getTotalNumber(): Int { + return totalNumber + } + + fun getPageIndex(): Int { + return pageIndex + } + + fun getPageSize(): Int { + return pageSize + } + + fun getItems(): MutableList? { + return items + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/RequestHandler.kt b/app/src/main/java/com/hjq/demo/http/model/RequestHandler.kt new file mode 100644 index 0000000..0485296 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/RequestHandler.kt @@ -0,0 +1,175 @@ +package com.hjq.demo.http.model + +import android.app.* +import android.content.* +import android.net.ConnectivityManager +import android.net.NetworkInfo +import androidx.lifecycle.LifecycleOwner +import com.google.gson.JsonSyntaxException +import com.hjq.demo.R +import com.hjq.demo.manager.ActivityManager +import com.hjq.demo.ui.activity.LoginActivity +import com.hjq.gson.factory.GsonFactory +import com.hjq.http.EasyLog +import com.hjq.http.config.IRequestApi +import com.hjq.http.config.IRequestHandler +import com.hjq.http.exception.* +import com.tencent.mmkv.MMKV +import okhttp3.Headers +import okhttp3.Response +import okhttp3.ResponseBody +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.io.InputStream +import java.lang.reflect.Type +import java.net.SocketTimeoutException +import java.net.UnknownHostException + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/07 + * desc : 请求处理类 + */ +class RequestHandler constructor(private val application: Application) : IRequestHandler { + + private val mmkv: MMKV = MMKV.mmkvWithID("http_cache_id") + + @Throws(Exception::class) + override fun requestSucceed(lifecycle: LifecycleOwner, api: IRequestApi, response: Response, type: Type): Any? { + if ((Response::class.java == type)) { + return response + } + if (!response.isSuccessful) { + // 返回响应异常 + throw ResponseException( + application.getString(R.string.http_response_error) + ",responseCode:" + response.code() + ",message:" + response.message(), + response + ) + } + if ((Headers::class.java == type)) { + return response.headers() + } + val body: ResponseBody = response.body() ?: return null + if ((InputStream::class.java == type)) { + return body.byteStream() + } + + val text: String + try { + text = body.string() + } catch (e: IOException) { + // 返回结果读取异常 + throw DataException(application.getString(R.string.http_data_explain_error), e) + } + + // 打印这个 Json 或者文本 + EasyLog.json(text) + if ((String::class.java == type)) { + return text + } + + if ((JSONObject::class.java == type)) { + try { + // 如果这是一个 JSONObject 对象 + return JSONObject(text) + } catch (e: JSONException) { + throw DataException(application.getString(R.string.http_data_explain_error), e) + } + } + + if ((JSONArray::class.java == type)) { + try { + // 如果这是一个 JSONArray 对象 + return JSONArray(text) + } catch (e: JSONException) { + throw DataException(application.getString(R.string.http_data_explain_error), e) + } + } + + val result: Any? + try { + result = GsonFactory.getSingletonGson().fromJson(text, type) + } catch (e: JsonSyntaxException) { + // 返回结果读取异常 + throw DataException(application.getString(R.string.http_data_explain_error), e) + } + + if (result is HttpData<*>) { + val model: HttpData<*> = result + if (model.isRequestSucceed()) { + // 代表执行成功 + return result + } + if (model.isTokenFailure()) { + // 代表登录失效,需要重新登录 + throw TokenException(application.getString(R.string.http_token_error)) + } + throw ResultException(model.getMessage(), model) + } + return result + } + + override fun requestFail(lifecycle: LifecycleOwner, api: IRequestApi, e: Exception): Exception { + // 判断这个异常是不是自己抛的 + if (e is HttpException) { + if (e is TokenException) { + // 登录信息失效,跳转到登录页 + val application: Application = ActivityManager.getInstance().getApplication() + val intent = Intent(application, LoginActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + application.startActivity(intent) + // 销毁除了登录页之外的 Activity + ActivityManager.getInstance().finishAllActivities(LoginActivity::class.java) + } + return e + } + if (e is SocketTimeoutException) { + return TimeoutException(application.getString(R.string.http_server_out_time), e) + } + if (e is UnknownHostException) { + val info: NetworkInfo? = (application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo + // 判断网络是否连接 + if (info == null || !info.isConnected) { + // 没有连接就是网络异常 + return NetworkException(application.getString(R.string.http_network_error), e) + } + + // 有连接就是服务器的问题 + return ServerException(application.getString(R.string.http_server_error), e) + } + if (e is IOException) { + //e = new CancelException(context.getString(R.string.http_request_cancel), e); + return CancelException("", e) + } + return HttpException(e.message, e) + } + + override fun readCache(lifecycle: LifecycleOwner, api: IRequestApi, type: Type): Any? { + val cacheKey: String? = GsonFactory.getSingletonGson().toJson(api) + val cacheValue: String? = mmkv.getString(cacheKey, null) + if ((cacheValue == null) || ("" == cacheValue) || ("{}" == cacheValue)) { + return null + } + EasyLog.print("---------- cacheKey ----------") + EasyLog.json(cacheKey) + EasyLog.print("---------- cacheValue ----------") + EasyLog.json(cacheValue) + return GsonFactory.getSingletonGson().fromJson(cacheValue, type) + } + + override fun writeCache(lifecycle: LifecycleOwner, api: IRequestApi, response: Response, result: Any?): Boolean { + val cacheKey: String? = GsonFactory.getSingletonGson().toJson(api) + val cacheValue: String? = GsonFactory.getSingletonGson().toJson(result) + if ((cacheValue == null) || ("" == cacheValue) || ("{}" == cacheValue)) { + return false + } + EasyLog.print("---------- cacheKey ----------") + EasyLog.json(cacheKey) + EasyLog.print("---------- cacheValue ----------") + EasyLog.json(cacheValue) + return mmkv.putString(cacheKey, cacheValue).commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/RequestServer.kt b/app/src/main/java/com/hjq/demo/http/model/RequestServer.kt new file mode 100644 index 0000000..58bcea5 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/RequestServer.kt @@ -0,0 +1,27 @@ +package com.hjq.demo.http.model + +import com.hjq.demo.other.AppConfig +import com.hjq.http.config.IRequestServer +import com.hjq.http.model.BodyType + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/10/02 + * desc : 服务器配置 + */ +class RequestServer : IRequestServer { + + override fun getHost(): String { + return AppConfig.getHostUrl() + } + + override fun getPath(): String { + return "api/" + } + + override fun getType(): BodyType { + // 以表单的形式提交参数 + return BodyType.FORM + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/ActivityManager.kt b/app/src/main/java/com/hjq/demo/manager/ActivityManager.kt new file mode 100644 index 0000000..2bd9f3f --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/ActivityManager.kt @@ -0,0 +1,252 @@ +package com.hjq.demo.manager + +import android.app.Activity +import android.app.Application +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import androidx.collection.ArrayMap +import timber.log.Timber +import java.util.* + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/11/18 + * desc : Activity 管理类 + */ +class ActivityManager private constructor() : ActivityLifecycleCallbacks { + + companion object { + + @Suppress("StaticFieldLeak") + @Volatile + private var sInstance: ActivityManager? = null + + fun getInstance(): ActivityManager { + if (sInstance == null) { + synchronized(ActivityManager::class.java) { + if (sInstance == null) { + sInstance = ActivityManager() + } + } + } + return sInstance!! + } + + /** + * 获取一个对象的独立无二的标记 + */ + private fun getObjectTag(`object`: Any): String { + // 对象所在的包名 + 对象的内存地址 + return `object`.javaClass.name + Integer.toHexString(`object`.hashCode()) + } + } + + /** Activity 存放集合 */ + private val activitySet: ArrayMap = ArrayMap() + + /** 应用生命周期回调 */ + private val lifecycleCallbacks: ArrayList = ArrayList() + + /** 当前应用上下文对象 */ + private lateinit var application: Application + + /** 栈顶的 Activity 对象 */ + private var topActivity: Activity? = null + + /** 前台并且可见的 Activity 对象 */ + private var resumedActivity: Activity? = null + + fun init(application: Application) { + this.application = application + this.application.registerActivityLifecycleCallbacks(this) + } + + /** + * 获取 Application 对象 + */ + fun getApplication(): Application { + return application + } + + /** + * 获取栈顶的 Activity + */ + fun getTopActivity(): Activity? { + return topActivity + } + + /** + * 获取前台并且可见的 Activity + */ + fun getResumedActivity(): Activity? { + return resumedActivity + } + + /** + * 判断当前应用是否处于前台状态 + */ + fun isForeground(): Boolean { + return getResumedActivity() != null + } + + /** + * 注册应用生命周期回调 + */ + fun registerApplicationLifecycleCallback(callback: ApplicationLifecycleCallback) { + lifecycleCallbacks.add(callback) + } + + /** + * 取消注册应用生命周期回调 + */ + fun unregisterApplicationLifecycleCallback(callback: ApplicationLifecycleCallback) { + lifecycleCallbacks.remove(callback) + } + + /** + * 销毁指定的 Activity + */ + fun finishActivity(clazz: Class?) { + if (clazz == null) { + return + } + val keys: Array = activitySet.keys.toTypedArray() + for (key: String? in keys) { + val activity: Activity? = activitySet[key] + if (activity == null || activity.isFinishing) { + continue + } + if ((activity.javaClass == clazz)) { + activity.finish() + activitySet.remove(key) + break + } + } + } + + /** + * 销毁所有的 Activity + */ + fun finishAllActivities() { + finishAllActivities(null as Class?) + } + + /** + * 销毁所有的 Activity + * + * @param classArray 白名单 Activity + */ + @SafeVarargs + fun finishAllActivities(vararg classArray: Class?) { + val keys: Array = activitySet.keys.toTypedArray() + for (key: String? in keys) { + val activity: Activity? = activitySet[key] + if (activity == null || activity.isFinishing) { + continue + } + var whiteClazz = false + for (clazz: Class? in classArray) { + if ((activity.javaClass == clazz)) { + whiteClazz = true + } + } + if (whiteClazz) { + continue + } + + // 如果不是白名单上面的 Activity 就销毁掉 + activity.finish() + activitySet.remove(key) + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + Timber.i("%s - onCreate", activity.javaClass.simpleName) + if (activitySet.size == 0) { + for (callback: ApplicationLifecycleCallback? in lifecycleCallbacks) { + callback?.onApplicationCreate(activity) + } + Timber.i("%s - onApplicationCreate", activity.javaClass.simpleName) + } + activitySet[getObjectTag(activity)] = activity + topActivity = activity + } + + override fun onActivityStarted(activity: Activity) { + Timber.i("%s - onStart", activity.javaClass.simpleName) + } + + override fun onActivityResumed(activity: Activity) { + Timber.i("%s - onResume", activity.javaClass.simpleName) + if (topActivity === activity && resumedActivity == null) { + for (callback: ApplicationLifecycleCallback in lifecycleCallbacks) { + callback.onApplicationForeground(activity) + } + Timber.i("%s - onApplicationForeground", activity.javaClass.simpleName) + } + topActivity = activity + resumedActivity = activity + } + + override fun onActivityPaused(activity: Activity) { + Timber.i("%s - onPause", activity.javaClass.simpleName) + } + + override fun onActivityStopped(activity: Activity) { + Timber.i("%s - onStop", activity.javaClass.simpleName) + if (resumedActivity === activity) { + resumedActivity = null + } + if (resumedActivity == null) { + for (callback: ApplicationLifecycleCallback in lifecycleCallbacks) { + callback.onApplicationBackground(activity) + } + Timber.i("%s - onApplicationBackground", activity.javaClass.simpleName) + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + Timber.i("%s - onSaveInstanceState", activity.javaClass.simpleName) + } + + override fun onActivityDestroyed(activity: Activity) { + Timber.i("%s - onDestroy", activity.javaClass.simpleName) + activitySet.remove(getObjectTag(activity)) + if (topActivity === activity) { + topActivity = null + } + if (activitySet.size == 0) { + for (callback: ApplicationLifecycleCallback in lifecycleCallbacks) { + callback.onApplicationDestroy(activity) + } + Timber.i("%s - onApplicationDestroy", activity.javaClass.simpleName) + } + } + + /** + * 应用生命周期回调 + */ + interface ApplicationLifecycleCallback { + + /** + * 第一个 Activity 创建了 + */ + fun onApplicationCreate(activity: Activity) + + /** + * 最后一个 Activity 销毁了 + */ + fun onApplicationDestroy(activity: Activity) + + /** + * 应用从前台进入到后台 + */ + fun onApplicationBackground(activity: Activity) + + /** + * 应用从后台进入到前台 + */ + fun onApplicationForeground(activity: Activity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/CacheDataManager.kt b/app/src/main/java/com/hjq/demo/manager/CacheDataManager.kt new file mode 100644 index 0000000..a275b25 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/CacheDataManager.kt @@ -0,0 +1,99 @@ +package com.hjq.demo.manager + +import android.content.Context +import android.os.Environment +import com.tencent.bugly.crashreport.CrashReport +import java.io.File +import java.math.BigDecimal + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/03/01 + * desc : 应用缓存管理 + */ +object CacheDataManager { + + /** + * 获取缓存大小 + */ + fun getTotalCacheSize(context: Context): String { + var cacheSize: Long = getFolderSize(context.cacheDir) + if ((Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED)) { + cacheSize += getFolderSize(context.externalCacheDir!!) + } + return getFormatSize(cacheSize.toDouble()) + } + + /** + * 清除缓存 + */ + fun clearAllCache(context: Context) { + deleteDir(context.cacheDir) + if ((Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED)) { + deleteDir(context.externalCacheDir) + } + } + + /** + * 删除文件夹 + */ + private fun deleteDir(dir: File?): Boolean { + if (dir == null) { + return false + } + if (!dir.isDirectory) { + return dir.delete() + } + val children: Array = dir.list() ?: return false + for (child: String in children) { + deleteDir(File(dir, child)) + } + return false + } + + // 获取文件大小 + // Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/ 目录,一般放一些长时间保存的数据 + // Context.getExternalCacheDir() --> SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据 + private fun getFolderSize(file: File): Long { + var size: Long = 0 + try { + val list: Array = file.listFiles() ?: return 0 + for (temp: File in list) { + // 如果下面还有文件 + size += if (temp.isDirectory) { + getFolderSize(temp) + } else { + temp.length() + } + } + } catch (e: Exception) { + CrashReport.postCatchedException(e) + } + return size + } + + /** + * 格式化单位 + */ + fun getFormatSize(size: Double): String { + val kiloByte: Double = size / 1024 + if (kiloByte < 1) { + // return size + "Byte"; + return "0K" + } + val megaByte: Double = kiloByte / 1024 + if (megaByte < 1) { + return BigDecimal(kiloByte).setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "K" + } + val gigaByte: Double = megaByte / 1024 + if (gigaByte < 1) { + return BigDecimal(megaByte).setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "M" + } + val teraBytes: Double = gigaByte / 1024 + if (teraBytes < 1) { + return BigDecimal(gigaByte).setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "GB" + } + return BigDecimal(teraBytes).setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/DialogManager.kt b/app/src/main/java/com/hjq/demo/manager/DialogManager.kt new file mode 100644 index 0000000..5042242 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/DialogManager.kt @@ -0,0 +1,92 @@ +package com.hjq.demo.manager + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import com.hjq.base.BaseDialog +import java.util.* + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2021/01/29 + * desc : Dialog 显示管理类 + */ +class DialogManager private constructor(lifecycleOwner: LifecycleOwner) : + LifecycleEventObserver, BaseDialog.OnDismissListener { + + companion object { + + private val DIALOG_MANAGER: HashMap = HashMap() + + fun getInstance(lifecycleOwner: LifecycleOwner): DialogManager { + + var manager: DialogManager? = DIALOG_MANAGER[lifecycleOwner] + if (manager == null) { + manager = DialogManager(lifecycleOwner) + DIALOG_MANAGER[lifecycleOwner] = manager + } + return manager + } + } + + private val dialogs: MutableList = ArrayList() + + init { + lifecycleOwner.lifecycle.addObserver(this) + } + + /** + * 排队显示 Dialog + */ + fun addShow(dialog: BaseDialog) { + if (dialog.isShowing) { + throw IllegalStateException("are you ok?") + } + dialogs.add(dialog) + val firstDialog: BaseDialog = dialogs[0] + if (!firstDialog.isShowing) { + firstDialog.addOnDismissListener(this) + firstDialog.show() + } + } + + /** + * 取消所有 Dialog 的显示 + */ + fun clearShow() { + if (dialogs.isEmpty()) { + return + } + val firstDialog: BaseDialog = dialogs[0] + if (firstDialog.isShowing) { + firstDialog.removeOnDismissListener(this) + firstDialog.dismiss() + } + dialogs.clear() + } + + override fun onDismiss(dialog: BaseDialog?) { + dialog?.removeOnDismissListener(this) + dialogs.remove(dialog) + for (nextDialog: BaseDialog in dialogs) { + if (!nextDialog.isShowing) { + nextDialog.addOnDismissListener(this) + nextDialog.show() + break + } + } + } + + /** + * [LifecycleEventObserver] + */ + override fun onStateChanged(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event) { + if (event != Lifecycle.Event.ON_DESTROY) { + return + } + DIALOG_MANAGER.remove(lifecycleOwner) + lifecycleOwner.lifecycle.removeObserver(this) + clearShow() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/InputTextManager.kt b/app/src/main/java/com/hjq/demo/manager/InputTextManager.kt new file mode 100644 index 0000000..52ebbf3 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/InputTextManager.kt @@ -0,0 +1,267 @@ +package com.hjq.demo.manager + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.* +import android.text.Editable +import android.text.TextWatcher +import android.view.* +import android.widget.TextView +import java.util.* + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : 文本输入管理类,通过管理多个 EditText 输入是否为空来启用或者禁用按钮的点击事件 + * blog : https://www.jianshu.com/p/fd3795e8a6b3 + */ +class InputTextManager private constructor(view: View, alpha: Boolean) : TextWatcher { + + companion object { + + fun with(activity: Activity): Builder { + return Builder(activity) + } + } + + /** 操作按钮的View */ + private val view: View + + /** 是否禁用后设置半透明度 */ + private val alpha: Boolean + + /** TextView集合 */ + private var viewSet: MutableList = mutableListOf() + + /** 输入监听器 */ + private var listener: OnInputTextListener? = null + + init { + this.view = view + this.alpha = alpha + } + + /** + * 添加 TextView + * + * @param views 传入单个或者多个 TextView + */ + fun addViews(views: MutableList) { + viewSet.addAll(views) + for (view: TextView in views) { + view.addTextChangedListener(this) + } + + // 触发一次监听 + notifyChanged() + } + + /** + * 添加 TextView + * + * @param views 传入单个或者多个 TextView + */ + fun addViews(vararg views: TextView) { + for (view: TextView in views) { + // 避免重复添加 + if (!viewSet.contains(view)) { + view.addTextChangedListener(this) + viewSet.add(view) + } + } + // 触发一次监听 + notifyChanged() + } + + /** + * 移除 TextView 监听,避免内存泄露 + */ + fun removeViews(vararg views: TextView) { + if (viewSet.isEmpty()) { + return + } + for (view: TextView in views) { + view.removeTextChangedListener(this) + viewSet.remove(view) + } + // 触发一次监听 + notifyChanged() + } + + /** + * 移除所有 TextView 监听,避免内存泄露 + */ + fun removeAllViews() { + for (view: TextView in viewSet) { + view.removeTextChangedListener(this) + } + viewSet.clear() + } + + /** + * 设置输入监听 + */ + fun setListener(listener: OnInputTextListener?) { + this.listener = listener + } + + /** + * [TextWatcher] + */ + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + notifyChanged() + } + + /** + * 通知更新 + */ + fun notifyChanged() { + // 重新遍历所有的输入 + for (view: TextView in viewSet) { + if (("" == view.text.toString())) { + setEnabled(false) + return + } + } + + listener.let { + if (it == null) { + setEnabled(true) + return + } + setEnabled(it.onInputChange(this)) + } + } + + /** + * 设置 View 的事件 + * + * @param enabled 启用或者禁用 View 的事件 + */ + fun setEnabled(enabled: Boolean) { + if (enabled == view.isEnabled) { + return + } + if (enabled) { + //启用View的事件 + view.isEnabled = true + if (alpha) { + //设置不透明 + view.alpha = 1f + } + } else { + //禁用View的事件 + view.isEnabled = false + if (alpha) { + //设置半透明 + view.alpha = 0.5f + } + } + } + + class Builder constructor(private val activity: Activity) { + + /** 操作按钮的 View */ + private var view: View? = null + + /** 是否禁用后设置半透明度 */ + private var alpha: Boolean = false + + /** TextView集合 */ + private val viewSet: MutableList = ArrayList() + + /** 输入变化监听 */ + private var listener: OnInputTextListener? = null + + fun addView(view: TextView?): Builder = apply { + if (view != null) { + viewSet.add(view) + } + } + + fun setMain(view: View): Builder = apply { + this.view = view + } + + fun setAlpha(alpha: Boolean): Builder = apply { + this.alpha = alpha + } + + fun setListener(listener: OnInputTextListener?): Builder = apply { + this.listener = listener + } + + fun build(): InputTextManager { + if (view == null) { + throw IllegalArgumentException("are you ok?") + } + val helper = InputTextManager(view!!, alpha) + helper.addViews(viewSet) + helper.setListener(listener) + TextInputLifecycle.register(activity, helper) + return helper + } + } + + private class TextInputLifecycle private constructor( + private var activity: Activity?, + private var textHelper: InputTextManager? + ) : ActivityLifecycleCallbacks { + + companion object { + + fun register(activity: Activity, helper: InputTextManager?) { + val lifecycle = TextInputLifecycle(activity, helper) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.registerActivityLifecycleCallbacks(lifecycle) + } else { + activity.application.registerActivityLifecycleCallbacks(lifecycle) + } + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) {} + + override fun onActivityPaused(activity: Activity) {} + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) { + if (this.activity !== activity) { + return + } + textHelper?.removeAllViews() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + this.activity?.unregisterActivityLifecycleCallbacks(this) + } else { + this.activity?.application?.unregisterActivityLifecycleCallbacks(this) + } + textHelper = null + this.activity = null + } + } + + /** + * 文本变化监听器 + */ + interface OnInputTextListener { + + /** + * 输入发生了变化 + * + * @return 返回按钮的 Enabled 状态 + */ + fun onInputChange(manager: InputTextManager?): Boolean + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/PickerLayoutManager.kt b/app/src/main/java/com/hjq/demo/manager/PickerLayoutManager.kt new file mode 100644 index 0000000..341817e --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/PickerLayoutManager.kt @@ -0,0 +1,233 @@ +package com.hjq.demo.manager + +import android.content.Context +import android.view.View +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSnapHelper +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Recycler +import kotlin.math.abs +import kotlin.math.min + +/** + * author : 钉某人 & Android 轮子哥 + * github : https://github.com/DingMouRen/LayoutManagerGroup + * time : 2019/09/11 + * desc : 选择器布局管理器 + */ +class PickerLayoutManager private constructor( + context: Context, orientation: Int, reverseLayout: Boolean, maxItem: Int, scale: Float, alpha: Boolean) : + LinearLayoutManager(context, orientation, reverseLayout) { + + private val linearSnapHelper: LinearSnapHelper = LinearSnapHelper() + private val maxItem: Int + private val scale: Float + private val alpha: Boolean + private var recyclerView: RecyclerView? = null + private var listener: OnPickerListener? = null + + init { + this.maxItem = maxItem + this.alpha = alpha + this.scale = scale + } + + override fun onAttachedToWindow(recyclerView: RecyclerView) { + super.onAttachedToWindow(recyclerView) + this.recyclerView = recyclerView + // 设置子控件的边界可以超过父布局的范围 + this.recyclerView!!.clipToPadding = false + // 添加 LinearSnapHelper + linearSnapHelper.attachToRecyclerView(this.recyclerView) + } + + override fun onDetachedFromWindow(recyclerView: RecyclerView?, recycler: Recycler?) { + super.onDetachedFromWindow(recyclerView, recycler) + this.recyclerView = null + } + + override fun isAutoMeasureEnabled(): Boolean { + return maxItem == 0 + } + + override fun onMeasure(recycler: Recycler, state: RecyclerView.State, widthSpec: Int, heightSpec: Int) { + var width: Int = chooseSize(widthSpec, paddingLeft + paddingRight, ViewCompat.getMinimumWidth(recyclerView!!)) + var height: Int = chooseSize(heightSpec, paddingTop + paddingBottom, ViewCompat.getMinimumHeight(recyclerView!!)) + if (state.itemCount != 0 && maxItem != 0) { + val itemView: View = recycler.getViewForPosition(0) + measureChildWithMargins(itemView, widthSpec, heightSpec) + if (orientation == HORIZONTAL) { + val measuredWidth: Int = itemView.measuredWidth + val paddingHorizontal: Int = (maxItem - 1) / 2 * measuredWidth + recyclerView!!.setPadding(paddingHorizontal, 0, paddingHorizontal, 0) + width = measuredWidth * maxItem + } else if (orientation == VERTICAL) { + val measuredHeight: Int = itemView.measuredHeight + val paddingVertical: Int = (maxItem - 1) / 2 * measuredHeight + recyclerView!!.setPadding(0, paddingVertical, 0, paddingVertical) + height = measuredHeight * maxItem + } + } + setMeasuredDimension(width, height) + } + + override fun onScrollStateChanged(state: Int) { + super.onScrollStateChanged(state) + // 当 RecyclerView 停止滚动时 + if (state != RecyclerView.SCROLL_STATE_IDLE) { + return + } + recyclerView?.let { + listener?.onPicked(it, getPickedPosition()) + } + } + + override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) { + super.onLayoutChildren(recycler, state) + if (itemCount < 0 || state.isPreLayout) { + return + } + if (orientation == HORIZONTAL) { + scaleHorizontalChildView() + } else if (orientation == VERTICAL) { + scaleVerticalChildView() + } + } + + override fun scrollHorizontallyBy(dx: Int, recycler: Recycler?, state: RecyclerView.State?): Int { + scaleHorizontalChildView() + return super.scrollHorizontallyBy(dx, recycler, state) + } + + override fun scrollVerticallyBy(dy: Int, recycler: Recycler?, state: RecyclerView.State?): Int { + scaleVerticalChildView() + return super.scrollVerticallyBy(dy, recycler, state) + } + + /** + * 横向情况下的缩放 + */ + private fun scaleHorizontalChildView() { + val mid: Float = width / 2.0f + for (i in 0 until childCount) { + val childView: View = getChildAt(i) ?: continue + val childMid: Float = + (getDecoratedLeft(childView) + getDecoratedRight(childView)) / 2.0f + val scale: Float = 1.0f + (-1 * (1 - scale)) * min(mid, abs(mid - childMid)) / mid + childView.scaleX = scale + childView.scaleY = scale + if (alpha) { + childView.alpha = scale + } + } + } + + /** + * 竖向方向上的缩放 + */ + private fun scaleVerticalChildView() { + val mid: Float = height / 2.0f + for (i in 0 until childCount) { + val childView: View = getChildAt(i) ?: continue + val childMid: Float = (getDecoratedTop(childView) + getDecoratedBottom(childView)) / 2.0f + val scale: Float = 1.0f + (-1 * (1 - scale)) * (min(mid, abs(mid - childMid))) / mid + childView.scaleX = scale + childView.scaleY = scale + if (alpha) { + childView.alpha = scale + } + } + } + + /** + * 获取选中的位置 + */ + fun getPickedPosition(): Int { + val itemView: View = linearSnapHelper.findSnapView(this) ?: return 0 + return getPosition(itemView) + } + + /** + * 设置监听器 + */ + fun setOnPickerListener(listener: OnPickerListener?) { + this.listener = listener + } + + interface OnPickerListener { + + /** + * 滚动停止时触发的监听 + * + * @param recyclerView RecyclerView 对象 + * @param position 当前滚动的位置 + */ + fun onPicked(recyclerView: RecyclerView, position: Int) + } + + class Builder constructor(private val context: Context) { + + private var orientation: Int = VERTICAL + private var reverseLayout: Boolean = false + private var listener: OnPickerListener? = null + private var maxItem: Int = 3 + private var scale: Float = 0.6f + private var alpha: Boolean = true + + /** + * 设置布局摆放器方向 + */ + fun setOrientation(@RecyclerView.Orientation orientation: Int): Builder = apply { + this.orientation = orientation + } + + /** + * 设置是否反向显示 + */ + fun setReverseLayout(reverseLayout: Boolean): Builder = apply { + this.reverseLayout = reverseLayout + } + + /** + * 设置最大显示条目数 + */ + fun setMaxItem(maxItem: Int): Builder = apply { + this.maxItem = maxItem + } + + /** + * 设置缩放比例 + */ + fun setScale(scale: Float): Builder = apply { + this.scale = scale + } + + /** + * 设置透明开关 + */ + fun setAlpha(alpha: Boolean): Builder = apply { + this.alpha = alpha + } + + fun setOnPickerListener(listener: OnPickerListener?): Builder = apply { + this.listener = listener + } + + /** + * 构建布局管理器 + */ + fun build(): PickerLayoutManager { + val layoutManager = PickerLayoutManager(context, orientation, reverseLayout, maxItem, scale, alpha) + layoutManager.setOnPickerListener(listener) + return layoutManager + } + + /** + * 应用到 RecyclerView + */ + fun into(recyclerView: RecyclerView) { + recyclerView.layoutManager = build() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/ThreadPoolManager.kt b/app/src/main/java/com/hjq/demo/manager/ThreadPoolManager.kt new file mode 100644 index 0000000..3b19a6d --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/ThreadPoolManager.kt @@ -0,0 +1,34 @@ +package com.hjq.demo.manager + +import java.util.concurrent.SynchronousQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/01/11 + * desc : 线程池管理类 + */ +class ThreadPoolManager : ThreadPoolExecutor( + 0, 200, + 30L, TimeUnit.MILLISECONDS, + SynchronousQueue()) { + + companion object { + + @Volatile + private var instance: ThreadPoolManager? = null + + fun getInstance(): ThreadPoolManager { + if (instance == null) { + synchronized(ThreadPoolManager::class.java) { + if (instance == null) { + instance = ThreadPoolManager() + } + } + } + return instance!! + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/AppConfig.kt b/app/src/main/java/com/hjq/demo/other/AppConfig.kt new file mode 100644 index 0000000..0300041 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/AppConfig.kt @@ -0,0 +1,67 @@ +package com.hjq.demo.other + +import com.hjq.demo.BuildConfig +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/09/02 + * desc : App 配置管理类 + */ +object AppConfig { + + /** + * 当前是否为调试模式 + */ + fun isDebug(): Boolean { + return BuildConfig.DEBUG + } + + /** + * 获取当前构建的模式 + */ + fun getBuildType(): String { + return BuildConfig.BUILD_TYPE + } + + /** + * 当前是否要开启日志打印功能 + */ + fun isLogEnable(): Boolean { + return BuildConfig.LOG_ENABLE + } + + /** + * 获取当前应用的包名 + */ + fun getPackageName(): String { + return BuildConfig.APPLICATION_ID + } + + /** + * 获取当前应用的版本名 + */ + fun getVersionName(): String { + return BuildConfig.VERSION_NAME + } + + /** + * 获取当前应用的版本码 + */ + fun getVersionCode(): Int { + return BuildConfig.VERSION_CODE + } + + /** + * 获取 Bugly Id + */ + fun getBuglyId(): String { + return BuildConfig.BUGLY_ID + } + + /** + * 获取服务器主机地址 + */ + fun getHostUrl(): String { + return BuildConfig.HOST_URL + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ArrowDrawable.kt b/app/src/main/java/com/hjq/demo/other/ArrowDrawable.kt new file mode 100644 index 0000000..c032a06 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/ArrowDrawable.kt @@ -0,0 +1,280 @@ +package com.hjq.demo.other + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import android.view.Gravity +import android.view.View +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import com.hjq.demo.R +import kotlin.math.max +import kotlin.math.min + +/** + * author : 王浩 & Android 轮子哥 + * github : https://github.com/bingoogolapple/BGATransformersTip-Android + * time : 2019/08/19 + * desc : 带箭头背景的 Drawable + */ +@Suppress("RtlHardcoded") +class ArrowDrawable private constructor(private val builder: Builder) : Drawable() { + + private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + private var path: Path = Path() + + override fun draw(canvas: Canvas) { + if (builder.shadowSize > 0) { + paint.maskFilter = BlurMaskFilter(builder.shadowSize.toFloat(), BlurMaskFilter.Blur.OUTER) + paint.color = builder.shadowColor + canvas.drawPath(path, paint) + } + paint.maskFilter = null + paint.color = builder.backgroundColor + canvas.drawPath(path, paint) + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + override fun onBoundsChange(viewRect: Rect) { + path.reset() + + val excludeShadowRectF = RectF(viewRect) + excludeShadowRectF.inset(builder.shadowSize.toFloat(), builder.shadowSize.toFloat()) + val centerPointF = PointF() + when (builder.arrowOrientation) { + Gravity.LEFT -> { + excludeShadowRectF.left += builder.arrowHeight.toFloat() + centerPointF.x = excludeShadowRectF.left + } + Gravity.RIGHT -> { + excludeShadowRectF.right -= builder.arrowHeight.toFloat() + centerPointF.x = excludeShadowRectF.right + } + Gravity.TOP -> { + excludeShadowRectF.top += builder.arrowHeight.toFloat() + centerPointF.y = excludeShadowRectF.top + } + Gravity.BOTTOM -> { + excludeShadowRectF.bottom -= builder.arrowHeight.toFloat() + centerPointF.y = excludeShadowRectF.bottom + } + } + when (builder.arrowGravity) { + Gravity.LEFT -> centerPointF.x = excludeShadowRectF.left + builder.arrowHeight + Gravity.CENTER_HORIZONTAL -> centerPointF.x = viewRect.width() / 2f + Gravity.RIGHT -> centerPointF.x = excludeShadowRectF.right - builder.arrowHeight + Gravity.TOP -> centerPointF.y = excludeShadowRectF.top + builder.arrowHeight + Gravity.CENTER_VERTICAL -> centerPointF.y = viewRect.height() / 2f + Gravity.BOTTOM -> centerPointF.y = excludeShadowRectF.bottom - builder.arrowHeight + } + + // 更新箭头偏移量 + centerPointF.x += builder.arrowOffsetX.toFloat() + centerPointF.y += builder.arrowOffsetY.toFloat() + when (builder.arrowGravity) { + Gravity.LEFT, Gravity.RIGHT, Gravity.CENTER_HORIZONTAL -> { + centerPointF.x = max(centerPointF.x, excludeShadowRectF.left + builder.radius + builder.arrowHeight) + centerPointF.x = min(centerPointF.x, excludeShadowRectF.right - builder.radius - builder.arrowHeight) + } + Gravity.TOP, Gravity.BOTTOM, Gravity.CENTER_VERTICAL -> { + centerPointF.y = max(centerPointF.y, excludeShadowRectF.top + builder.radius + builder.arrowHeight) + centerPointF.y = min(centerPointF.y, excludeShadowRectF.bottom - builder.radius - builder.arrowHeight) + } + } + when (builder.arrowOrientation) { + Gravity.LEFT, Gravity.RIGHT -> { + centerPointF.x = max(centerPointF.x, excludeShadowRectF.left) + centerPointF.x = min(centerPointF.x, excludeShadowRectF.right) + } + Gravity.TOP, Gravity.BOTTOM -> { + centerPointF.y = max(centerPointF.y, excludeShadowRectF.top) + centerPointF.y = min(centerPointF.y, excludeShadowRectF.bottom) + } + } + + // 箭头区域(其实是旋转了 90 度后的正方形区域) + val arrowPath = Path() + arrowPath.moveTo(centerPointF.x - builder.arrowHeight, centerPointF.y) + arrowPath.lineTo(centerPointF.x, centerPointF.y - builder.arrowHeight) + arrowPath.lineTo(centerPointF.x + builder.arrowHeight, centerPointF.y) + arrowPath.lineTo(centerPointF.x, centerPointF.y + builder.arrowHeight) + arrowPath.close() + path.addRoundRect( + excludeShadowRectF, + builder.radius.toFloat(), + builder.radius.toFloat(), + Path.Direction.CW + ) + path.addPath(arrowPath) + invalidateSelf() + } + + class Builder constructor(private val context: Context) { + + /** 箭头高度 */ + var arrowHeight: Int + + /** 背景圆角大小 */ + var radius: Int + + /** 箭头方向 */ + var arrowOrientation: Int + + /** 箭头重心 */ + var arrowGravity: Int + + /** 箭头水平方向偏移 */ + var arrowOffsetX: Int + + /** 箭头垂直方向偏移 */ + var arrowOffsetY: Int + + /** 阴影大小 */ + var shadowSize: Int + + /** 背景颜色 */ + var backgroundColor: Int + + /** 阴影颜色 */ + var shadowColor: Int + + init { + backgroundColor = ContextCompat.getColor(context, R.color.black) + shadowColor = ContextCompat.getColor(context, R.color.black20) + arrowHeight = context.resources.getDimension(R.dimen.dp_6).toInt() + radius = context.resources.getDimension(R.dimen.dp_4).toInt() + shadowSize = 0 + arrowOffsetX = 0 + arrowOffsetY = 0 + arrowOrientation = Gravity.NO_GRAVITY + arrowGravity = Gravity.NO_GRAVITY + } + + /** + * 设置背景色 + */ + fun setBackgroundColor(@ColorInt color: Int): Builder = apply { + backgroundColor = color + } + + /** + * 设置阴影色 + */ + fun setShadowColor(@ColorInt color: Int): Builder = apply { + shadowColor = color + } + + /** + * 设置箭头高度 + */ + fun setArrowHeight(height: Int): Builder = apply { + arrowHeight = height + } + + /** + * 设置浮窗圆角半径 + */ + fun setRadius(radius: Int): Builder = apply { + this.radius = radius + } + + /** + * 设置箭头方向(左上右下) + */ + fun setArrowOrientation(orientation: Int): Builder = apply { + when (val finalOrientation: Int = Gravity.getAbsoluteGravity(orientation, context.resources.configuration.layoutDirection)) { + Gravity.LEFT, Gravity.TOP, Gravity.RIGHT, Gravity.BOTTOM -> { + arrowOrientation = finalOrientation + } + else -> throw IllegalArgumentException("are you ok?") + } + } + + /** + * 设置箭头布局重心 + */ + fun setArrowGravity(gravity: Int): Builder = apply { + var finalGravity: Int = gravity + finalGravity = Gravity.getAbsoluteGravity( + finalGravity, + context.resources.configuration.layoutDirection + ) + if (finalGravity == Gravity.CENTER) { + when (arrowOrientation) { + Gravity.LEFT, Gravity.RIGHT -> finalGravity = Gravity.CENTER_VERTICAL + Gravity.TOP, Gravity.BOTTOM -> finalGravity = Gravity.CENTER_HORIZONTAL + } + } + when (finalGravity) { + Gravity.LEFT, Gravity.RIGHT -> if (arrowOrientation == Gravity.LEFT || arrowOrientation == Gravity.RIGHT) { + throw IllegalArgumentException("are you ok?") + } + Gravity.TOP, Gravity.BOTTOM -> if (arrowOrientation == Gravity.TOP || arrowOrientation == Gravity.BOTTOM) { + throw IllegalArgumentException("are you ok?") + } + Gravity.CENTER_VERTICAL, Gravity.CENTER_HORIZONTAL -> {} + else -> { + throw IllegalArgumentException("are you ok?") + } + } + arrowGravity = finalGravity + } + + /** + * 设置箭头在 x 轴的偏移量 + */ + fun setArrowOffsetX(offsetX: Int): Builder = apply { + arrowOffsetX = offsetX + } + + /** + * 设置箭头在 y 轴的偏移量 + */ + fun setArrowOffsetY(offsetY: Int): Builder = apply { + arrowOffsetY = offsetY + } + + /** + * 设置阴影宽度 + */ + fun setShadowSize(size: Int): Builder = apply { + shadowSize = size + } + + /** + * 构建 Drawable + */ + fun build(): Drawable { + if (arrowOrientation == Gravity.NO_GRAVITY || arrowGravity == Gravity.NO_GRAVITY) { + // 必须要先设置箭头的方向及重心 + throw IllegalArgumentException("are you ok?") + } + return ArrowDrawable(this) + } + + /** + * 应用到 View + */ + fun apply(view: View) { + view.background = build() + if (shadowSize > 0 || arrowHeight > 0) { + if ((view.paddingTop == 0) && (view.bottom == 0) && + (view.paddingLeft == 0) && (view.paddingRight == 0)) { + + view.setPadding(shadowSize, shadowSize + arrowHeight, shadowSize, shadowSize) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/CrashHandler.kt b/app/src/main/java/com/hjq/demo/other/CrashHandler.kt new file mode 100644 index 0000000..332422a --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/CrashHandler.kt @@ -0,0 +1,73 @@ +package com.hjq.demo.other + +import android.app.* +import android.content.* +import android.os.Process +import com.hjq.demo.ui.activity.CrashActivity +import com.hjq.demo.ui.activity.RestartActivity + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/02/03 + * desc : Crash 处理类 + */ +class CrashHandler private constructor(private val application: Application) : + Thread.UncaughtExceptionHandler { + + companion object { + + /** Crash 文件名 */ + private const val CRASH_FILE_NAME: String = "crash_file" + + /** Crash 时间记录 */ + private const val KEY_CRASH_TIME: String = "key_crash_time" + + /** + * 注册 Crash 监听 + */ + fun register(application: Application) { + Thread.setDefaultUncaughtExceptionHandler(CrashHandler(application)) + } + } + + private val nextHandler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler() + + init { + if ((javaClass.name == nextHandler?.javaClass?.name)) { + // 请不要重复注册 Crash 监听 + throw IllegalStateException("are you ok?") + } + } + + @Suppress("ApplySharedPref") + override fun uncaughtException(thread: Thread, throwable: Throwable) { + val sharedPreferences: SharedPreferences = application.getSharedPreferences( + CRASH_FILE_NAME, Context.MODE_PRIVATE) + val currentCrashTime: Long = System.currentTimeMillis() + val lastCrashTime: Long = sharedPreferences.getLong(KEY_CRASH_TIME, 0) + // 记录当前崩溃的时间,以便下次崩溃时进行比对 + sharedPreferences.edit().putLong(KEY_CRASH_TIME, currentCrashTime).commit() + + // 致命异常标记:如果上次崩溃的时间距离当前崩溃小于 5 分钟,那么判定为致命异常 + val deadlyCrash: Boolean = currentCrashTime - lastCrashTime < 1000 * 60 * 5 + if (AppConfig.isDebug()) { + CrashActivity.start(application, throwable) + } else { + if (!deadlyCrash) { + // 如果不是致命的异常就自动重启应用 + RestartActivity.start(application) + } + } + + // 不去触发系统的崩溃处理(com.android.internal.os.RuntimeInit$KillApplicationHandler) + if (nextHandler != null && !nextHandler.javaClass.name + .startsWith("com.android.internal.os")) { + nextHandler.uncaughtException(thread, throwable) + } + + // 杀死进程(这个事应该是系统干的,但是它会多弹出一个崩溃对话框,所以需要我们自己手动杀死进程) + Process.killProcess(Process.myPid()) + System.exit(10) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.kt b/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.kt new file mode 100644 index 0000000..7d768c5 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.kt @@ -0,0 +1,29 @@ +package com.hjq.demo.other + +import android.os.Build +import timber.log.Timber.DebugTree + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/08/12 + * desc : 自定义日志打印规则 + */ +class DebugLoggerTree : DebugTree() { + + companion object { + private const val MAX_TAG_LENGTH: Int = 23 + } + + /** + * 创建日志堆栈 TAG + */ + override fun createStackElementTag(element: StackTraceElement): String { + val tag: String = "(" + element.fileName + ":" + element.lineNumber + ")" + // 日志 TAG 长度限制已经在 Android 8.0 被移除 + if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return tag + } + return tag.substring(0, MAX_TAG_LENGTH) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/DoubleClickHelper.kt b/app/src/main/java/com/hjq/demo/other/DoubleClickHelper.kt new file mode 100644 index 0000000..4b4047e --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/DoubleClickHelper.kt @@ -0,0 +1,32 @@ +package com.hjq.demo.other + +import android.os.SystemClock + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : 双击判断工具类 + */ +object DoubleClickHelper { + + /** 数组的长度为2代表只记录双击操作 */ + private val TIME_ARRAY: LongArray = LongArray(2) + + /** + * 是否在短时间内进行了双击操作 + */ + fun isOnDoubleClick(): Boolean { + // 默认间隔时长 + return isOnDoubleClick(1500) + } + + /** + * 是否在短时间内进行了双击操作 + */ + fun isOnDoubleClick(time: Int): Boolean { + System.arraycopy(TIME_ARRAY, 1, TIME_ARRAY, 0, TIME_ARRAY.size - 1) + TIME_ARRAY[TIME_ARRAY.size - 1] = SystemClock.uptimeMillis() + return TIME_ARRAY[0] >= (SystemClock.uptimeMillis() - time) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/GridSpaceDecoration.kt b/app/src/main/java/com/hjq/demo/other/GridSpaceDecoration.kt new file mode 100644 index 0000000..c7df378 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/GridSpaceDecoration.kt @@ -0,0 +1,38 @@ +package com.hjq.demo.other + +import android.graphics.Canvas +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/07/25 + * desc : 图片选择列表分割线 + */ +class GridSpaceDecoration constructor(private val space: Int) : ItemDecoration() { + + override fun onDraw(canvas: Canvas, recyclerView: RecyclerView, state: RecyclerView.State) {} + + override fun onDrawOver(canvas: Canvas, recyclerView: RecyclerView, state: RecyclerView.State) {} + + override fun getItemOffsets(rect: Rect, view: View, recyclerView: RecyclerView, state: RecyclerView.State) { + val position: Int = recyclerView.getChildAdapterPosition(view) + val spanCount: Int = (recyclerView.layoutManager as GridLayoutManager).spanCount + + // 每一行的最后一个才留出右边间隙 + if ((position + 1) % spanCount == 0) { + rect.right = space + } + + // 只有第一行才留出顶部间隙 + if (position < spanCount) { + rect.top = space + } + rect.bottom = space + rect.left = space + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/KeyboardWatcher.kt b/app/src/main/java/com/hjq/demo/other/KeyboardWatcher.kt new file mode 100644 index 0000000..16bd3bb --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/KeyboardWatcher.kt @@ -0,0 +1,120 @@ +package com.hjq.demo.other + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.graphics.* +import android.os.* +import android.view.* +import android.view.ViewTreeObserver.OnGlobalLayoutListener + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/07/04 + * desc : 软键盘监听类 + */ +class KeyboardWatcher private constructor(private var activity: Activity) : + OnGlobalLayoutListener, ActivityLifecycleCallbacks { + + companion object { + + fun with(activity: Activity): KeyboardWatcher { + return KeyboardWatcher(activity) + } + } + + private var contentView: View = activity.findViewById(Window.ID_ANDROID_CONTENT) + private var listeners: SoftKeyboardStateListener? = null + private var softKeyboardOpened: Boolean = false + private var statusBarHeight: Int = 0 + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.registerActivityLifecycleCallbacks(this) + } else { + activity.application.registerActivityLifecycleCallbacks(this) + } + contentView.viewTreeObserver.addOnGlobalLayoutListener(this) + + // 获取 status_bar_height 资源的 ID + val resourceId: Int = activity.resources.getIdentifier("status_bar_height", "dimen", "android") + if (resourceId > 0) { + //根据资源 ID 获取响应的尺寸值 + statusBarHeight = activity.resources.getDimensionPixelSize(resourceId) + } + } + + /** + * [ViewTreeObserver.OnGlobalLayoutListener] + */ + override fun onGlobalLayout() { + val r = Rect() + //r will be populated with the coordinates of your view that area still visible. + contentView.getWindowVisibleDisplayFrame(r) + val heightDiff: Int = contentView.rootView.height - (r.bottom - r.top) + if (!softKeyboardOpened && heightDiff > contentView.rootView.height / 4) { + softKeyboardOpened = true + if ((activity.window.attributes.flags and WindowManager.LayoutParams.FLAG_FULLSCREEN) != WindowManager.LayoutParams.FLAG_FULLSCREEN) { + listeners?.onSoftKeyboardOpened(heightDiff - statusBarHeight) + } else { + listeners?.onSoftKeyboardOpened(heightDiff) + } + } else if (softKeyboardOpened && heightDiff < contentView.rootView.height / 4) { + softKeyboardOpened = false + listeners?.onSoftKeyboardClosed() + } + } + + /** + * 设置软键盘弹出监听 + */ + fun setListener(listener: SoftKeyboardStateListener?) { + listeners = listener + } + + /** + * [ActivityLifecycleCallbacks] + */ + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) {} + + override fun onActivityPaused(activity: Activity) {} + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) { + if (this.activity === activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + this.activity.unregisterActivityLifecycleCallbacks(this) + } else { + this.activity.application.unregisterActivityLifecycleCallbacks(this) + } + contentView.viewTreeObserver.removeOnGlobalLayoutListener(this) + listeners = null + } + } + + /** + * 软键盘状态监听器 + */ + interface SoftKeyboardStateListener { + + /** + * 软键盘弹出了 + * + * @param keyboardHeight 软键盘高度 + */ + fun onSoftKeyboardOpened(keyboardHeight: Int) + + /** + * 软键盘收起了 + */ + fun onSoftKeyboardClosed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/MaterialHeader.kt b/app/src/main/java/com/hjq/demo/other/MaterialHeader.kt new file mode 100644 index 0000000..2915dd4 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/MaterialHeader.kt @@ -0,0 +1,271 @@ +package com.hjq.demo.other + +import android.content.Context +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.util.AttributeSet +import android.widget.ImageView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import com.hjq.demo.R +import com.scwang.smart.refresh.header.material.CircleImageView +import com.scwang.smart.refresh.header.material.MaterialProgressDrawable +import com.scwang.smart.refresh.layout.api.RefreshHeader +import com.scwang.smart.refresh.layout.api.RefreshKernel +import com.scwang.smart.refresh.layout.api.RefreshLayout +import com.scwang.smart.refresh.layout.constant.RefreshState +import com.scwang.smart.refresh.layout.constant.SpinnerStyle +import com.scwang.smart.refresh.layout.simple.SimpleComponent +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +/** + * author : 树朾 & Android 轮子哥 + * github : https://github.com/scwang90/SmartRefreshLayout/tree/master/refresh-header-material + * time : 2021/02/28 + * desc : Material 风格的刷新球,参考 [com.scwang.smart.refresh.header.MaterialHeader] + */ +class MaterialHeader @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + SimpleComponent(context, attrs, 0), RefreshHeader { + + companion object { + + /** 刷新球大样式 */ + const val BALL_STYLE_LARGE: Int = 0 + + /** 刷新球默认样式 */ + const val BALL_STYLE_DEFAULT: Int = 1 + + private val CIRCLE_BG_LIGHT: Int = Color.parseColor("#FAFAFA") + private const val MAX_PROGRESS_ANGLE: Float = 0.8f + } + + private var finished: Boolean = false + private var circleDiameter: Int + private var circleView: ImageView + private var progressDrawable: MaterialProgressDrawable + private var waveHeight: Int = 0 + private var headHeight: Int = 0 + private var bezierPath: Path + private var bezierPaint: Paint + private var refreshState: RefreshState? = null + private var showBezierWave: Boolean = false + private var scrollableWhenRefreshing: Boolean = true + + init { + mSpinnerStyle = SpinnerStyle.MatchLayout + minimumHeight = resources.getDimension(R.dimen.dp_100).toInt() + progressDrawable = MaterialProgressDrawable(this) + progressDrawable.setColorSchemeColors( + Color.parseColor("#0099CC"), + Color.parseColor("#FF4444"), + Color.parseColor("#669900"), + Color.parseColor("#AA66CC"), + Color.parseColor("#FF8800")) + circleView = CircleImageView(context, CIRCLE_BG_LIGHT) + circleView.setImageDrawable(progressDrawable) + circleView.alpha = 0f + addView(circleView) + circleDiameter = resources.getDimension(R.dimen.dp_40).toInt() + bezierPath = Path() + bezierPaint = Paint() + bezierPaint.isAntiAlias = true + bezierPaint.style = Paint.Style.FILL + val typedArray: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialHeader) + showBezierWave = typedArray.getBoolean(R.styleable.MaterialHeader_srlShowBezierWave, showBezierWave) + scrollableWhenRefreshing = typedArray.getBoolean(R.styleable.MaterialHeader_srlScrollableWhenRefreshing, scrollableWhenRefreshing) + bezierPaint.color = typedArray.getColor(R.styleable.MaterialHeader_srlPrimaryColor, Color.parseColor("#11BBFF")) + if (typedArray.hasValue(R.styleable.MaterialHeader_srlShadowRadius)) { + val radius: Int = typedArray.getDimensionPixelOffset(R.styleable.MaterialHeader_srlShadowRadius, 0) + val color: Int = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, Color.parseColor("#000000")) + bezierPaint.setShadowLayer(radius.toFloat(), 0f, 0f, color) + setLayerType(LAYER_TYPE_SOFTWARE, null) + } + showBezierWave = typedArray.getBoolean(R.styleable.MaterialHeader_mhShowBezierWave, showBezierWave) + scrollableWhenRefreshing = typedArray.getBoolean(R.styleable.MaterialHeader_mhScrollableWhenRefreshing, scrollableWhenRefreshing) + if (typedArray.hasValue(R.styleable.MaterialHeader_mhPrimaryColor)) { + bezierPaint.color = typedArray.getColor(R.styleable.MaterialHeader_mhPrimaryColor, Color.parseColor("#11BBFF")) + } + if (typedArray.hasValue(R.styleable.MaterialHeader_mhShadowRadius)) { + val radius: Int = typedArray.getDimensionPixelOffset(R.styleable.MaterialHeader_mhShadowRadius, 0) + val color: Int = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, Color.parseColor("#000000")) + bezierPaint.setShadowLayer(radius.toFloat(), 0f, 0f, color) + setLayerType(LAYER_TYPE_SOFTWARE, null) + } + typedArray.recycle() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) + + circleView.measure(MeasureSpec.makeMeasureSpec(circleDiameter, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(circleDiameter, MeasureSpec.EXACTLY)) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + if (childCount == 0) { + return + } + val width: Int = measuredWidth + val circleWidth: Int = circleView.measuredWidth + val circleHeight: Int = circleView.measuredHeight + if (isInEditMode && headHeight > 0) { + val circleTop: Int = headHeight - circleHeight / 2 + circleView.layout((width / 2 - circleWidth / 2), circleTop, + (width / 2 + circleWidth / 2), circleTop + circleHeight) + progressDrawable.showArrow(true) + progressDrawable.setStartEndTrim(0f, MAX_PROGRESS_ANGLE) + progressDrawable.setArrowScale(1f) + circleView.alpha = 1f + circleView.visibility = VISIBLE + } else { + circleView.layout((width / 2 - circleWidth / 2), -circleHeight, (width / 2 + circleWidth / 2), 0) + } + } + + override fun dispatchDraw(canvas: Canvas) { + if (showBezierWave) { + // 重置画笔 + bezierPath.reset() + bezierPath.lineTo(0f, headHeight.toFloat()) + // 绘制贝塞尔曲线 + bezierPath.quadTo(measuredWidth / 2f, headHeight + waveHeight * 1.9f, measuredWidth.toFloat(), headHeight.toFloat()) + bezierPath.lineTo(measuredWidth.toFloat(), 0f) + canvas.drawPath(bezierPath, bezierPaint) + } + super.dispatchDraw(canvas) + } + + override fun onInitialized(kernel: RefreshKernel, height: Int, maxDragHeight: Int) { + if (!showBezierWave) { + kernel.requestDefaultTranslationContentFor(this, false) + } + if (isInEditMode) { + headHeight = height / 2 + waveHeight = headHeight + } + } + + override fun onMoving(dragging: Boolean, percent: Float, offset: Int, height: Int, maxDragHeight: Int) { + if (refreshState == RefreshState.Refreshing) { + return + } + if (showBezierWave) { + headHeight = min(offset, height) + waveHeight = max(0, offset - height) + postInvalidate() + } + if (dragging || (!progressDrawable.isRunning && !finished)) { + if (refreshState != RefreshState.Refreshing) { + val originalDragPercent: Float = 1f * offset / height + val dragPercent: Float = min(1f, abs(originalDragPercent)) + val adjustedPercent: Float = max(dragPercent - .4, 0.0).toFloat() * 5 / 3 + val extraOs: Float = (abs(offset) - height).toFloat() + val tensionSlingshotPercent: Float = max(0f, (min(extraOs, height.toFloat() * 2) / height.toFloat())) + val tensionPercent: Float = ((tensionSlingshotPercent / 4) - (tensionSlingshotPercent / 4).toDouble().pow(2.0)).toFloat() * 2f + val strokeStart: Float = adjustedPercent * .8f + progressDrawable.showArrow(true) + progressDrawable.setStartEndTrim(0f, min(MAX_PROGRESS_ANGLE, strokeStart)) + progressDrawable.setArrowScale(min(1f, adjustedPercent)) + val rotation: Float = (-0.25f + (.4f * adjustedPercent) + (tensionPercent * 2)) * .5f + progressDrawable.setProgressRotation(rotation) + } + val targetY: Float = offset / 2f + circleDiameter / 2f + circleView.translationY = min(offset.toFloat(), targetY) + circleView.alpha = min(1f, 4f * offset / circleDiameter) + } + } + + override fun onReleased(layout: RefreshLayout, height: Int, maxDragHeight: Int) { + progressDrawable.start() + } + + override fun onStateChanged(refreshLayout: RefreshLayout, oldState: RefreshState, newState: RefreshState) { + refreshState = newState + if (newState == RefreshState.PullDownToRefresh) { + finished = false + circleView.visibility = VISIBLE + circleView.translationY = 0f + circleView.scaleX = 1f + circleView.scaleY = 1f + } + } + + override fun onFinish(layout: RefreshLayout, success: Boolean): Int { + progressDrawable.stop() + circleView.animate().scaleX(0f).scaleY(0f) + finished = true + return 0 + } + + /** + * 设置背景色 + */ + fun setProgressBackgroundResource(@ColorRes id: Int): MaterialHeader = apply { + setProgressBackgroundColor(ContextCompat.getColor(context, id)) + } + + fun setProgressBackgroundColor(@ColorInt color: Int): MaterialHeader = apply { + circleView.setBackgroundColor(color) + } + + /** + * 设置 ColorScheme + * + * @param colors ColorScheme + */ + fun setColorSchemeColors(@ColorInt vararg colors: Int): MaterialHeader = apply { + progressDrawable.setColorSchemeColors(*colors) + } + + /** + * 设置 ColorScheme + * + * @param ids ColorSchemeResources + */ + fun setColorSchemeResources(@ColorRes vararg ids: Int): MaterialHeader = apply { + val colors = IntArray(ids.size) + for (i in ids.indices) { + colors[i] = ContextCompat.getColor(context, ids[i]) + } + setColorSchemeColors(*colors) + } + + /** + * 设置刷新球样式 + * + * @param style 可传入:[,#BALL_STYLE_DEFAULT][.BALL_STYLE_LARGE] + */ + fun setBallStyle(style: Int): MaterialHeader = apply { + if (style != BALL_STYLE_LARGE && style != BALL_STYLE_DEFAULT) { + return@apply + } + circleDiameter = if (style == BALL_STYLE_LARGE) resources.getDimension(R.dimen.dp_56).toInt() else resources.getDimension(R.dimen.dp_40).toInt() + // force the bounds of the progress circle inside the circle view to + // update by setting it to null before updating its size and then + // re-setting it + circleView.setImageDrawable(null) + progressDrawable.updateSizes(style) + circleView.setImageDrawable(progressDrawable) + } + + /** + * 是否显示贝塞尔图形 + */ + fun setShowBezierWave(show: Boolean): MaterialHeader = apply { + showBezierWave = show + } + + /** + * 设置实在正在刷新的时候可以上下滚动 Header + */ + fun setScrollableWhenRefreshing(scrollable: Boolean): MaterialHeader = apply { + scrollableWhenRefreshing = scrollable + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/PermissionCallback.kt b/app/src/main/java/com/hjq/demo/other/PermissionCallback.kt new file mode 100644 index 0000000..c97fe8c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/PermissionCallback.kt @@ -0,0 +1,199 @@ +package com.hjq.demo.other + +import android.app.Activity +import android.content.* +import android.os.* +import com.hjq.base.BaseDialog +import com.hjq.demo.manager.* +import com.hjq.demo.ui.dialog.MessageDialog +import com.hjq.permissions.OnPermissionCallback +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import com.hjq.toast.ToastUtils +import java.util.* +import com.hjq.demo.R + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/10/24 + * desc : 权限申请回调封装 + */ +abstract class PermissionCallback : OnPermissionCallback { + + override fun onDenied(permissions: MutableList, never: Boolean) { + if (never) { + showPermissionDialog(permissions) + return + } + if (permissions.size == 1 && (Permission.ACCESS_BACKGROUND_LOCATION == permissions[0])) { + ToastUtils.show(R.string.common_permission_fail_4) + return + } + ToastUtils.show(R.string.common_permission_fail_1) + } + + /** + * 显示授权对话框 + */ + protected fun showPermissionDialog(permissions: MutableList) { + val activity: Activity? = ActivityManager.getInstance().getTopActivity() + if ((activity == null) || activity.isFinishing || activity.isDestroyed) { + return + } + MessageDialog.Builder(activity) + .setTitle(R.string.common_permission_alert) + .setMessage(getPermissionHint(activity, permissions)) + .setConfirm(R.string.common_permission_goto) + .setCancel(null) + .setCancelable(false) + .setListener(object : MessageDialog.OnListener { + + override fun onConfirm(dialog: BaseDialog?) { + XXPermissions.startPermissionActivity(activity, permissions) + } + }) + .show() + } + + /** + * 根据权限获取提示 + */ + protected fun getPermissionHint(context: Context, permissions: MutableList): String { + if (permissions.isEmpty()) { + return context.getString(R.string.common_permission_fail_2) + } + val hints: MutableList = ArrayList() + for (permission: String? in permissions) { + when (permission) { + Permission.READ_EXTERNAL_STORAGE, + Permission.WRITE_EXTERNAL_STORAGE, + Permission.MANAGE_EXTERNAL_STORAGE -> { + val hint: String = context.getString(R.string.common_permission_storage) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.CAMERA -> { + val hint: String = context.getString(R.string.common_permission_camera) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.RECORD_AUDIO -> { + val hint: String = context.getString(R.string.common_permission_microphone) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.ACCESS_FINE_LOCATION, + Permission.ACCESS_COARSE_LOCATION, + Permission.ACCESS_BACKGROUND_LOCATION -> { + val hint: String = if (!permissions.contains(Permission.ACCESS_FINE_LOCATION) && + !permissions.contains(Permission.ACCESS_COARSE_LOCATION)) { + context.getString(R.string.common_permission_location_background) + } else { + context.getString(R.string.common_permission_location) + } + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.READ_PHONE_STATE, + Permission.CALL_PHONE, + Permission.ADD_VOICEMAIL, + Permission.USE_SIP, + Permission.READ_PHONE_NUMBERS, + Permission.ANSWER_PHONE_CALLS -> { + val hint: String = context.getString(R.string.common_permission_phone) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.GET_ACCOUNTS, + Permission.READ_CONTACTS, + Permission.WRITE_CONTACTS -> { + val hint: String = context.getString(R.string.common_permission_contacts) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.READ_CALENDAR, + Permission.WRITE_CALENDAR -> { + val hint: String = context.getString(R.string.common_permission_calendar) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.READ_CALL_LOG, + Permission.WRITE_CALL_LOG, + Permission.PROCESS_OUTGOING_CALLS -> { + val hint: String = context.getString(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) R.string.common_permission_call_log else R.string.common_permission_phone) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.BODY_SENSORS -> { + val hint: String = context.getString(R.string.common_permission_sensors) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.ACTIVITY_RECOGNITION -> { + val hint: String = context.getString(R.string.common_permission_activity_recognition) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.SEND_SMS, + Permission.RECEIVE_SMS, + Permission.READ_SMS, + Permission.RECEIVE_WAP_PUSH, + Permission.RECEIVE_MMS -> { + val hint: String = context.getString(R.string.common_permission_sms) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.REQUEST_INSTALL_PACKAGES -> { + val hint: String = context.getString(R.string.common_permission_install) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.NOTIFICATION_SERVICE -> { + val hint: String = context.getString(R.string.common_permission_notification) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.SYSTEM_ALERT_WINDOW -> { + val hint: String = context.getString(R.string.common_permission_window) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + Permission.WRITE_SETTINGS -> { + val hint: String = context.getString(R.string.common_permission_setting) + if (!hints.contains(hint)) { + hints.add(hint) + } + } + } + } + if (hints.isNotEmpty()) { + val builder: StringBuilder = StringBuilder() + for (text: String? in hints) { + if (builder.isEmpty()) { + builder.append(text) + } else { + builder.append("、") + .append(text) + } + } + builder.append(" ") + return context.getString(R.string.common_permission_fail_3, builder.toString()) + } + return context.getString(R.string.common_permission_fail_2) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.kt b/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.kt new file mode 100644 index 0000000..7588376 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.kt @@ -0,0 +1,149 @@ +package com.hjq.demo.other + +import android.animation.TimeInterpolator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.annotation.ColorInt +import androidx.core.graphics.ColorUtils +import com.hjq.demo.R +import com.scwang.smart.refresh.layout.api.RefreshFooter +import com.scwang.smart.refresh.layout.api.RefreshLayout +import com.scwang.smart.refresh.layout.constant.SpinnerStyle +import com.scwang.smart.refresh.layout.simple.SimpleComponent +import kotlin.math.min + +/** + * author : 树朾 & Android 轮子哥 + * github : https://github.com/scwang90/SmartRefreshLayout/tree/master/refresh-footer-ball + * time : 2020/08/01 + * desc : 球脉冲底部加载组件 + */ +class SmartBallPulseFooter @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + SimpleComponent(context, attrs, 0), RefreshFooter { + + private val interpolator: TimeInterpolator = AccelerateDecelerateInterpolator() + + private var noMoreData: Boolean = false + private var manualNormalColor: Boolean = false + private var manualAnimationColor: Boolean = false + private val paint: Paint = Paint() + private var normalColor: Int = Color.parseColor("#EEEEEE") + + private var animatingColor: IntArray = intArrayOf( + Color.parseColor("#30B399"), + Color.parseColor("#FF4600"), + Color.parseColor("#142DCC")) + + private val circleSpacing: Float + private var startTime: Long = 0 + private var started: Boolean = false + private val textWidth: Float + + init { + minimumHeight = resources.getDimension(R.dimen.dp_60).toInt() + paint.color = Color.WHITE + paint.style = Paint.Style.FILL + paint.isAntiAlias = true + mSpinnerStyle = SpinnerStyle.Translate + circleSpacing = resources.getDimension(R.dimen.dp_2) + paint.textSize = resources.getDimension(R.dimen.sp_14) + textWidth = paint.measureText(getContext().getString(R.string.common_no_more_data)) + } + + override fun dispatchDraw(canvas: Canvas) { + val width: Int = width + val height: Int = height + if (noMoreData) { + paint.color = Color.parseColor("#898989") + canvas.drawText(context.getString(R.string.common_no_more_data), + (width - textWidth) / 2, (height - paint.textSize) / 2, paint) + } else { + val radius: Float = (min(width, height) - circleSpacing * 2) / 7 + val x: Float = width / 2f - (radius * 2 + circleSpacing) + val y: Float = height / 2f + val now: Long = System.currentTimeMillis() + for (i in 0..2) { + val time: Long = now - startTime - (120 * (i + 1)) + var percent: Float = if (time > 0) ((time % 750) / 750f) else 0f + percent = interpolator.getInterpolation(percent) + canvas.save() + val translateX: Float = x + ((radius * 2) * i) + (circleSpacing * i) + if (percent < 0.5) { + val scale: Float = 1 - percent * 2 * 0.7f + val translateY: Float = y - scale * 10 + canvas.translate(translateX, translateY) + } else { + val scale: Float = percent * 2 * 0.7f - 0.4f + val translateY: Float = y + scale * 10 + canvas.translate(translateX, translateY) + } + paint.color = animatingColor[i % animatingColor.size] + canvas.drawCircle(0f, 0f, radius / 3, paint) + canvas.restore() + } + } + if (started) { + postInvalidate() + } + } + + override fun onStartAnimator(layout: RefreshLayout, height: Int, maxDragHeight: Int) { + if (started) { + return + } + invalidate() + started = true + startTime = System.currentTimeMillis() + } + + override fun onFinish(layout: RefreshLayout, success: Boolean): Int { + started = false + startTime = 0 + paint.color = normalColor + return 0 + } + + override fun setPrimaryColors(@ColorInt vararg colors: Int) { + if (!manualAnimationColor && colors.size > 1) { + setAnimatingColor(colors[0]) + manualAnimationColor = false + } + if (!manualNormalColor) { + if (colors.size > 1) { + setNormalColor(colors[1]) + } else if (colors.isNotEmpty()) { + setNormalColor(ColorUtils.compositeColors(Color.parseColor("#99FFFFFF"), colors[0])) + } + manualNormalColor = false + } + } + + override fun setNoMoreData(noMoreData: Boolean): Boolean { + this.noMoreData = noMoreData + return true + } + + fun setSpinnerStyle(style: SpinnerStyle?): SmartBallPulseFooter = apply { + mSpinnerStyle = style + } + + fun setNormalColor(@ColorInt color: Int): SmartBallPulseFooter = apply { + normalColor = color + manualNormalColor = true + if (!started) { + paint.color = color + } + } + + fun setAnimatingColor(@ColorInt color: Int): SmartBallPulseFooter = apply { + animatingColor = intArrayOf(color) + manualAnimationColor = true + if (started) { + paint.color = color + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/TitleBarStyle.kt b/app/src/main/java/com/hjq/demo/other/TitleBarStyle.kt new file mode 100644 index 0000000..716b838 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/TitleBarStyle.kt @@ -0,0 +1,80 @@ +package com.hjq.demo.other + +import android.content.* +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.ContextCompat +import com.hjq.bar.style.LightBarStyle +import com.hjq.demo.R +import com.hjq.widget.view.PressAlphaTextView + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2021/02/27 + * desc : 标题栏初始器 + */ +class TitleBarStyle : LightBarStyle() { + + override fun newTitleView(context: Context): TextView { + return AppCompatTextView(context) + } + + override fun newLeftView(context: Context): TextView { + return PressAlphaTextView(context) + } + + override fun newRightView(context: Context): TextView { + return PressAlphaTextView(context) + } + + override fun getTitleBarBackground(context: Context): Drawable { + return ColorDrawable(ContextCompat.getColor(context, R.color.common_primary_color)) + } + + override fun getBackButtonDrawable(context: Context): Drawable? { + return ContextCompat.getDrawable(context, R.drawable.arrows_left_ic) + } + + override fun getLeftTitleBackground(context: Context): Drawable? { + return null + } + + override fun getRightTitleBackground(context: Context): Drawable? { + return null + } + + override fun getChildHorizontalPadding(context: Context): Int { + return context.resources.getDimension(R.dimen.dp_12).toInt() + } + + override fun getChildVerticalPadding(context: Context): Int { + return context.resources.getDimension(R.dimen.dp_14).toInt() + } + + override fun getTitleSize(context: Context): Float { + return context.resources.getDimension(R.dimen.sp_15) + } + + override fun getLeftTitleSize(context: Context): Float { + return context.resources.getDimension(R.dimen.sp_13) + } + + override fun getRightTitleSize(context: Context): Float { + return context.resources.getDimension(R.dimen.sp_13) + } + + override fun getTitleIconPadding(context: Context): Int { + return context.resources.getDimension(R.dimen.dp_2).toInt() + } + + override fun getLeftIconPadding(context: Context): Int { + return context.resources.getDimension(R.dimen.dp_2).toInt() + } + + override fun getRightIconPadding(context: Context): Int { + return context.resources.getDimension(R.dimen.dp_2).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ToastLogInterceptor.kt b/app/src/main/java/com/hjq/demo/other/ToastLogInterceptor.kt new file mode 100644 index 0000000..cbc5c7b --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/ToastLogInterceptor.kt @@ -0,0 +1,42 @@ +package com.hjq.demo.other + +import com.hjq.demo.action.ToastAction +import com.hjq.toast.ToastUtils +import com.hjq.toast.config.IToastInterceptor +import timber.log.Timber + + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2020/11/04 + * desc : 自定义 Toast 拦截器(用于追踪 Toast 调用的位置) + */ +class ToastLogInterceptor : IToastInterceptor { + + override fun intercept(text: CharSequence): Boolean { + if (AppConfig.isLogEnable()) { + // 获取调用的堆栈信息 + val stackTrace: Array = Throwable().stackTrace + // 跳过最前面两个堆栈 + var i = 2 + while (stackTrace.size > 2 && i < stackTrace.size) { + + // 获取代码行数 + val lineNumber: Int = stackTrace[i].lineNumber + // 获取类的全路径 + val className: String = stackTrace[i].className + if (((lineNumber <= 0) || className.startsWith(ToastUtils::class.java.name) || + className.startsWith(ToastAction::class.java.name))) { + i++ + continue + } + Timber.tag("ToastUtils") + Timber.i("(%s:%s) %s", stackTrace[i].fileName, lineNumber, text.toString()) + break + i++ + } + } + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ToastStyle.kt b/app/src/main/java/com/hjq/demo/other/ToastStyle.kt new file mode 100644 index 0000000..d36264c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/ToastStyle.kt @@ -0,0 +1,37 @@ +package com.hjq.demo.other + +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import com.hjq.demo.R +import com.hjq.toast.style.BlackToastStyle + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2021/02/27 + * desc : Toast 样式配置 + */ +class ToastStyle : BlackToastStyle() { + + override fun getBackgroundDrawable(context: Context): Drawable { + val drawable = GradientDrawable() + // 设置颜色 + drawable.setColor(-0x78000000) + // 设置圆角 + drawable.cornerRadius = context.resources.getDimension(R.dimen.button_circle_size) + return drawable + } + + override fun getTextSize(context: Context): Float { + return context.resources.getDimension(R.dimen.sp_14) + } + + override fun getHorizontalPadding(context: Context): Int { + return context.resources.getDimension(R.dimen.sp_24).toInt() + } + + override fun getVerticalPadding(context: Context): Int { + return context.resources.getDimension(R.dimen.sp_16).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.kt b/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.kt new file mode 100644 index 0000000..14b3016 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.kt @@ -0,0 +1,21 @@ +package com.hjq.demo.ui.activity + +import com.hjq.demo.R +import com.hjq.demo.app.AppActivity + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : 关于界面 + */ +class AboutActivity : AppActivity() { + + override fun getLayoutId(): Int { + return R.layout.about_activity + } + + override fun initView() {} + + override fun initData() {} +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/BrowserActivity.kt b/app/src/main/java/com/hjq/demo/ui/activity/BrowserActivity.kt new file mode 100644 index 0000000..5d17d59 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/ui/activity/BrowserActivity.kt @@ -0,0 +1,170 @@ +package com.hjq.demo.ui.activity + +import android.app.Activity +import android.content.* +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.text.TextUtils +import android.view.* +import android.webkit.WebView +import android.widget.ProgressBar +import com.hjq.demo.R +import com.hjq.demo.action.StatusAction +import com.hjq.demo.aop.CheckNet +import com.hjq.demo.aop.Log +import com.hjq.demo.app.AppActivity +import com.hjq.demo.widget.BrowserView +import com.hjq.demo.widget.BrowserView.BrowserChromeClient +import com.hjq.demo.widget.BrowserView.BrowserViewClient +import com.hjq.demo.widget.StatusLayout +import com.hjq.demo.widget.StatusLayout.OnRetryListener +import com.scwang.smart.refresh.layout.SmartRefreshLayout +import com.scwang.smart.refresh.layout.api.RefreshLayout +import com.scwang.smart.refresh.layout.listener.OnRefreshListener + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : 浏览器界面 + */ +class BrowserActivity : AppActivity(), StatusAction, OnRefreshListener { + + companion object { + + const val INTENT_KEY_IN_URL: String = "url" + + @CheckNet + @Log + fun start(context: Context, url: String) { + if (TextUtils.isEmpty(url)) { + return + } + val intent = Intent(context, BrowserActivity::class.java) + intent.putExtra(INTENT_KEY_IN_URL, url) + if (context !is Activity) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + } + + private val hintLayout: StatusLayout? by lazy { findViewById(R.id.hl_browser_hint) } + private val progressBar: ProgressBar? by lazy { findViewById(R.id.pb_browser_progress) } + private val refreshLayout: SmartRefreshLayout? by lazy { findViewById(R.id.sl_browser_refresh) } + private val browserView: BrowserView? by lazy { findViewById(R.id.wv_browser_view) } + + override fun getLayoutId(): Int { + return R.layout.browser_activity + } + + override fun initView() { + // 设置 WebView 生命管控 + browserView?.setLifecycleOwner(this) + // 设置网页刷新监听 + refreshLayout?.setOnRefreshListener(this) + } + + override fun initData() { + showLoading() + browserView?.apply { + setBrowserViewClient(AppBrowserViewClient()) + setBrowserChromeClient(AppBrowserChromeClient(this)) + loadUrl(getString(INTENT_KEY_IN_URL)!!) + } + } + + override fun getStatusLayout(): StatusLayout? { + return hintLayout + } + + override fun onLeftClick(view: View) { + finish() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + browserView?.apply { + if (keyCode == KeyEvent.KEYCODE_BACK && canGoBack()) { + // 后退网页并且拦截该事件 + goBack() + return true + } + } + return super.onKeyDown(keyCode, event) + } + + /** + * 重新加载当前页 + */ + @CheckNet + private fun reload() { + browserView?.reload() + } + + /** + * [OnRefreshListener] + */ + override fun onRefresh(refreshLayout: RefreshLayout) { + reload() + } + + private inner class AppBrowserViewClient : BrowserViewClient() { + + /** + * 网页加载错误时回调,这个方法会在 onPageFinished 之前调用 + */ + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + // 这里为什么要用延迟呢?因为加载出错之后会先调用 onReceivedError 再调用 onPageFinished + post { + showError(object : OnRetryListener { + override fun onRetry(layout: StatusLayout) { + reload() + } + }) + } + } + + /** + * 开始加载网页 + */ + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + progressBar?.visibility = View.VISIBLE + } + + /** + * 完成加载网页 + */ + override fun onPageFinished(view: WebView, url: String) { + progressBar?.visibility = View.GONE + refreshLayout?.finishRefresh() + showComplete() + } + } + + private inner class AppBrowserChromeClient constructor(view: BrowserView) : BrowserChromeClient(view) { + + /** + * 收到网页标题 + */ + override fun onReceivedTitle(view: WebView, title: String?) { + if (title == null) { + return + } + setTitle(title) + } + + override fun onReceivedIcon(view: WebView, icon: Bitmap?) { + if (icon == null) { + return + } + setRightIcon(BitmapDrawable(resources, icon)) + } + + /** + * 收到加载进度变化 + */ + override fun onProgressChanged(view: WebView, newProgress: Int) { + progressBar?.progress = newProgress + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/CameraActivity.kt b/app/src/main/java/com/hjq/demo/ui/activity/CameraActivity.kt new file mode 100644 index 0000000..a228cfd --- /dev/null +++ b/app/src/main/java/com/hjq/demo/ui/activity/CameraActivity.kt @@ -0,0 +1,168 @@ +package com.hjq.demo.ui.activity + +import android.content.Intent +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.* +import android.provider.MediaStore +import androidx.core.content.FileProvider +import com.hjq.base.BaseActivity +import com.hjq.demo.R +import com.hjq.demo.aop.Log +import com.hjq.demo.aop.Permissions +import com.hjq.demo.app.AppActivity +import com.hjq.demo.other.AppConfig +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/12/18 + * desc : 拍摄图片、视频 + */ +class CameraActivity : AppActivity() { + + companion object { + + const val INTENT_KEY_IN_FILE: String = "file" + const val INTENT_KEY_IN_VIDEO: String = "video" + const val INTENT_KEY_OUT_ERROR: String = "error" + + fun start(activity: BaseActivity, listener: OnCameraListener?) { + start(activity, false, listener) + } + + @Log + @Permissions(Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_EXTERNAL_STORAGE, Permission.CAMERA) + fun start(activity: BaseActivity, video: Boolean, listener: OnCameraListener?) { + val file: File = createCameraFile(video) + val intent = Intent(activity, CameraActivity::class.java) + intent.putExtra(INTENT_KEY_IN_FILE, file) + intent.putExtra(INTENT_KEY_IN_VIDEO, video) + activity.startActivityForResult(intent, object : OnActivityCallback { + + override fun onActivityResult(resultCode: Int, data: Intent?) { + if (listener == null) { + return + } + when (resultCode) { + RESULT_OK -> { + if (file.isFile) { + listener.onSelected(file) + } else { + listener.onCancel() + } + } + RESULT_ERROR -> { + var details: String? = null + if (data != null) { + details = data.getStringExtra(INTENT_KEY_OUT_ERROR) + } + if (details == null) { + details = activity.getString(R.string.common_unknown_error) + } + listener.onError(details) + } + RESULT_CANCELED -> listener.onCancel() + else -> listener.onCancel() + } + } + }) + } + + /** + * 创建一个拍照图片文件路径 + */ + @Suppress("deprecation") + private fun createCameraFile(video: Boolean): File { + var folder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera") + if (!folder.exists() || !folder.isDirectory) { + if (!folder.mkdirs()) { + folder = Environment.getExternalStorageDirectory() + } + } + return File(folder, ((if (video) "VID" else "IMG") + "_" + + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + + (if (video) ".mp4" else ".jpg"))) + } + } + + override fun getLayoutId(): Int { + return 0 + } + + override fun initView() {} + + override fun initData() { + val intent = Intent() + // 启动系统相机 + if (getBoolean(INTENT_KEY_IN_VIDEO)) { + // 录制视频 + intent.action = MediaStore.ACTION_VIDEO_CAPTURE + } else { + // 拍摄照片 + intent.action = MediaStore.ACTION_IMAGE_CAPTURE + } + if (intent.resolveActivity(packageManager) == null || !XXPermissions.isGranted(this, + Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE, Permission.CAMERA)) { + setResult(RESULT_ERROR, Intent().putExtra(INTENT_KEY_OUT_ERROR, getString(R.string.camera_launch_fail))) + finish() + return + } + val file: File? = getSerializable(INTENT_KEY_IN_FILE) + if (file == null) { + setResult(RESULT_ERROR, Intent().putExtra(INTENT_KEY_OUT_ERROR, getString(R.string.camera_image_error))) + finish() + return + } + val imageUri: Uri? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // 通过 FileProvider 创建一个 Content 类型的 Uri 文件 + FileProvider.getUriForFile(this, AppConfig.getPackageName() + ".provider", file) + } else { + Uri.fromFile(file) + } + // 对目标应用临时授权该 Uri 所代表的文件 + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + // 将拍取的照片保存到指定 Uri + intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri) + startActivityForResult(intent, object : OnActivityCallback { + override fun onActivityResult(resultCode: Int, data: Intent?) { + if (resultCode == RESULT_OK) { + // 通知系统多媒体扫描该文件,否则会导致拍摄出来的图片或者视频没有及时显示到相册中,而需要通过重启手机才能看到 + MediaScannerConnection.scanFile(applicationContext, arrayOf(file.path), null, null) + } + setResult(resultCode) + finish() + } + }) + } + + /** + * 拍照选择监听 + */ + interface OnCameraListener { + + /** + * 选择回调 + * + * @param file 文件 + */ + fun onSelected(file: File) + + /** + * 错误回调 + * + * @param details 错误详情 + */ + fun onError(details: String) + + /** + * 取消回调 + */ + fun onCancel() {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.kt b/app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.kt new file mode 100644 index 0000000..3738e87 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.kt @@ -0,0 +1,21 @@ +package com.hjq.demo.ui.activity + +import com.hjq.demo.R +import com.hjq.demo.app.AppActivity + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/10/18 + * desc : 可进行拷贝的副本 + */ +class CopyActivity : AppActivity() { + + override fun getLayoutId(): Int { + return R.layout.copy_activity + } + + override fun initView() {} + + override fun initData() {} +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/CrashActivity.kt b/app/src/main/java/com/hjq/demo/ui/activity/CrashActivity.kt new file mode 100644 index 0000000..8568db9 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/ui/activity/CrashActivity.kt @@ -0,0 +1,285 @@ +package com.hjq.demo.ui.activity + +import android.Manifest +import android.app.Application +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.Color +import android.os.Build +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextUtils +import android.text.style.ForegroundColorSpan +import android.text.style.UnderlineSpan +import android.util.DisplayMetrics +import android.view.View +import android.widget.TextView +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import com.gyf.immersionbar.ImmersionBar +import com.hjq.demo.R +import com.hjq.demo.aop.SingleClick +import com.hjq.demo.app.AppActivity +import com.hjq.demo.manager.ThreadPoolManager +import com.hjq.demo.other.AppConfig +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import com.tencent.bugly.crashreport.CrashReport +import java.io.PrintWriter +import java.io.StringWriter +import java.net.InetAddress +import java.net.UnknownHostException +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern +import kotlin.math.min + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2019/06/27 + * desc : 崩溃捕捉界面 + */ +class CrashActivity : AppActivity() { + + companion object { + + private const val INTENT_KEY_IN_THROWABLE: String = "throwable" + + /** 系统包前缀列表 */ + private val SYSTEM_PACKAGE_PREFIX_LIST: Array = arrayOf("android", "com.android", + "androidx", "com.google.android", "java", "javax", "dalvik", "kotlin") + + /** 报错代码行数正则表达式 */ + private val CODE_REGEX: Pattern = Pattern.compile("\\(\\w+\\.\\w+:\\d+\\)") + + fun start(application: Application, throwable: Throwable?) { + if (throwable == null) { + return + } + val intent = Intent(application, CrashActivity::class.java) + intent.putExtra(INTENT_KEY_IN_THROWABLE, throwable) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + application.startActivity(intent) + } + } + + private val titleView: TextView? by lazy { findViewById(R.id.tv_crash_title) } + private val drawerLayout: DrawerLayout? by lazy { findViewById(R.id.dl_crash_drawer) } + private val infoView: TextView? by lazy { findViewById(R.id.tv_crash_info) } + private val messageView: TextView? by lazy { findViewById(R.id.tv_crash_message) } + private var stackTrace: String? = null + + override fun getLayoutId(): Int { + return R.layout.crash_activity + } + + override fun initView() { + setOnClickListener(R.id.iv_crash_info, R.id.iv_crash_share, R.id.iv_crash_restart) + + // 设置状态栏沉浸 + ImmersionBar.setTitleBar(this, findViewById(R.id.ll_crash_bar)) + ImmersionBar.setTitleBar(this, findViewById(R.id.ll_crash_info)) + } + + override fun initData() { + val throwable: Throwable = getSerializable(INTENT_KEY_IN_THROWABLE) ?: return + titleView?.text = throwable.javaClass.simpleName + val stringWriter = StringWriter() + val printWriter = PrintWriter(stringWriter) + throwable.printStackTrace(printWriter) + throwable.cause?.printStackTrace(printWriter) + stackTrace = stringWriter.toString() + val matcher: Matcher = CODE_REGEX.matcher(stackTrace!!) + val spannable = SpannableStringBuilder(stackTrace) + if (spannable.isNotEmpty()) { + while (matcher.find()) { + // 不包含左括号( + val start: Int = matcher.start() + "(".length + // 不包含右括号 ) + val end: Int = matcher.end() - ")".length + + // 代码信息颜色 + var codeColor: Int = Color.parseColor("#999999") + val lineIndex: Int = stackTrace!!.lastIndexOf("at ", start) + if (lineIndex != -1) { + val lineData: String = spannable.subSequence(lineIndex, start).toString() + if (TextUtils.isEmpty(lineData)) { + continue + } + // 是否高亮代码行数 + var highlight = true + for (packagePrefix: String? in SYSTEM_PACKAGE_PREFIX_LIST) { + if (lineData.startsWith("at $packagePrefix")) { + highlight = false + break + } + } + if (highlight) { + codeColor = Color.parseColor("#287BDE") + } + } + + // 设置前景 + spannable.setSpan(ForegroundColorSpan(codeColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + // 设置下划线 + spannable.setSpan(UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + messageView?.text = spannable + } + val displayMetrics: DisplayMetrics = resources.displayMetrics + val screenWidth: Int = displayMetrics.widthPixels + val screenHeight: Int = displayMetrics.heightPixels + val smallestWidth: Float = min(screenWidth, screenHeight) / displayMetrics.density + val targetResource: String? + when { + displayMetrics.densityDpi > 480 -> { + targetResource = "xxxhdpi" + } + displayMetrics.densityDpi > 320 -> { + targetResource = "xxhdpi" + } + displayMetrics.densityDpi > 240 -> { + targetResource = "xhdpi" + } + displayMetrics.densityDpi > 160 -> { + targetResource = "hdpi" + } + displayMetrics.densityDpi > 120 -> { + targetResource = "mdpi" + } + else -> { + targetResource = "ldpi" + } + } + val builder: StringBuilder = StringBuilder() + builder.append("设备品牌:\t").append(Build.BRAND) + .append("\n设备型号:\t").append(Build.MODEL) + .append("\n设备类型:\t").append(if (isTablet()) "平板" else "手机") + + builder.append("\n屏幕宽高:\t").append(screenWidth).append(" x ").append(screenHeight) + .append("\n屏幕密度:\t").append(displayMetrics.densityDpi) + .append("\n密度像素:\t").append(displayMetrics.density) + .append("\n目标资源:\t").append(targetResource) + .append("\n最小宽度:\t").append(smallestWidth.toInt()) + + builder.append("\n安卓版本:\t").append(Build.VERSION.RELEASE) + .append("\nAPI 版本:\t").append(Build.VERSION.SDK_INT) + .append("\nCPU 架构:\t").append(Build.SUPPORTED_ABIS[0]) + + builder.append("\n应用版本:\t").append(AppConfig.getVersionName()) + .append("\n版本代码:\t").append(AppConfig.getVersionCode()) + + try { + val dateFormat = SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()) + val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS) + builder.append("\n首次安装:\t") + .append(dateFormat.format(Date(packageInfo.firstInstallTime))) + .append("\n最近安装:\t").append(dateFormat.format(Date(packageInfo.lastUpdateTime))) + .append("\n崩溃时间:\t").append(dateFormat.format(Date())) + val permissions: MutableList = mutableListOf(*packageInfo.requestedPermissions) + if (permissions.contains(Permission.READ_EXTERNAL_STORAGE) || + permissions.contains(Permission.WRITE_EXTERNAL_STORAGE)) { + builder.append("\n存储权限:\t").append( + if (XXPermissions.isGranted(this, *Permission.Group.STORAGE)) "已获得" else "未获得" + ) + } + if (permissions.contains(Permission.ACCESS_FINE_LOCATION) || + permissions.contains(Permission.ACCESS_COARSE_LOCATION)) { + builder.append("\n定位权限:\t") + if (XXPermissions.isGranted(this, Permission.ACCESS_FINE_LOCATION, Permission.ACCESS_COARSE_LOCATION)) { + builder.append("精确、粗略") + } else { + when { + XXPermissions.isGranted(this, Permission.ACCESS_FINE_LOCATION) -> { + builder.append("精确") + } + XXPermissions.isGranted(this, Permission.ACCESS_COARSE_LOCATION) -> { + builder.append("粗略") + } + else -> { + builder.append("未获得") + } + } + } + } + if (permissions.contains(Permission.CAMERA)) { + builder.append("\n相机权限:\t") + .append(if (XXPermissions.isGranted(this, Permission.CAMERA)) "已获得" else "未获得") + } + if (permissions.contains(Permission.RECORD_AUDIO)) { + builder.append("\n录音权限:\t").append( + if (XXPermissions.isGranted(this, Permission.RECORD_AUDIO)) "已获得" else "未获得" + ) + } + if (permissions.contains(Permission.SYSTEM_ALERT_WINDOW)) { + builder.append("\n悬浮窗权限:\t").append( + if (XXPermissions.isGranted(this, Permission.SYSTEM_ALERT_WINDOW)) "已获得" else "未获得" + ) + } + if (permissions.contains(Permission.REQUEST_INSTALL_PACKAGES)) { + builder.append("\n安装包权限:\t").append( + if (XXPermissions.isGranted(this, Permission.REQUEST_INSTALL_PACKAGES)) "已获得" else "未获得" + ) + } + if (permissions.contains(Manifest.permission.INTERNET)) { + builder.append("\n当前网络访问:\t") + ThreadPoolManager.getInstance().execute { + try { + InetAddress.getByName("www.baidu.com") + builder.append("正常") + } catch (ignored: UnknownHostException) { + builder.append("异常") + } + post { infoView?.text = builder } + } + } else { + infoView?.text = builder + } + } catch (e: PackageManager.NameNotFoundException) { + CrashReport.postCatchedException(e) + } + } + + @SingleClick + override fun onClick(view: View) { + when (view.id) { + R.id.iv_crash_info -> { + drawerLayout?.openDrawer(GravityCompat.START) + } + R.id.iv_crash_share -> { + // 分享文本 + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, stackTrace) + startActivity(Intent.createChooser(intent, "")) + } + R.id.iv_crash_restart -> { + onBackPressed() + } + } + } + + override fun onBackPressed() { + // 重启应用 + RestartActivity.restart(this) + finish() + } + + override fun createStatusBarConfig(): ImmersionBar { + return super.createStatusBarConfig() // 指定导航栏背景颜色 + .navigationBarColor(R.color.white) + } + + /** + * 判断当前设备是否是平板 + */ + fun isTablet(): Boolean { + return ((resources.configuration.screenLayout + and Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.kt b/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.kt new file mode 100644 index 0000000..79ac47b --- /dev/null +++ b/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.kt @@ -0,0 +1,540 @@ +package com.hjq.demo.ui.activity + +import android.content.Intent +import android.view.* +import android.widget.* +import com.hjq.base.BaseDialog +import com.hjq.base.BasePopupWindow +import com.hjq.base.action.AnimAction +import com.hjq.demo.R +import com.hjq.demo.aop.SingleClick +import com.hjq.demo.app.AppActivity +import com.hjq.demo.manager.DialogManager +import com.hjq.demo.ui.dialog.* +import com.hjq.demo.ui.popup.ListPopup +import com.hjq.umeng.Platform +import com.hjq.umeng.UmengClient +import com.hjq.umeng.UmengShare.OnShareListener +import com.umeng.socialize.media.UMImage +import com.umeng.socialize.media.UMWeb +import java.util.* + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject-Kotlin + * time : 2018/12/02 + * desc : 对话框使用案例 + */ +class DialogActivity : AppActivity() { + + /** 等待对话框 */ + private var waitDialog: BaseDialog? = null + + override fun getLayoutId(): Int { + return R.layout.dialog_activity + } + + override fun initView() { + setOnClickListener(R.id.btn_dialog_message, R.id.btn_dialog_input, + R.id.btn_dialog_bottom_menu, R.id.btn_dialog_center_menu, + R.id.btn_dialog_single_select, R.id.btn_dialog_more_select, + R.id.btn_dialog_succeed_toast, R.id.btn_dialog_fail_toast, + R.id.btn_dialog_warn_toast, R.id.btn_dialog_wait, + R.id.btn_dialog_pay, R.id.btn_dialog_address, + R.id.btn_dialog_date, R.id.btn_dialog_time, + R.id.btn_dialog_update, R.id.btn_dialog_share, + R.id.btn_dialog_safe, R.id.btn_dialog_custom, + R.id.btn_dialog_multi + ) + } + + override fun initData() {} + + @SingleClick + override fun onClick(view: View) { + when (view.id) { + R.id.btn_dialog_message -> { + + // 消息对话框 + MessageDialog.Builder(this) + // 标题可以不用填写 + .setTitle("我是标题") + // 内容必须要填写 + .setMessage("我是内容") + // 确定按钮文本 + .setConfirm(getString(R.string.common_confirm)) + // 设置 null 表示不显示取消按钮 + .setCancel(getString(R.string.common_cancel)) + // 设置点击按钮后不关闭对话框 + //.setAutoDismiss(false) + .setListener(object : MessageDialog.OnListener { + + override fun onConfirm(dialog: BaseDialog?) { + toast("确定了") + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + } + R.id.btn_dialog_input -> { + + // 输入对话框 + InputDialog.Builder(this) + // 标题可以不用填写 + .setTitle("我是标题") + // 内容可以不用填写 + .setContent("我是内容") + // 提示可以不用填写 + .setHint("我是提示") + // 确定按钮文本 + .setConfirm(getString(R.string.common_confirm)) + // 设置 null 表示不显示取消按钮 + .setCancel(getString(R.string.common_cancel)) + // 设置点击按钮后不关闭对话框 + //.setAutoDismiss(false) + .setListener(object : InputDialog.OnListener { + + override fun onConfirm(dialog: BaseDialog?, content: String) { + toast("确定了:$content") + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + } + R.id.btn_dialog_bottom_menu -> { + + val data = ArrayList() + for (i in 0..9) { + data.add("我是数据" + (i + 1)) + } + + // 底部选择框 + MenuDialog.Builder(this) + // 设置 null 表示不显示取消按钮 + //.setCancel(getString(R.string.common_cancel)) + // 设置点击按钮后不关闭对话框 + //.setAutoDismiss(false) + .setList(data) + .setListener(object : MenuDialog.OnListener { + + override fun onSelected(dialog: BaseDialog?, position: Int, data: String) { + toast("位置:$position,文本:$data") + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_center_menu -> { + + val data = ArrayList() + for (i in 0..9) { + data.add("我是数据" + (i + 1)) + } + // 居中选择框 + MenuDialog.Builder(this) + .setGravity(Gravity.CENTER) + // 设置 null 表示不显示取消按钮 + //.setCancel(null) + // 设置点击按钮后不关闭对话框 + //.setAutoDismiss(false) + .setList(data) + .setListener(object : MenuDialog.OnListener { + + override fun onSelected(dialog: BaseDialog?, position: Int, data: String) { + toast("位置:$position,文本:$data") + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_single_select -> { + + // 单选对话框 + SelectDialog.Builder(this) + .setTitle("请选择你的性别") + .setList("男", "女") + // 设置单选模式 + .setSingleSelect() + // 设置默认选中 + .setSelect(0) + .setListener(object : SelectDialog.OnListener { + + override fun onSelected(dialog: BaseDialog?, data: HashMap) { + toast("确定了:$data") + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_more_select -> { + + // 多选对话框 + SelectDialog.Builder(this) + .setTitle("请选择工作日") + .setList("星期一", "星期二", "星期三", "星期四", "星期五") + // 设置最大选择数 + .setMaxSelect(3) + // 设置默认选中 + .setSelect(2, 3, 4) + .setListener(object : SelectDialog.OnListener { + + override fun onSelected(dialog: BaseDialog?, data: HashMap) { + toast("确定了:$data") + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_succeed_toast -> { + + // 成功对话框 + TipsDialog.Builder(this) + .setIcon(TipsDialog.ICON_FINISH) + .setMessage("完成") + .show() + + } + R.id.btn_dialog_fail_toast -> { + + // 失败对话框 + TipsDialog.Builder(this) + .setIcon(TipsDialog.ICON_ERROR) + .setMessage("错误") + .show() + + } + R.id.btn_dialog_warn_toast -> { + + // 警告对话框 + TipsDialog.Builder(this) + .setIcon(TipsDialog.ICON_WARNING) + .setMessage("警告") + .show() + + } + R.id.btn_dialog_wait -> { + + if (waitDialog == null) { + waitDialog = WaitDialog.Builder(this) // 消息文本可以不用填写 + .setMessage(getString(R.string.common_loading)) + .create() + } + waitDialog?.apply { + if (!isShowing) { + show() + postDelayed({ dismiss() }, 2000) + } + } + + } + R.id.btn_dialog_pay -> { + + // 支付密码输入对话框 + PayPasswordDialog.Builder(this) + .setTitle(getString(R.string.pay_title)) + .setSubTitle("用于购买一个女盆友") + .setMoney("¥ 100.00") + // 设置点击按钮后不关闭对话框 + //.setAutoDismiss(false) + .setListener(object : PayPasswordDialog.OnListener { + + override fun onCompleted(dialog: BaseDialog?, password: String) { + toast(password) + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_address -> { + + // 选择地区对话框 + AddressDialog.Builder(this) + .setTitle(getString(R.string.address_title)) // 设置默认省份 + //.setProvince("广东省") + // 设置默认城市(必须要先设置默认省份) + //.setCity("广州市") + // 不选择县级区域 + //.setIgnoreArea() + .setListener(object : AddressDialog.OnListener { + + override fun onSelected(dialog: BaseDialog?, province: String, city: String, area: String) { + toast(province + city + area) + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_date -> { + + // 日期选择对话框 + DateDialog.Builder(this) + .setTitle(getString(R.string.date_title)) + // 确定按钮文本 + .setConfirm(getString(R.string.common_confirm)) + // 设置 null 表示不显示取消按钮 + .setCancel(getString(R.string.common_cancel)) + // 设置日期 + //.setDate("2018-12-31") + //.setDate("20181231") + //.setDate(1546263036137) + // 设置年份 + //.setYear(2018) + // 设置月份 + //.setMonth(2) + // 设置天数 + //.setDay(20) + // 不选择天数 + //.setIgnoreDay() + .setListener(object : DateDialog.OnListener { + + override fun onSelected(dialog: BaseDialog?, year: Int, month: Int, day: Int) { + toast(year.toString() + getString(R.string.common_year) + month + + getString(R.string.common_month) + day + getString(R.string.common_day)) + + // 如果不指定时分秒则默认为现在的时间 + val calendar: Calendar = Calendar.getInstance() + calendar.set(Calendar.YEAR, year) + // 月份从零开始,所以需要减 1 + calendar.set(Calendar.MONTH, month - 1) + calendar.set(Calendar.DAY_OF_MONTH, day) + toast("时间戳:" + calendar.timeInMillis) + //toast(new SimpleDateFormat("yyyy年MM月dd日 kk:mm:ss").format(calendar.getTime())); + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_time -> { + + // 时间选择对话框 + TimeDialog.Builder(this) + .setTitle(getString(R.string.time_title)) + // 确定按钮文本 + .setConfirm(getString(R.string.common_confirm)) + // 设置 null 表示不显示取消按钮 + .setCancel(getString(R.string.common_cancel)) + // 设置时间 + //.setTime("23:59:59") + //.setTime("235959") + // 设置小时 + //.setHour(23) + // 设置分钟 + //.setMinute(59) + // 设置秒数 + //.setSecond(59) + // 不选择秒数 + //.setIgnoreSecond() + .setListener(object : TimeDialog.OnListener { + + override fun onSelected(dialog: BaseDialog?, hour: Int, minute: Int, second: Int) { + toast(hour.toString() + getString(R.string.common_hour) + minute + getString( + R.string.common_minute + ) + second + getString(R.string.common_second)) + + // 如果不指定年月日则默认为今天的日期 + val calendar: Calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, hour) + calendar.set(Calendar.MINUTE, minute) + calendar.set(Calendar.SECOND, second) + toast("时间戳:" + calendar.timeInMillis) + //toast(new SimpleDateFormat("yyyy年MM月dd日 kk:mm:ss").format(calendar.getTime())); + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + + } + R.id.btn_dialog_share -> { + + toast("记得改好第三方 AppID 和 Secret,否则会调不起来哦") + val content = UMWeb("https://github.com/getActivity/AndroidProject-Kotlin") + content.title = "Github" + content.setThumb(UMImage(this, R.mipmap.launcher_ic)) + content.description = getString(R.string.app_name) + + // 分享对话框 + ShareDialog.Builder(this) + .setShareLink(content) + .setListener(object : OnShareListener { + + override fun onSucceed(platform: Platform?) { + toast("分享成功") + } + + override fun onError(platform: Platform?, t: Throwable) { + toast(t.message) + } + + override fun onCancel(platform: Platform?) { + toast("分享取消") + } + }) + .show() + + } + R.id.btn_dialog_update -> { + + // 升级对话框 + UpdateDialog.Builder(this) + // 版本名 + .setVersionName("5.2.0") + // 是否强制更新 + .setForceUpdate(false) + // 更新日志 + .setUpdateLog("到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥") + // 下载 URL + .setDownloadUrl("https://dldir1.qq.com/weixin/android/weixin807android1920_arm64.apk") + // 文件 MD5 + .setFileMd5("df2f045dfa854d8461d9cefe08b813c8") + .show() + + } + R.id.btn_dialog_safe -> { + + // 身份校验对话框 + SafeDialog.Builder(this) + .setListener(object : SafeDialog.OnListener { + override fun onConfirm(dialog: BaseDialog?, phone: String, code: String) { + toast("手机号:$phone\n验证码:$code") + } + + override fun onCancel(dialog: BaseDialog?) { + toast("取消了") + } + }) + .show() + } + R.id.btn_dialog_custom -> { + + // 自定义对话框 + BaseDialog.Builder>(this) + .setContentView(R.layout.custom_dialog) + .setAnimStyle(AnimAction.ANIM_SCALE) //.setText(id, "我是预设置的文本") + .setOnClickListener(R.id.btn_dialog_custom_ok, object : BaseDialog.OnClickListener