更名为安卓技术中台

适配 Android 11
升级框架版本
新增崩溃自动重启机制
新增常见问题解答文档
新增对 BuildType 的定制化
新增 Dialog 排队显示机制
新增 Timber 日志打印框架
新增图片裁剪功能
新增输入框抖动动画
新增提交按钮动画
新增 Fragment 按键事件回调
新增 Fragment onFragmentResume 回调
新增 Fragment onActivityResume 回调
新增 BottomSheetDialog 类
新增 TitleBarFragment 类
新增 BrowserFragment 类
新增 RecylcerPagerAdapter 类
新增 SlantedTextView 自定义控件
新增 Toast 调用追踪策略
新增权限申请失败提示对话框
新增 WebView 调试模式开关控制
新增支持 WebView 打电话功能
新增支持 WebView 定位功能
新增支持显示升级通知栏进度条
新增支持 DIalog 创建监听
新增支持 PopupWindow 创建监听
新增支持网络状态变化提醒策略
优化代码嵌套
优化代码警告
优化基类命名
优化混淆规则
优化 AOP 配置
优化 Git 忽略规则
优化启动页白屏的问题
优化 FileProvider 配置
优化 View ID 判断逻辑
优化 Color ID 命名方式
优化 WebView Https 证书校验
优化 startActivityForResult 逻辑
优化 SingleClick 方法切面逻辑
优化 Glide 在低内存下的缓存处理逻辑
优化 so 库在 debug 模式下的编译速度
优化首页软键盘弹出闪动的问题
优化查看图片界面指示器逻辑
优化视频播放界面提示逻辑
优化图片等比拉伸的问题
美化 Toast 样式
美化刷新加载样式
更新加载中 Lottie 动画
更新指示器技术选型
移除 Logcat 框架
移除 AndResGuard 插件
移除 RecyclerView 滚动监听封装
移除清单文件中的 configChanges 属性
更多一些细节变化请查看 Git 修改记录
This commit is contained in:
Android 轮子哥 2021-02-22 15:58:10 +08:00
parent 5885540419
commit c1dcac57c3
265 changed files with 8730 additions and 5471 deletions

2
.gitignore vendored
View File

@ -4,8 +4,10 @@
*/build
/captures
/.cxx
*/.cxx
/.externalNativeBuild
._*
*.iml
.DS_Store
local.properties

Binary file not shown.

289
HelpDoc.md Normal file
View File

@ -0,0 +1,289 @@
#### 目录
* [为什么没有用 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-的封装)
* [为什么不用 Dagger 框架](#为什么不用-dagger-框架)
* [这不就是一个模板工程换成我也能写一个](#这不就是一个模板工程换成我也能写一个)
* [轮子哥你怎么看待层出不穷的新技术](#轮子哥你怎么看待层出不穷的新技术)
#### 为什么没有用 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 面对的是大众开发者,所以更倾向中小型的项目代码的设计,虽然我没有做过大型的项目,但是在我看来是差不多的,最大的不同可能是代码分类方式的不同,该做的事情不会少,该写的代码也不会少,就是业务和代码的体量上比我们大,所以他们要处理体量大所带来的的问题。
#### 为什么不用今日头条的适配方案
* 关于屏幕适配方案,其实不能说头条的方案就是最好的,其实谷歌已经针对屏幕适配做了处理,就是 dp 和 sp ,而 dp 的计算转换是由屏幕的像素决定,系统只认 px 单位, dp 需要进行转换,比如 1dp 等于几个 px ,这个时候就需要基数进行转换,比如 1dp = 2px这个基数就是 2。
* ldpi1dp=0.75px
* mdpi1dp=1px
* hdpi1dp=1.5px
* xhdpi1dp=2px
* xxhdpi1dp=3px
* xxxhdpi1dp=4px
* 这个是谷歌对屏幕适配的一种默认方式,厂商也可以根据需要去修改默认的基数,从而达到最优的显示效果。
* 谷歌的屏幕适配方案也不是百分之一百完美的,其实会存在一些需求不能满足的问题。谷歌的设计理念是屏幕越大显示的东西越多,这种想法并没有错,但有些 APP 可能对这块会有要求,希望根据屏幕大小对控件进行百分比压缩。这个时候谷歌那套适配方案的设计已经和需求完全不一致了。
* 那什么样的 App 才会有那样的需求呢?现在手机的屏幕大多在 5 - 6寸而平板大多在 8 - 10 寸,也就是说我们只适配手机的话,只需要针对 5 - 6 寸的,并且它们的分辨率都差不多,其实用谷歌那种方案是最优的,如果我们需要适配平板的话,一般都会要求对控件进行百分比压缩,这个时候谷歌那套方案会把原先在手机显示的控件在平板上面变大一点,这样就会导致屏幕剩余的空间过大,导致控件显示出来的效果比较小,而如果采用百分比对控件压缩的方式,能比较好地控制 APP 在不同屏幕下显示的效果。
* 另外谈谈我的经历,我自己之前的公司主要是做平板上面的应用,所以也用过 [AutoSize 框架](https://github.com/JessYanCoding/AndroidAutoSize),一年多的使用体验下来,发现这个框架 Bug 还算是比较多的,例如框架会偶尔出现机型适配失效,重写了 **getResources** 方法情况之后出现的情况少了一些,但是仍然还有一些奇奇怪怪的问题,这里就不一一举例了,最后总结下来就是框架还不够成熟,但是框架的思想还是很不错的。我后面换了一家公司,也是做平板应用,项目用的是用[通配符的适配方案](https://github.com/wildma/ScreenAdaptation),跟 AutoSize 相对比,没有了那些奇奇怪怪的问题,但是对项目的侵入性高。这两种方案各有优缺点,大家看着选择。
* 最后我对屏幕适配方案进行了总结,如果我们不适配平板的情况下,使用谷歌原生那套方案的效果是最优的;如果需要适配平板,并且在要求手机和平板显示的效果一致的时候,可以换成百分比那种适配方案。
* 这个是我认为比较好的方式,手机和平板的应用我也都做过,尽管这样我说得也不一定是最好的,大家如果有更好的想法也欢迎和我交流。
#### 字体大小为什么不用 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 风格要跟随项目的设计走,所以你懂的
* 基于以上几点,我并不认为所有的东西都适合抽取成框架给大家用,有些东西还是跟随 **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/image.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 方法即可,具体用法不做过多介绍。
#### 为什么不用 Dagger 框架
* 框架的学习和使用成本极高,但总体收益不高,不适用于大部分人,所以不会考虑加入。
#### 这不就是一个模板工程换成我也能写一个
* 想把 AndroidProject 做出来并不难,我当时只花了一两个星期,而做好它需要无限的时间和精力,我花了两年多的时间仍然还在半路之上,尽管有很多人认为它很好用,没有任何 Bug但是在我看来还不够因为每个人衡量标准的程度不同我的标准是随着时间的推移和技术的提升而不断提高。具体付出了多少努力[可以先让我们看一组数据](https://github.com/getActivity/AndroidProject/graphs/contributors)
![](picture/help/contributors.jpg)
* 与其说 AndroidProject 做的是模板工程,但实际我在架构设计上花费的时间和精力会更多,其实这两者我都有在做,因为我的目的只有一个,能够帮助大家更好地开发和维护项目。具体 AndroidProject 在代码设计上有什么亮点,这里我建议你看一下里面的代码,我相信你看完后会有收获的,后面我可能也会出一篇文章具体讲述 AndroidProject 的亮点。
#### 轮子哥你怎么看待层出不穷的新技术
* 新东西的出现总能引起别人的好奇和尝试,但是我建议有兴趣的人可以学一下,但是如果要应用到项目中,我个人建议还是慎重,因为纵观历史,我们不难发现,技术创新虽然很受欢迎,但是大多数都经不住时间的考验,最终一个个气尽倒下,这是因为很多新技术,表面看起来很美好,但实际上一入坑深似海。当然也有一些优秀的技术创新活了下来,但是毕竟占的是少数。
* 谈谈我对新技术的看法,首先我会思考这种新技术能解决什么痛点,这点非常重要,再好的技术创新,也必须得创造价值,否则就是在扯淡。有人肯定会问,什么样的技术才算有价值?对于我们 Android 程序员来讲,无非就围绕两点,开发和维护。要么在前期开发上,能发挥很大的作用,要么在后续维护上面,能体现它的优势。
* 还有谷歌的新技术不一定都是好的,也有一些是 KPI 的产物,别忘了,他们也是程序员,他们也有 KPI 考核,为了年终奖和晋升,他们不得不卖力宣传,纵使他们知道这个东西有硬伤,但是他们也会推出来看看市场反应。所以我们看待一种新技术,不要太看重是否是大公司出品的,也不要太看重是哪个行业名人写的,我们应该要重点关注的是,产品的质量以及能带给我们带来哪些帮助,这个才是正确的技术价值观。

View File

@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@ -1,14 +1,16 @@
# 安卓架构
# 安卓技术中台
> 博客地址:[但愿人长久,搬砖不再有](https://www.jianshu.com/p/77dd326f21dc)
* 码云地址:[Gitee](https://gitee.com/getActivity/AndroidProject)
> 码云地址:[Gitee](https://gitee.com/getActivity/AndroidProject)
* Kotlin 版本:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin)
> 当我们日复一日年复一年的搬砖的时候,你是否曾想过提升一下开发效率,如果一个通用的基建项目摆在你的面前,你还会选择自己搭架构么
* 博客地址:[但愿人长久,搬砖不再有](https://www.jianshu.com/p/77dd326f21dc)
> 但是搭建出一个好的架构并非易事,有多少人愿意选择去做,还有多少人选择努力去做好,可能寥寥无几,但是你今天看到的,正是你所想要的,一个真正能解决你开发新项目时最大痛点的架构工程,你不需要再麻木 Copy 原有旧项目的代码,只需改动少量代码就能得到想要的效果,你会发现开发新项目其实是一件很快乐的事
* 当我们日复一日年复一年的搬砖的时候,你是否曾想过提升一下开发效率,如果一个通用的架构摆在你的面前,你还会选择自己搭架构么,但是搭建出一个好的架构并非易事,有多少人愿意选择去做,还有多少人选择努力去做好,可能寥寥无几,但是你今天看到的,正是你所想要的,一个真正能解决你开发新项目时最大痛点的架构工程,你不需要再麻木 Copy 原有旧项目的代码,只需改动少量代码就能得到想要的效果,你会发现开发新项目其实是一件很快乐的事
> 已经正式投入多个公司项目实践,暂时没有发现任何问题,[点击此处下载Demo](AndroidProject.apk)
* AndroidProject 已维护两年多的时间,几乎耗尽我所有的业余时间,里面的代码改了再改,改了又改,不断 Review、不断创新、不断改进、不断测试、不断优化每天都在重复这些枯燥的步骤但是只有这样才能把这件事做好因为我相信把同样一件事重复做迟早有一天可以做好。
* 已经正式投入到多个公司项目实践中,暂时没有发现任何问题或者 Bug[点击下载 Apk 体验](AndroidProject.apk),又或者扫码下载
![](picture/demo_code.png)
@ -76,7 +78,7 @@
* App 优化:已经进行了全面的内存优化、布局优化、代码优化、瘦身优化,并且对结果进行了严格的长久测试。
* 代码注释:参照 Android SDK 、Support 源码和参考阿里巴巴的代码规范文档对代码进行命名,对难点代码进行了注释,对重点代码进行了说明。
* 代码规范:参照 Android SDK 、Support 源码和参考阿里巴巴的代码规范文档对代码进行命名,对难点代码进行了注释,对重点代码进行了说明。
* 代码统一:对项目中常见的代码进行了封装,或是封装到基类中、或是封装到工具类中、或者封装到框架中,不追求过度封装,根据实际场景和代码维护性考虑,尽量保证同一个功能的代码在项目中不重复。
@ -84,36 +86,40 @@
* 无任何瑕疵:对小屏手机、全面屏手机、带虚拟按键手机进行了适配和优化,确保每一个界面细节都能处理到位、每一个功能细节都能符合大众的需求、乃至每一行代码都能贴合 Android 程序员的审美观。
* 兼容性优良:在此感谢开源道路上给予我支持和帮助的小伙伴,一个人一台机在兼容性面前无能为力,而在几百人几百台机面前却不是问题。如果没有这些的测试,有些问题我一个人可能这辈子都发现不了,纵使代码写得再好,逻辑再严谨,没有经过大众的验证,无异于纸上谈兵。
* 兼容性优良:在此感谢开源道路上给予我支持和帮助的小伙伴,一个人一台机在兼容性面前无能为力,而在几百人几百台机面前却不是问题。如果没有这些的测试,有些问题我一个人可能这辈子都发现不了,纵使代码写得再好,逻辑再严谨,没有经过大众的验证,无异于纸上谈兵。
#### [代码规范文档请点击这里查看](https://github.com/getActivity/AndroidCodeStandard)
#### 为什么没有用 MVP
![](picture/mvp.jpg)
* AndroidProject 舍弃 MVP 的最大一个原因,需要写各种类,各种回调,如果这个页面比较简单的话,使用 MVP 会让原本简单的代码变复杂,导致后续开发和维护成本是非常高,前期付出的代价和后期的维护不成正比关系,当然这种说法只适用于各种中小型项目,大型的项目我还没有经历过,不过我觉得,无论是 MVC、MVP、MVVM它们出现的目的是为了解决代码多并且乱的问题作用就是给代码做分类但是可以跟大家分享我的心得我并不看好 MVP因为它让我开发和维护都很痛苦所以我就直接将它从 AndroidProject 移除,目的也很简单,不推荐大家使用,因为 MVP 不适合大多数项目的开发和维护。我更推荐大家直接将代码写在 Activity但是有一个前提条件需要大家遵守大家要做好代码封装和重复代码的抽取尽量让 Activity 成为只有业务代码的类,这样一个项目里面的大多数 Activity 代码量都能很好控制在 1000 行代码以内。但是这种看似简单的操作,但是实际要做到是一件不容易的事情,这里面不仅要解决代码带来的问题,还要解决带来的各种人性矛盾,困难重重,这种想法经过很长一段时间的思考,虽然写法在开发和维护中效率是非常高的,但是不被大多数人认可,大家更愿意相信 MVC、MVP、MVVM而很少有人理解这三种模式的本质是什么就是为了给代码做分类但这三种模式都不够灵活很生硬像是一套套规则而这样的代码分类只会让大多数人的开发越来越头疼。
#### [常见问题解答请点击这里查看](HelpDoc.md)
#### 作者的其他开源项目
* 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) (已集成)
* 日志框架:[Logcat](https://github.com/getActivity/Logcat) (已集成)
* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) (已集成)
* 吐司框架:[ToastUtils](https://github.com/getActivity/ToastUtils) (已集成)
* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) (已集成)
* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) (已集成)
* 悬浮窗框架:[XToast](https://github.com/getActivity/XToast) (未集成)
* 国际化框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) (未集成)
#### Android技术讨论Q群78797078
* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) (未集成)
#### 微信公众号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

View File

@ -1,13 +1,14 @@
apply plugin: 'com.android.application'
apply plugin: 'android-aspectjx'
apply from: '../config.gradle'
apply from: 'proguard.gradle'
apply plugin : 'com.android.application'
apply plugin : 'android-aspectjx'
apply from : '../config.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
// https://www.jianshu.com/p/17327e191d2e
applicationId 'com.hjq.demo'
//
@ -16,19 +17,11 @@ android {
// xxhdpi 1920 * 1080
resConfig 'xxhdpi'
// so Bugly
ndk {
// armeabi0%
// armeabi-v7a10%
// arm64-v8a90%
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
//
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-app.pro'
proguardFiles 'proguard-sdk.pro', 'proguard-app.pro'
}
// APK https://www.jianshu.com/p/a1f8e5896aa2
// Apk https://www.jianshu.com/p/a1f8e5896aa2
signingConfigs {
config {
storeFile file(StoreFile)
@ -41,44 +34,84 @@ android {
buildTypes {
debug {
//
applicationIdSuffix '.debug'
//
debuggable true
jniDebuggable true
//
shrinkResources false
// ZipAlign
//
zipAlignEnabled false
//
//
shrinkResources false
//
minifyEnabled false
// BuglyId
buildConfigField 'String', 'BUGLY_ID', '\"请自行替换 Bugly 上面的 AppID\"'
//
signingConfig signingConfigs.config
//
buildConfigField('boolean', 'LOG_ENABLE', 'true')
// BuglyId
buildConfigField('String', 'BUGLY_ID', '"请自行替换 Bugly 上面的 AppID"')
//
buildConfigField('String', 'HOST_URL', '"https://www.test.baidu.com/"')
//
manifestPlaceholders = ['app_name' : '安卓技术中台 Debug 版']
// so
ndk {
abiFilters 'armeabi-v7a'
}
}
preview.initWith(debug)
preview {
applicationIdSuffix ''
//
manifestPlaceholders = ['app_name' : '安卓技术中台 Preview 版']
}
release {
//
debuggable false
jniDebuggable false
//
shrinkResources true
// ZipAlign
//
zipAlignEnabled true
//
//
shrinkResources true
//
minifyEnabled true
// BuglyId
buildConfigField 'String', 'BUGLY_ID', '\"请自行替换 Bugly 上面的 AppID\"'
//
signingConfig signingConfigs.config
//
buildConfigField('boolean', 'LOG_ENABLE', 'false')
// BuglyId
buildConfigField('String', 'BUGLY_ID', '"请自行替换 Bugly 上面的 AppID"')
//
buildConfigField('String', 'HOST_URL', '"https://www.baidu.com/"')
//
manifestPlaceholders = ['app_name' : '@string/app_name']
// so Bugly
ndk {
// armeabi0%
// armeabi-v7a10%
// arm64-v8a95%
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}
packagingOptions {
//
//
exclude 'META-INF/*******'
}
// AOP
// AOP exclude include
aspectjx {
// Gson LeakCanary AOP
// ClassNotFoundException: Didn't find class on path: DexPathList
exclude 'androidx', 'com.google', 'com.squareup', 'com.alipay', 'com.taobao', 'org.apache'
// exclude 'androidx', 'com.google', 'com.squareup', 'org.apache', 'com.alipay', 'com.taobao', 'versions.9'
// AOP
include 'com.hjq.demo'
//
// java.util.zip.ZipExceptionCause: zip file is empty
// ClassNotFoundException: Didn't find class on path: DexPathList
}
applicationVariants.all { variant ->
@ -96,80 +129,87 @@ android {
// api implementation https://www.jianshu.com/p/8962d6ba936e
dependencies {
//
//
implementation project(':base')
// View
//
implementation project(':widget')
//
implementation project(':umeng')
// https://github.com/getActivity/EasyHttp
implementation 'com.hjq:http:6.9'
// https://github.com/getActivity/XXPermissions
implementation 'com.hjq:xxpermissions:6.5'
implementation 'com.hjq:xxpermissions:9.8'
// https://github.com/getActivity/TitleBar
implementation 'com.hjq:titlebar:6.5'
implementation 'com.hjq:titlebar:8.2'
// https://github.com/getActivity/ToastUtils
implementation 'com.hjq:toast:8.6'
implementation 'com.hjq:toast:8.8'
// https://github.com/getActivity/EasyHttp
implementation 'com.hjq:http:9.0'
// OkHttp https://github.com/square/okhttp
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
// noinspection GradleDependency
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
// JSON https://github.com/google/gson
// Json https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.8.6'
// Gson https://github.com/getActivity/GsonFactory
implementation 'com.hjq.gson:factory:3.0'
// AOP https://mvnrepository.com/artifact/org.aspectj/aspectjrt
implementation 'org.aspectj:aspectjrt:1.9.5'
implementation 'org.aspectj:aspectjrt:1.9.6'
// https://github.com/bumptech/glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
// 使https://muyangmin.github.io/glide-docs-cn/
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
// https://github.com/gyf-dev/ImmersionBar
// https://github.com/gyf-dev/ImmersionBar
implementation 'com.gyf.immersionbar:immersionbar:3.0.0'
// ImageViewhttps://github.com/chrisbanes/PhotoView
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
// ViewPager https://github.com/romandanylyk/PageIndicatorView
implementation 'com.romandanylyk:pageindicatorview:1.0.3'
// Bugly https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20190418140644
implementation 'com.tencent.bugly:crashreport:3.1.9'
implementation 'com.tencent.bugly:nativecrashreport:3.7.1'
implementation 'com.tencent.bugly:crashreport:3.3.3'
implementation 'com.tencent.bugly:nativecrashreport:3.7.700'
// https://github.com/airbnb/lottie-android
// https://lottiefiles.comhttps://icons8.com/animated-icons
implementation 'com.airbnb.android:lottie:3.4.1'
implementation 'com.airbnb.android:lottie:3.6.1'
// https://github.com/scwang90/SmartRefreshLayout
implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2'
// SmartRefreshLayout
implementation 'com.scwang.smartrefresh:SmartRefreshHeader:1.1.2'
// 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/luckybilly/SmartSwipe
implementation 'com.billy.android:smart-swipe:1.1.2'
implementation 'com.billy.android:smart-swipe-x:1.1.0'
// https://github.com/getActivity/Logcat
debugImplementation 'com.hjq:logcat:8.2'
// https://github.com/JakeWharton/timber
implementation 'com.jakewharton.timber:timber:4.7.1'
// https://github.com/square/leakcanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
// https://github.com/ongakuer/CircleIndicator
implementation 'me.relex:circleindicator:2.1.4'
// https://github.com/square/leakcanary
// noinspection GradleDependency
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
// noinspection GradleDependency
previewImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
// https://github.com/getActivity/MultiLanguages
// https://github.com/getActivity/XToast
// https://github.com/getActivity/Logcat
// https://github.com/Blankj/AndroidUtilCode
// https://github.com/JessYanCoding/AndroidAutoSize
// https://github.com/bingoogolapple/BGABanner-Android
// https://github.com/bingoogolapple/BGAQRCode-Android
// https://github.com/sunfusheng/MarqueeView
// https://github.com/Cuieney/RxPay
// Log https://github.com/JakeWharton/timber
// https://github.com/Yalantis/uCrop
// https://www.jianshu.com/p/f1f888e4a35f
// https://github.com/JessYanCoding/AndroidAutoSize
// https://github.com/Curzibn/Luban
// https://github.com/leavesC/DoKV
// https://www.jianshu.com/p/f1f888e4a35f
// https://github.com/Cuieney/RxPay
// https://github.com/Meituan-Dianping/walle
// http://msa-alliance.cn/col.jsp?id=120
}

View File

@ -4,60 +4,18 @@
# 混淆保护自己项目的部分代码以及引用的第三方jar包
#-libraryjars libs/umeng-analytics-v5.2.4.jar
# 不混淆 WebView JS 接口
-keepattributes *JavascriptInterface*
# 不混淆 WebView 的类的所有的内部类
-keepclassmembers class com.hjq.demo.ui.activity.BrowserActivity$*{
*;
}
# 不混淆 WebChromeClient 中的 openFileChooser 方法
-keepclassmembers class * extends android.webkit.WebChromeClient{
public void openFileChooser(...);
}
# EventBus3
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
# Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# 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 <methods>;
}
# OkHttp3
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn org.conscrypt.**
# 不混淆这个包下的字段名
-keepclassmembernames class com.hjq.demo.http.** {
# 不混淆这些包下的字段名
-keepclassmembernames class com.hjq.demo.http.request.** {
<fields>;
}
-keepclassmembernames class com.hjq.demo.http.response.** {
<fields>;
}
-keepclassmembernames class com.hjq.demo.http.model.** {
<fields>;
}
# 不混淆被 DebugLog 注解的方法信息
-keepclassmembernames class ** {
@com.hjq.demo.aop.DebugLog <methods>;
}

36
app/proguard-sdk.pro Normal file
View File

@ -0,0 +1,36 @@
# Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule {
<init>(...);
}
-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 <methods>;
}
# OkHttp3
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn org.conscrypt.**

View File

@ -1,49 +0,0 @@
apply plugin: 'AndResGuard'
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
//
keepRoot = false
//
whiteList = [
// for your icon
"R.drawable.icon",
// for fabric
"R.string.com.crashlytics.*",
// for google-services
"R.string.google_app_id",
"R.string.gcm_defaultSenderId",
"R.string.default_web_client_id",
"R.string.ga_trackingId",
"R.string.firebase_database_url",
"R.string.google_api_key",
"R.string.google_crash_reporting_api_key"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.17'
//path = "/usr/local/bin/7za"
}
//
//mergeDuplicatedRes = true
/**
* assemble输出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* : v1签名时生成jar文件的摘要算法
* SHA-1
**/
// digestalg = "SHA-256"
}

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 清单文件合并指引https://developer.android.google.cn/studio/build/manifest-merge?hl=zh-cn -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.hjq.demo">
@ -6,32 +7,40 @@
<!-- 联网权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 访问网络状态 -->
<!-- 网络状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 外部存储读写权限 -->
<!-- 外部存储 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<!-- 拍照权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 安装包的权限 -->
<!-- 安装权限 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- 定位权限(用于 WebView 定位)-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Application 节点详解https://developer.android.google.cn/guide/topics/manifest/application-element -->
<!-- Activity 节点详解https://developer.android.google.cn/guide/topics/manifest/activity-element -->
<application
android:name=".common.MyApplication"
android:name=".app.AppApplication"
android:allowBackup="false"
android:icon="@mipmap/launcher_ic"
android:label="@string/app_name"
android:label="${app_name}"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:resizeableActivity="true"
android:roundIcon="@mipmap/launcher_ic"
android:supportsRtl="false"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute,LockedOrientationActivity"
tools:ignore="AllowBackup,LockedOrientationActivity"
tools:replace="android:allowBackup,android:supportsRtl"
tools:targetApi="n">
@ -47,13 +56,12 @@
android:resource="@xml/file_paths" />
</provider>
<!-- 闪屏界面 -->
<!-- 闪屏 -->
<activity
android:name=".ui.activity.SplashActivity"
android:alwaysRetainTaskState="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait">
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<!-- 程序入口 -->
<intent-filter>
@ -65,46 +73,47 @@
</activity>
<!-- 崩溃界面 -->
<!-- 引导页 -->
<activity
android:name=".ui.activity.GuideActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 首页 -->
<activity
android:name=".ui.activity.HomeActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan" />
<!-- 登录页 -->
<activity
android:name=".ui.activity.LoginActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 注册页 -->
<activity
android:name=".ui.activity.RegisterActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 崩溃展示(必须在独立进程) -->
<activity
android:name=".ui.activity.CrashActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:process=":crash"
android:screenOrientation="landscape" />
<!-- 引导界面 -->
<!-- 重启应用(必须在独立进程) -->
<activity
android:name=".ui.activity.GuideActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:name=".ui.activity.RestartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
android:process=":restart" />
<!-- App 首页 -->
<activity
android:name=".ui.activity.HomeActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<!-- 登录界面 -->
<activity
android:name=".ui.activity.LoginActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 注册界面 -->
<activity
android:name=".ui.activity.RegisterActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 设置界面 -->
<!-- 设置页 -->
<activity
android:name=".ui.activity.SettingActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/setting_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
@ -112,7 +121,6 @@
<!-- 忘记密码 -->
<activity
android:name=".ui.activity.PasswordForgetActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/password_forget_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
@ -120,15 +128,13 @@
<!-- 重置密码 -->
<activity
android:name=".ui.activity.PasswordResetActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/password_reset_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 手机号更换 -->
<!-- 更换手机 -->
<activity
android:name=".ui.activity.PhoneResetActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/phone_reset_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
@ -136,7 +142,6 @@
<!-- 关于我们 -->
<activity
android:name=".ui.activity.AboutActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/about_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
@ -144,15 +149,13 @@
<!-- 个人资料 -->
<activity
android:name=".ui.activity.PersonalDataActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/personal_data_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 浏览 -->
<!-- 网页浏览 -->
<activity
android:name=".ui.activity.BrowserActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/web_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
@ -160,14 +163,12 @@
<!-- 拍照选择 -->
<activity
android:name=".ui.activity.CameraActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 图片选择 -->
<activity
android:name=".ui.activity.ImageSelectActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/image_select_title"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
@ -175,14 +176,12 @@
<!-- 查看大图 -->
<activity
android:name=".ui.activity.ImagePreviewActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 播放视频 -->
<activity
android:name=".ui.activity.VideoPlayActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="landscape"
android:theme="@style/FullScreenTheme" />
@ -190,21 +189,18 @@
<!-- 选择视频 -->
<activity
android:name=".ui.activity.VideoSelectActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 对话框案例 -->
<activity
android:name=".ui.activity.DialogActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<!-- 状态案例 -->
<activity
android:name=".ui.activity.StatusActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTop"
android:screenOrientation="portrait" />

View File

@ -12,20 +12,20 @@ import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import com.hjq.demo.R;
import com.hjq.demo.widget.HintLayout;
import com.hjq.demo.widget.StatusLayout;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/08
* desc : 界面状态提示
* desc : 状态布局意图
*/
public interface StatusAction {
/**
* 获取提示布局
* 获取状态布局
*/
HintLayout getHintLayout();
StatusLayout getStatusLayout();
/**
* 显示加载中
@ -35,9 +35,9 @@ public interface StatusAction {
}
default void showLoading(@RawRes int id) {
HintLayout layout = getHintLayout();
StatusLayout layout = getStatusLayout();
layout.show();
layout.setAnim(id);
layout.setAnimResource(id);
layout.setHint("");
layout.setOnClickListener(null);
}
@ -46,7 +46,7 @@ public interface StatusAction {
* 显示加载完成
*/
default void showComplete() {
HintLayout layout = getHintLayout();
StatusLayout layout = getStatusLayout();
if (layout != null && layout.isShow()) {
layout.hide();
}
@ -56,38 +56,38 @@ public interface StatusAction {
* 显示空提示
*/
default void showEmpty() {
showLayout(R.drawable.hint_empty_ic, R.string.hint_layout_no_data, null);
showLayout(R.drawable.status_empty_ic, R.string.status_layout_no_data, null);
}
/**
* 显示错误提示
*/
default void showError(View.OnClickListener listener) {
HintLayout layout = getHintLayout();
StatusLayout layout = getStatusLayout();
Context context = layout.getContext();
ConnectivityManager manager = ContextCompat.getSystemService(context, ConnectivityManager.class);
if (manager != null) {
NetworkInfo info = manager.getActiveNetworkInfo();
// 判断网络是否连接
if (info == null || !info.isConnected()) {
showLayout(R.drawable.hint_nerwork_ic, R.string.hint_layout_error_network, listener);
showLayout(R.drawable.status_nerwork_ic, R.string.status_layout_error_network, listener);
return;
}
}
showLayout(R.drawable.hint_error_ic, R.string.hint_layout_error_request, listener);
showLayout(R.drawable.status_error_ic, R.string.status_layout_error_request, listener);
}
/**
* 显示自定义提示
*/
default void showLayout(@DrawableRes int drawableId, @StringRes int stringId, View.OnClickListener listener) {
HintLayout layout = getHintLayout();
StatusLayout layout = getStatusLayout();
Context context = layout.getContext();
showLayout(ContextCompat.getDrawable(context, drawableId), context.getString(stringId), listener);
}
default void showLayout(Drawable drawable, CharSequence hint, View.OnClickListener listener) {
HintLayout layout = getHintLayout();
StatusLayout layout = getStatusLayout();
layout.show();
layout.setIcon(drawable);
layout.setHint(hint);

View File

@ -4,12 +4,12 @@ package com.hjq.demo.action;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/08
* desc : 侧滑意图
* desc : 界面侧滑意图
*/
public interface SwipeAction {
/**
* 是否使用侧滑
* 是否使用界面侧滑
*/
default boolean isSwipeEnable() {
// 默认开启

View File

@ -24,26 +24,26 @@ public interface TitleBarAction extends OnTitleBarListener {
/**
* 左项被点击
*
* @param v 被点击的左项View
* @param view 被点击的左项View
*/
@Override
default void onLeftClick(View v) {}
default void onLeftClick(View view) {}
/**
* 标题被点击
*
* @param v 被点击的标题View
* @param view 被点击的标题View
*/
@Override
default void onTitleClick(View v){}
default void onTitleClick(View view) {}
/**
* 右项被点击
*
* @param v 被点击的右项View
* @param view 被点击的右项View
*/
@Override
default void onRightClick(View v) {}
default void onRightClick(View view) {}
/**
* 设置标题栏的标题

View File

@ -9,7 +9,7 @@ import java.lang.annotation.Target;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/01/11
* desc : 检测网络注解
* desc : 网络检测注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)

View File

@ -7,7 +7,7 @@ import android.net.NetworkInfo;
import androidx.core.content.ContextCompat;
import com.hjq.demo.R;
import com.hjq.demo.helper.ActivityStackManager;
import com.hjq.demo.manager.ActivityManager;
import com.hjq.toast.ToastUtils;
import org.aspectj.lang.ProceedingJoinPoint;
@ -19,7 +19,7 @@ import org.aspectj.lang.annotation.Pointcut;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/01/11
* desc : 网络检测
* desc : 网络检测切面
*/
@Aspect
public class CheckNetAspect {
@ -35,14 +35,14 @@ public class CheckNetAspect {
*/
@Around("method() && @annotation(checkNet)")
public void aroundJoinPoint(ProceedingJoinPoint joinPoint, CheckNet checkNet) throws Throwable {
Application application = ActivityStackManager.getInstance().getApplication();
Application application = ActivityManager.getInstance().getApplication();
if (application != null) {
ConnectivityManager manager = ContextCompat.getSystemService(application, ConnectivityManager.class);
if (manager != null) {
NetworkInfo info = manager.getActiveNetworkInfo();
// 判断网络是否连接
if (info == null || !info.isConnected()) {
ToastUtils.show(R.string.common_network);
ToastUtils.show(R.string.common_network_hint);
return;
}
}

View File

@ -2,12 +2,9 @@ package com.hjq.demo.aop;
import android.os.Looper;
import android.os.Trace;
import android.util.Log;
import androidx.annotation.NonNull;
import com.hjq.demo.other.AppConfig;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
@ -18,11 +15,13 @@ import org.aspectj.lang.reflect.MethodSignature;
import java.util.concurrent.TimeUnit;
import timber.log.Timber;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/06
* desc : Debug 日志打印
* desc : Debug 日志切面
*/
@Aspect
public class DebugLogAspect {
@ -68,7 +67,7 @@ public class DebugLogAspect {
String methodName = codeSignature.getName();
// 方法参数名集合
String[] parameterNames = codeSignature.getParameterNames();
// 方法参数集合
// 方法参数集合
Object[] parameterValues = joinPoint.getArgs();
//记录并打印方法的信息
@ -118,10 +117,6 @@ public class DebugLogAspect {
* @param lengthMillis 执行方法所需要的时间
*/
private void exitMethod(ProceedingJoinPoint joinPoint, DebugLog debugLog, Object result, long lengthMillis) {
if (!AppConfig.isDebug()) {
return;
}
Trace.endSection();
Signature signature = joinPoint.getSignature();
@ -147,6 +142,7 @@ public class DebugLogAspect {
}
private void log(String tag, String msg) {
Log.d(tag, msg);
Timber.tag(tag);
Timber.d(msg);
}
}

View File

@ -2,11 +2,9 @@ package com.hjq.demo.aop;
import android.app.Activity;
import com.hjq.demo.R;
import com.hjq.demo.helper.ActivityStackManager;
import com.hjq.permissions.OnPermission;
import com.hjq.demo.manager.ActivityManager;
import com.hjq.demo.other.PermissionCallback;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.ToastUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@ -19,7 +17,7 @@ import java.util.List;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/06
* desc : 权限申请处理
* desc : 权限申请切面
*/
@Aspect
public class PermissionsAspect {
@ -35,16 +33,17 @@ public class PermissionsAspect {
*/
@Around("method() && @annotation(permissions)")
public void aroundJoinPoint(final ProceedingJoinPoint joinPoint, Permissions permissions) {
Activity activity = ActivityStackManager.getInstance().getTopActivity();
Activity activity = ActivityManager.getInstance().getTopActivity();
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
return;
}
XXPermissions.with(activity)
.permission(permissions.value())
.request(new OnPermission() {
.request(new PermissionCallback() {
@Override
public void hasPermission(List<String> granted, boolean all) {
public void onGranted(List<String> permissions, boolean all) {
if (all) {
try {
// 获得权限执行原方法
@ -54,16 +53,6 @@ public class PermissionsAspect {
}
}
}
@Override
public void noPermission(List<String> denied, boolean quick) {
if (quick) {
ToastUtils.show(R.string.common_permission_fail);
XXPermissions.startPermissionActivity(activity, false);
} else {
ToastUtils.show(R.string.common_permission_hint);
}
}
});
}
}

View File

@ -1,32 +1,27 @@
package com.hjq.demo.aop;
import android.util.Log;
import android.view.View;
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 java.util.Calendar;
import timber.log.Timber;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/06
* desc : 防重复点击处理
* desc : 防重复点击切面
*/
@Aspect
public class SingleClickAspect {
/**
* 最近一次点击的时间
*/
/** 最近一次点击的时间 */
private long mLastTime;
/**
* 最近一次点击的控件ID
*/
private int mLastId;
/** 最近一次点击的标记 */
private String mLastTag;
/**
* 方法切入点
@ -39,23 +34,36 @@ public class SingleClickAspect {
*/
@Around("method() && @annotation(singleClick)")
public void aroundJoinPoint(ProceedingJoinPoint joinPoint, SingleClick singleClick) throws Throwable {
View view = null;
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof View) {
view = (View) arg;
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
// 方法所在类
String className = codeSignature.getDeclaringType().getName();
// 方法名
String methodName = codeSignature.getName();
// 构建方法 TAG
StringBuilder builder = new StringBuilder(className + "." + methodName);
builder.append("(");
Object[] parameterValues = joinPoint.getArgs();
for (int i = 0; i < parameterValues.length; i++) {
Object arg = parameterValues[i];
if (i == 0) {
builder.append(arg);
} else {
builder.append(", ")
.append(arg);
}
}
if (view != null) {
long currentTime = Calendar.getInstance().getTimeInMillis();
if (currentTime - mLastTime < singleClick.value() && view.getId()
== mLastId) {
Log.i("SingleClick", "发生快速点击");
return;
}
mLastTime = currentTime;
mLastId = view.getId();
//执行原方法
joinPoint.proceed();
builder.append(")");
String tag = builder.toString();
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - mLastTime < singleClick.value() && tag.equals(mLastTag)) {
Timber.tag("SingleClick");
Timber.i("%s 毫秒内发生快速点击:%s", singleClick.value(), tag);
return;
}
mLastTime = currentTimeMillis;
mLastTag = tag;
// 执行原方法
joinPoint.proceed();
}
}

View File

@ -1,4 +1,4 @@
package com.hjq.demo.common;
package com.hjq.demo.app;
import android.content.Intent;
import android.os.Bundle;
@ -26,11 +26,11 @@ import okhttp3.Call;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 项目中的 Activity 基类
* desc : 业务 Activity 基类
*/
public abstract class MyActivity extends BaseActivity
public abstract class AppActivity extends BaseActivity
implements ToastAction, TitleBarAction,
SwipeAction, OnHttpListener {
SwipeAction, OnHttpListener<Object> {
/** 标题栏对象 */
private TitleBar mTitleBar;
@ -55,15 +55,17 @@ public abstract class MyActivity extends BaseActivity
public void showDialog() {
mDialogTotal++;
postDelayed(() -> {
if (mDialogTotal > 0 && !isFinishing()) {
if (mDialog == null) {
mDialog = new WaitDialog.Builder(this)
.setCancelable(false)
.create();
}
if (!mDialog.isShowing()) {
mDialog.show();
}
if (mDialogTotal <= 0 || isFinishing() || isDestroyed()) {
return;
}
if (mDialog == null) {
mDialog = new WaitDialog.Builder(this)
.setCancelable(false)
.create();
}
if (!mDialog.isShowing()) {
mDialog.show();
}
}, 300);
}
@ -114,16 +116,6 @@ public abstract class MyActivity extends BaseActivity
return true;
}
/**
* 初始化沉浸式状态栏
*/
@NonNull
protected ImmersionBar createStatusBarConfig() {
return ImmersionBar.with(this)
// 默认状态栏字体颜色为黑色
.statusBarDarkFont(isStatusBarDarkFont());
}
/**
* 获取状态栏沉浸的配置对象
*/
@ -135,6 +127,20 @@ public abstract class MyActivity extends BaseActivity
return mImmersionBar;
}
/**
* 初始化沉浸式状态栏
*/
@NonNull
protected ImmersionBar createStatusBarConfig() {
return ImmersionBar.with(this)
// 默认状态栏字体颜色为黑色
.statusBarDarkFont(isStatusBarDarkFont())
// 指定导航栏背景颜色
.navigationBarColor(android.R.color.white)
// 状态栏字体和导航栏内容自动变色必须指定状态栏颜色和导航栏颜色才可以自动变色
.autoDarkModeEnable(true, 0.2f);
}
/**
* 设置标题栏的标题
*/
@ -164,7 +170,7 @@ public abstract class MyActivity extends BaseActivity
}
@Override
public void onLeftClick(View v) {
public void onLeftClick(View view) {
onBackPressed();
}
@ -192,7 +198,7 @@ public abstract class MyActivity extends BaseActivity
@Override
public void onSucceed(Object result) {
if (result instanceof HttpData) {
toast(((HttpData) result).getMessage());
toast(((HttpData<?>) result).getMessage());
}
}
@ -208,10 +214,10 @@ public abstract class MyActivity extends BaseActivity
@Override
protected void onDestroy() {
super.onDestroy();
if (isShowDialog()) {
hideDialog();
}
mDialog = null;
super.onDestroy();
}
}

View File

@ -1,4 +1,4 @@
package com.hjq.demo.common;
package com.hjq.demo.app;
import android.content.Context;
import android.view.View;
@ -18,9 +18,9 @@ import java.util.List;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/12/19
* desc : 项目中 RecyclerView 适配器基类
* desc : 业务 RecyclerView 适配器基类
*/
public abstract class MyAdapter<T> extends BaseAdapter<BaseAdapter.ViewHolder> {
public abstract class AppAdapter<T> extends BaseAdapter<BaseAdapter<?>.ViewHolder> {
/** 列表数据 */
private List<T> mDataSet;
@ -31,13 +31,23 @@ public abstract class MyAdapter<T> extends BaseAdapter<BaseAdapter.ViewHolder> {
/** 标记对象 */
private Object mTag;
public MyAdapter(@NonNull Context context) {
public AppAdapter(@NonNull Context context) {
super(context);
}
@Override
public int getItemCount() {
return mDataSet == null ? 0 : mDataSet.size();
return getCount();
}
/**
* 获取数据总数
*/
public int getCount() {
if (mDataSet == null) {
return 0;
}
return mDataSet.size();
}
/**
@ -66,10 +76,11 @@ public abstract class MyAdapter<T> extends BaseAdapter<BaseAdapter.ViewHolder> {
if (mDataSet == null || mDataSet.size() == 0) {
setData(data);
} else {
mDataSet.addAll(data);
notifyItemRangeInserted(mDataSet.size() - data.size(), data.size());
return;
}
mDataSet.addAll(data);
notifyItemRangeInserted(mDataSet.size() - data.size(), data.size());
}
/**
@ -138,9 +149,7 @@ public abstract class MyAdapter<T> extends BaseAdapter<BaseAdapter.ViewHolder> {
}
public void removeItem(@IntRange(from = 0) int position) {
// 如果是在for循环删除后要记得i--
mDataSet.remove(position);
// 告诉适配器删除数据的位置会有动画效果
notifyItemRemoved(position);
}
@ -154,8 +163,8 @@ public abstract class MyAdapter<T> extends BaseAdapter<BaseAdapter.ViewHolder> {
/**
* 设置当前的页码
*/
public void setPageNumber(@IntRange(from = 0)int pageNumber) {
mPageNumber = pageNumber;
public void setPageNumber(@IntRange(from = 0) int number) {
mPageNumber = number;
}
/**
@ -168,8 +177,8 @@ public abstract class MyAdapter<T> extends BaseAdapter<BaseAdapter.ViewHolder> {
/**
* 设置是否为最后一页
*/
public void setLastPage(boolean flag) {
mLastPage = flag;
public void setLastPage(boolean last) {
mLastPage = last;
}
/**

View File

@ -0,0 +1,194 @@
package com.hjq.demo.app;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.Network;
import android.os.Build;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import com.billy.android.swipe.SmartSwipeBack;
import com.hjq.bar.TitleBar;
import com.hjq.bar.initializer.LightBarInitializer;
import com.hjq.demo.R;
import com.hjq.demo.action.SwipeAction;
import com.hjq.demo.aop.DebugLog;
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.AppConfig;
import com.hjq.demo.other.CrashHandler;
import com.hjq.demo.other.DebugLoggerTree;
import com.hjq.demo.other.SmartBallPulseFooter;
import com.hjq.demo.other.ToastInterceptor;
import com.hjq.http.EasyConfig;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.ToastUtils;
import com.hjq.toast.style.ToastBlackStyle;
import com.hjq.umeng.UmengClient;
import com.scwang.smart.refresh.header.MaterialHeader;
import com.scwang.smart.refresh.layout.SmartRefreshLayout;
import com.tencent.bugly.crashreport.CrashReport;
import okhttp3.OkHttpClient;
import timber.log.Timber;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 应用入口
*/
public final class AppApplication extends Application {
@DebugLog("启动耗时")
@Override
public void onCreate() {
super.onCreate();
initSdk(this);
}
@Override
public void onLowMemory() {
super.onLowMemory();
// 清理所有图片内存缓存
GlideApp.get(this).onLowMemory();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
// 根据手机内存剩余情况清理图片内存缓存
GlideApp.get(this).onTrimMemory(level);
}
/**
* 初始化一些第三方框架
*/
public static void initSdk(Application application) {
// 设置权限请求调试模式
XXPermissions.setDebugMode(AppConfig.isDebug());
// 初始化吐司
ToastUtils.init(application, new ToastBlackStyle(application) {
@Override
public int getCornerRadius() {
return (int) application.getResources().getDimension(R.dimen.button_round_size);
}
});
// 设置 Toast 拦截器
ToastUtils.setToastInterceptor(new ToastInterceptor());
// 设置标题栏初始化器
TitleBar.setDefaultInitializer(new LightBarInitializer() {
@Override
public Drawable getBackgroundDrawable(Context context) {
return new ColorDrawable(ContextCompat.getColor(application, R.color.common_primary_color));
}
@Override
public Drawable getBackIcon(Context context) {
return ContextCompat.getDrawable(context, R.drawable.arrows_left_ic);
}
@Override
protected TextView createTextView(Context context) {
return new AppCompatTextView(context);
}
});
// 本地异常捕捉
CrashHandler.register(application);
// 友盟统计登录分享 SDK
UmengClient.init(application);
// Bugly 异常捕捉
CrashReport.initCrashReport(application, AppConfig.getBuglyId(), AppConfig.isDebug());
// 设置全局的 Header 构建器
SmartRefreshLayout.setDefaultRefreshHeaderCreator((context, layout) ->
new MaterialHeader(context).setColorSchemeColors(ContextCompat.getColor(context, R.color.common_accent_color)));
// 设置全局的 Footer 构建器
SmartRefreshLayout.setDefaultRefreshFooterCreator((context, layout) -> new SmartBallPulseFooter(context));
// 设置全局初始化器
SmartRefreshLayout.setDefaultRefreshInitializer((context, layout) -> {
// 刷新头部是否跟随内容偏移
layout.setEnableHeaderTranslationContent(true)
// 刷新尾部是否跟随内容偏移
.setEnableFooterTranslationContent(true)
// 加载更多是否跟随内容偏移
.setEnableFooterFollowWhenNoMoreData(true)
// 内容不满一页时是否可以上拉加载更多
.setEnableLoadMoreWhenContentNotFull(false)
// 仿苹果越界效果开关
.setEnableOverScrollDrag(false);
});
// Activity 栈管理初始化
ActivityManager.getInstance().init(application);
// 网络请求框架初始化
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();
EasyConfig.with(okHttpClient)
// 是否打印日志
.setLogEnabled(AppConfig.isLogEnable())
// 设置服务器配置
.setServer(new RequestServer())
// 设置请求处理策略
.setHandler(new RequestHandler(application))
// 设置请求重试次数
.setRetryCount(1)
// 添加全局请求参数
//.addParam("token", "6666666")
// 添加全局请求头
//.addHeader("time", "20191030")
// 启用配置
.into();
// Activity 侧滑返回
SmartSwipeBack.activitySlidingBack(application, activity -> {
if (activity instanceof SwipeAction) {
return ((SwipeAction) activity).isSwipeEnable();
}
return true;
});
// 初始化日志打印
if (AppConfig.isLogEnable()) {
Timber.plant(new DebugLoggerTree());
}
// 注册网络状态变化监听
ConnectivityManager connectivityManager = ContextCompat.getSystemService(application, ConnectivityManager.class);
if (connectivityManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() {
@Override
public void onLost(@NonNull Network network) {
Activity topActivity = ActivityManager.getInstance().getTopActivity();
if (topActivity instanceof LifecycleOwner) {
LifecycleOwner lifecycleOwner = ((LifecycleOwner) topActivity);
if (lifecycleOwner.getLifecycle().getCurrentState() == Lifecycle.State.RESUMED) {
ToastUtils.show(R.string.common_network_error);
}
}
}
});
}
}
}

View File

@ -0,0 +1,76 @@
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
* time : 2018/10/18
* desc : 业务 Fragment 基类
*/
public abstract class AppFragment<A extends AppActivity> extends BaseFragment<A>
implements ToastAction, OnHttpListener<Object> {
/**
* 当前加载对话框是否在显示中
*/
public boolean isShowDialog() {
A activity = getAttachActivity();
if (activity != null) {
return activity.isShowDialog();
}
return false;
}
/**
* 显示加载对话框
*/
public void showDialog() {
A activity = getAttachActivity();
if (activity != null) {
activity.showDialog();
}
}
/**
* 隐藏加载对话框
*/
public void hideDialog() {
A activity = getAttachActivity();
if (activity != null) {
activity.hideDialog();
}
}
/**
* {@link OnHttpListener}
*/
@Override
public void onStart(Call call) {
showDialog();
}
@Override
public void onSucceed(Object result) {
if (result instanceof HttpData) {
toast(((HttpData<?>) result).getMessage());
}
}
@Override
public void onFail(Exception e) {
toast(e.getMessage());
}
@Override
public void onEnd(Call call) {
hideDialog();
}
}

View File

@ -0,0 +1,103 @@
package com.hjq.demo.app;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.gyf.immersionbar.ImmersionBar;
import com.hjq.bar.TitleBar;
import com.hjq.demo.action.TitleBarAction;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/10/31
* desc : 带标题栏的 Fragment 基类
*/
public abstract class TitleBarFragment<A extends AppActivity> extends AppFragment<A>
implements TitleBarAction {
/** 标题栏对象 */
private TitleBar mTitleBar;
/** 状态栏沉浸 */
private ImmersionBar mImmersionBar;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 设置标题栏沉浸
if (isStatusBarEnabled() && getTitleBar() != null) {
ImmersionBar.setTitleBar(this, getTitleBar());
}
if (getTitleBar() != null) {
getTitleBar().setOnTitleBarListener(this);
}
// 初始化沉浸式状态栏
if (isStatusBarEnabled()) {
getStatusBarConfig().init();
}
}
@Override
public void onResume() {
super.onResume();
if (isStatusBarEnabled()) {
// 重新初始化状态栏
getStatusBarConfig().init();
}
}
/**
* 是否在 Fragment 使用沉浸式
*/
public boolean isStatusBarEnabled() {
return false;
}
/**
* 获取状态栏沉浸的配置对象
*/
@NonNull
protected ImmersionBar getStatusBarConfig() {
if (mImmersionBar == null) {
mImmersionBar = createStatusBarConfig();
}
return mImmersionBar;
}
/**
* 初始化沉浸式
*/
@NonNull
protected ImmersionBar createStatusBarConfig() {
return ImmersionBar.with(this)
// 默认状态栏字体颜色为黑色
.statusBarDarkFont(isStatusBarDarkFont())
// 指定导航栏背景颜色
.navigationBarColor(android.R.color.white)
// 状态栏字体和导航栏内容自动变色必须指定状态栏颜色和导航栏颜色才可以自动变色
.autoDarkModeEnable(true, 0.2f);
}
/**
* 获取状态栏字体颜色
*/
protected boolean isStatusBarDarkFont() {
// 返回真表示黑色字体
return getAttachActivity().isStatusBarDarkFont();
}
@Override
@Nullable
public TitleBar getTitleBar() {
if (mTitleBar == null || !isLoading()) {
mTitleBar = obtainTitleBar((ViewGroup) getView());
}
return mTitleBar;
}
}

View File

@ -1,145 +0,0 @@
package com.hjq.demo.common;
import android.app.Application;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import com.billy.android.swipe.SmartSwipeBack;
import com.hjq.bar.TitleBar;
import com.hjq.bar.style.TitleBarLightStyle;
import com.hjq.demo.R;
import com.hjq.demo.action.SwipeAction;
import com.hjq.demo.helper.ActivityStackManager;
import com.hjq.demo.http.model.RequestHandler;
import com.hjq.demo.http.server.ReleaseServer;
import com.hjq.demo.http.server.TestServer;
import com.hjq.demo.other.AppConfig;
import com.hjq.demo.other.CrashHandler;
import com.hjq.http.EasyConfig;
import com.hjq.http.config.IRequestServer;
import com.hjq.toast.ToastInterceptor;
import com.hjq.toast.ToastUtils;
import com.hjq.umeng.UmengClient;
import com.scwang.smartrefresh.layout.SmartRefreshLayout;
import com.scwang.smartrefresh.layout.footer.ClassicsFooter;
import com.scwang.smartrefresh.layout.header.ClassicsHeader;
import com.tencent.bugly.crashreport.CrashReport;
import okhttp3.OkHttpClient;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 项目中的 Application 基类
*/
public final class MyApplication extends Application implements LifecycleOwner {
private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
@Override
public void onCreate() {
super.onCreate();
mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
initSdk(this);
}
@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycle;
}
/**
* 初始化一些第三方框架
*/
public static void initSdk(Application application) {
// 吐司工具类
ToastUtils.init(application);
// 设置 Toast 拦截器
ToastUtils.setToastInterceptor(new ToastInterceptor() {
@Override
public boolean intercept(Toast toast, CharSequence text) {
boolean intercept = super.intercept(toast, text);
if (intercept) {
Log.e("Toast", "空 Toast");
} else {
Log.i("Toast", text.toString());
}
return intercept;
}
});
// 初始化标题栏全局样式
TitleBar.initStyle(new TitleBarLightStyle(application) {
@Override
public Drawable getBackground() {
return new ColorDrawable(ContextCompat.getColor(application, R.color.colorPrimary));
}
@Override
public Drawable getBackIcon() {
return getDrawable(R.drawable.arrows_left_ic);
}
});
// 本地异常捕捉
CrashHandler.register(application);
// 友盟统计登录分享 SDK
UmengClient.init(application);
// Bugly 异常捕捉
CrashReport.initCrashReport(application, AppConfig.getBuglyId(), AppConfig.isDebug());
// 设置全局的 Header 构建器
SmartRefreshLayout.setDefaultRefreshHeaderCreator((context, layout) -> new ClassicsHeader(context).setEnableLastTime(false));
// 设置全局的 Footer 构建器
SmartRefreshLayout.setDefaultRefreshFooterCreator((context, layout) -> new ClassicsFooter(context).setDrawableSize(20));
// Activity 栈管理初始化
ActivityStackManager.getInstance().init(application);
// 网络请求框架初始化
IRequestServer server;
if (AppConfig.isDebug()) {
server = new TestServer();
} else {
server = new ReleaseServer();
}
EasyConfig.with(new OkHttpClient())
// 是否打印日志
//.setLogEnabled(AppConfig.isDebug())
// 设置服务器配置
.setServer(server)
// 设置请求处理策略
.setHandler(new RequestHandler(application))
// 设置请求重试次数
.setRetryCount(1)
// 添加全局请求参数
//.addParam("token", "6666666")
// 添加全局请求头
//.addHeader("time", "20191030")
// 启用配置
.into();
// Activity 侧滑返回
SmartSwipeBack.activitySlidingBack(application, activity -> {
if (activity instanceof SwipeAction) {
return ((SwipeAction) activity).isSwipeEnable();
}
return true;
});
}
}

View File

@ -1,163 +0,0 @@
package com.hjq.demo.common;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.gyf.immersionbar.ImmersionBar;
import com.hjq.bar.TitleBar;
import com.hjq.base.BaseFragment;
import com.hjq.demo.action.TitleBarAction;
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
* time : 2018/10/18
* desc : 项目中 Fragment 懒加载基类
*/
public abstract class MyFragment<A extends MyActivity> extends BaseFragment<A>
implements ToastAction, TitleBarAction, OnHttpListener {
/** 标题栏对象 */
private TitleBar mTitleBar;
/** 状态栏沉浸 */
private ImmersionBar mImmersionBar;
@Override
protected void initFragment() {
super.initFragment();
if (getTitleBar() != null) {
getTitleBar().setOnTitleBarListener(this);
}
// 初始化沉浸式状态栏
if (isStatusBarEnabled()) {
getStatusBarConfig().init();
// 设置标题栏沉浸
if (getTitleBar() != null) {
ImmersionBar.setTitleBar(this, getTitleBar());
}
}
}
/**
* 是否在 Fragment 使用沉浸式
*/
public boolean isStatusBarEnabled() {
return false;
}
/**
* 获取状态栏沉浸的配置对象
*/
@NonNull
protected ImmersionBar getStatusBarConfig() {
if (mImmersionBar == null) {
mImmersionBar = createStatusBarConfig();
}
return mImmersionBar;
}
/**
* 初始化沉浸式
*/
@NonNull
protected ImmersionBar createStatusBarConfig() {
return ImmersionBar.with(this)
// 默认状态栏字体颜色为黑色
.statusBarDarkFont(statusBarDarkFont())
// 解决软键盘与底部输入框冲突问题默认为false还有一个重载方法可以指定软键盘mode
.keyboardEnable(true);
}
/**
* 获取状态栏字体颜色
*/
protected boolean statusBarDarkFont() {
// 返回真表示黑色字体
return true;
}
@Override
@Nullable
public TitleBar getTitleBar() {
if (mTitleBar == null) {
mTitleBar = obtainTitleBar((ViewGroup) getView());
}
return mTitleBar;
}
/**
* 当前加载对话框是否在显示中
*/
public boolean isShowDialog() {
A activity = getAttachActivity();
if (activity != null) {
return activity.isShowDialog();
} else {
return false;
}
}
/**
* 显示加载对话框
*/
public void showDialog() {
A activity = getAttachActivity();
if (activity != null) {
activity.showDialog();
}
}
/**
* 隐藏加载对话框
*/
public void hideDialog() {
A activity = getAttachActivity();
if (activity != null) {
activity.hideDialog();
}
}
/**
* {@link OnHttpListener}
*/
@Override
public void onStart(Call call) {
showDialog();
}
@Override
public void onSucceed(Object result) {
if (result instanceof HttpData) {
toast(((HttpData) result).getMessage());
}
}
@Override
public void onFail(Exception e) {
toast(e.getMessage());
}
@Override
public void onEnd(Call call) {
hideDialog();
}
@Override
public void onResume() {
super.onResume();
if (isStatusBarEnabled()) {
// 重新初始化状态栏
getStatusBarConfig().init();
}
}
}

View File

@ -1,62 +0,0 @@
package com.hjq.demo.helper;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 软键盘工具类
*/
public final class KeyboardUtils {
/**
* 显示软键盘
*
* @param view 依附的View
*/
public static void showKeyboard(View view) {
if (view == null) {
return;
}
InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(view, 0);
}
}
/**
* 隐藏软键盘
*
* @param view 依附的View
*/
public static void hideKeyboard(View view) {
if (view == null) {
return;
}
InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
/**
* 切换软键盘
*
* @param view 依附的View
*/
public static void toggleSoftInput(View view) {
if (view == null) {
return;
}
InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.toggleSoftInput(0, 0);
}
}
}

View File

@ -1,116 +0,0 @@
package com.hjq.demo.helper;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import androidx.annotation.IdRes;
import java.util.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 多个 CompoundButton 选中处理辅助类用于代替 RadioGroup
*/
public final class RadioButtonGroupHelper implements CompoundButton.OnCheckedChangeListener {
/** RadioButton集合 */
private List<RadioButton> mViewSet;
/** 多个RadioButton监听对象 */
private OnCheckedChangeListener mListener;
public RadioButtonGroupHelper(RadioButton... groups) {
mViewSet = new ArrayList<>(groups.length - 1);
for (RadioButton view : groups) {
// 如果这个RadioButton没有设置id的话
if (view.getId() == View.NO_ID) {
throw new IllegalArgumentException("are you ok?");
}
view.setOnCheckedChangeListener(this);
mViewSet.add(view);
}
}
public RadioButtonGroupHelper(View rootView, @IdRes int... ids) {
mViewSet = new ArrayList<>(ids.length - 1);
for (@IdRes int id : ids) {
RadioButton view = rootView.findViewById(id);
view.setOnCheckedChangeListener(this);
mViewSet.add(view);
}
}
/** 监听标记,避免重复回调 */
private boolean mTag;
/**
* {@link CompoundButton.OnCheckedChangeListener}
*/
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked && !mTag) {
mTag = true;
for (CompoundButton view : mViewSet) {
if (view != buttonView && view.isChecked()) {
// 这个 API 会触发监听事件
view.setChecked(false);
}
}
if (mListener != null) {
mListener.onCheckedChanged((RadioButton) buttonView, buttonView.getId());
}
mTag = false;
}
}
/**
* 移除监听避免内存泄露
*/
public void removeViews() {
if (mViewSet == null) {
return;
}
for (CompoundButton view : mViewSet) {
view.setOnCheckedChangeListener(null);
}
mViewSet.clear();
mViewSet = null;
}
/**
* 取消选中
*/
public void clearCheck() {
for (CompoundButton view : mViewSet) {
if (view.isChecked()) {
view.setChecked(false);
}
}
}
/**
* 设置多个RadioButton的监听
*/
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
mListener = listener;
}
/**
* 多个CompoundButton选中监听
*/
public interface OnCheckedChangeListener {
/**
* 被选中的CompoundButton对象
*
* @param radioButton 选中的RadioButton
* @param checkedId 选中的资源id
*/
void onCheckedChanged(RadioButton radioButton, @IdRes int checkedId);
}
}

View File

@ -60,13 +60,16 @@ public final class GlideConfig extends AppGlideModule {
builder.setMemoryCache(new LruResourceCache(customMemoryCacheSize));
builder.setBitmapPool(new LruBitmapPool(customBitmapPoolSize));
// 设置默认的加载占位图和加载出错图
builder.setDefaultRequestOptions(new RequestOptions().placeholder(R.drawable.image_loading_bg).error(R.drawable.image_error_bg));
builder.setDefaultRequestOptions(new RequestOptions()
// 设置默认加载中占位图
.placeholder(R.drawable.image_loading_bg)
// 设置默认加载出错占位图
.error(R.drawable.image_error_bg));
}
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
// Glide 默认用的是 HttpURLConnection 来做网络请求这里切换成更高效的 OkHttp
// Glide 默认使用的是 HttpURLConnection 来做网络请求这里切换成更高效的 OkHttp
registry.replace(GlideUrl.class, InputStream.class, new OkHttpLoader.Factory(EasyConfig.getInstance().getClient()));
}

View File

@ -41,8 +41,7 @@ public final class OkHttpFetcher implements DataFetcher<InputStream>, Callback {
}
@Override
public void loadData(@NonNull Priority priority,
@NonNull final DataFetcher.DataCallback<? super InputStream> callback) {
public void loadData(@NonNull Priority priority, @NonNull final DataFetcher.DataCallback<? super InputStream> callback) {
Request.Builder requestBuilder = new Request.Builder().url(mGlideUrl.toStringUrl());
for (Map.Entry<String, String> headerEntry : mGlideUrl.getHeaders().entrySet()) {
String key = headerEntry.getKey();

View File

@ -32,8 +32,7 @@ public final class OkHttpLoader implements ModelLoader<GlideUrl, InputStream> {
}
@Override
public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height,
@NonNull Options options) {
public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) {
return new LoadData<>(model, new OkHttpFetcher(mFactory, model));
}

View File

@ -1,40 +0,0 @@
package com.hjq.demo.http.json;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/05/05
* desc : boolean / Boolean 类型解析适配器参考{@link com.google.gson.internal.bind.TypeAdapters#BOOLEAN}
*/
public class BooleanTypeAdapter extends TypeAdapter<Boolean> {
@Override
public Boolean read(JsonReader in) throws IOException {
switch (in.peek()) {
case BOOLEAN:
return in.nextBoolean();
case STRING:
// 如果后台返回 "true" 或者 "TRUE"则处理为 true否则为 false
return Boolean.parseBoolean(in.nextString());
case NUMBER:
// 如果这个后台返回是 1 则处理为 true否则为 false
return in.nextInt() == 1;
case NULL:
in.nextNull();
return null;
default:
in.skipValue();
return null;
}
}
@Override
public void write(JsonWriter out, Boolean value) throws IOException {
out.value(value);
}
}

View File

@ -1,41 +0,0 @@
package com.hjq.demo.http.json;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/05/05
* desc : double / Double 类型解析适配器参考{@link com.google.gson.internal.bind.TypeAdapters#DOUBLE}
*/
public class DoubleTypeAdapter extends TypeAdapter<Number> {
@Override
public Number read(JsonReader in) throws IOException {
switch (in.peek()) {
case NUMBER:
return in.nextDouble();
case STRING:
try {
return Double.parseDouble(in.nextString());
} catch (NumberFormatException e) {
// 如果是空字符串则会抛出这个异常
return 0;
}
case NULL:
in.nextNull();
return null;
default:
in.skipValue();
return 0;
}
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
out.value(value);
}
}

View File

@ -1,23 +0,0 @@
package com.hjq.demo.http.json;
import com.google.gson.stream.JsonReader;
import java.io.IOException;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/05/05
* desc : float / Float 类型解析适配器参考{@link com.google.gson.internal.bind.TypeAdapters#FLOAT}
*/
public class FloatTypeAdapter extends DoubleTypeAdapter {
@Override
public Number read(JsonReader in) throws IOException {
Number number = super.read(in);
if (number != null) {
return number.floatValue();
}
return null;
}
}

View File

@ -1,23 +0,0 @@
package com.hjq.demo.http.json;
import com.google.gson.stream.JsonReader;
import java.io.IOException;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/05/05
* desc : int / Integer 类型解析适配器参考{@link com.google.gson.internal.bind.TypeAdapters#INTEGER}
*/
public class IntegerTypeAdapter extends DoubleTypeAdapter {
@Override
public Number read(JsonReader in) throws IOException {
Number number = super.read(in);
if (number != null) {
return number.intValue();
}
return null;
}
}

View File

@ -1,43 +0,0 @@
package com.hjq.demo.http.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/05/05
* desc : List 类型解析适配器
*/
public class ListTypeAdapter implements JsonDeserializer<List> {
@SuppressWarnings("unchecked")
@Override
public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// 如果这是一个数组
if (json.isJsonArray()) {
JsonArray array = json.getAsJsonArray();
// 获取 List 上的泛型
Type itemType = ((ParameterizedType) typeOfT).getActualTypeArguments()[0];
List list = new ArrayList();
for (int i = 0; i < array.size(); i++) {
JsonElement element = array.get(i);
// 解析 List 中的条目对象
Object item = context.deserialize(element, itemType);
list.add(item);
}
return list;
} else {
// 类型不符直接返回 null
return null;
}
}
}

View File

@ -1,23 +0,0 @@
package com.hjq.demo.http.json;
import com.google.gson.stream.JsonReader;
import java.io.IOException;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/05/05
* desc : long / Long 类型解析适配器参考{@link com.google.gson.internal.bind.TypeAdapters#LONG}
*/
public class LongTypeAdapter extends DoubleTypeAdapter {
@Override
public Number read(JsonReader in) throws IOException {
Number number = super.read(in);
if (number != null) {
return number.longValue();
}
return null;
}
}

View File

@ -1,39 +0,0 @@
package com.hjq.demo.http.json;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/05/05
* desc : String 类型解析适配器参考{@link com.google.gson.internal.bind.TypeAdapters#STRING}
*/
public class StringTypeAdapter extends TypeAdapter<String> {
@Override
public String read(JsonReader in) throws IOException {
switch (in.peek()) {
case STRING:
case NUMBER:
return in.nextString();
case BOOLEAN:
// 对于布尔类型比较特殊需要做针对性处理
return Boolean.toString(in.nextBoolean());
case NULL:
in.nextNull();
return null;
default:
in.skipValue();
return null;
}
}
@Override
public void write(JsonWriter out, String value) throws IOException {
out.value(value);
}
}

View File

@ -0,0 +1,47 @@
package com.hjq.demo.http.model;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/EasyHttp
* time : 2020/10/07
* desc : 统一接口列表数据结构
*/
public class HttpListData<T> extends HttpData<HttpListData.ListBean<T>> {
public static class ListBean<T> {
/** 当前页码 */
private int pageIndex;
/** 页大小 */
private int pageSize;
/** 总数量 */
private int totalNumber;
/** 数据 */
private List<T> items;
/**
* 判断是否是最后一页
*/
public boolean isLastPage() {
return Math.ceil((float) totalNumber / pageSize) <= pageIndex;
}
public int getTotalNumber() {
return totalNumber;
}
public int getPageIndex() {
return pageIndex;
}
public int getPageSize() {
return pageSize;
}
public List<T> getItems() {
return items;
}
}
}

View File

@ -3,27 +3,16 @@ package com.hjq.demo.http.model;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import androidx.lifecycle.LifecycleOwner;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.internal.bind.TypeAdapters;
import com.hjq.demo.R;
import com.hjq.demo.helper.ActivityStackManager;
import com.hjq.demo.http.json.BooleanTypeAdapter;
import com.hjq.demo.http.json.DoubleTypeAdapter;
import com.hjq.demo.http.json.FloatTypeAdapter;
import com.hjq.demo.http.json.IntegerTypeAdapter;
import com.hjq.demo.http.json.ListTypeAdapter;
import com.hjq.demo.http.json.LongTypeAdapter;
import com.hjq.demo.http.json.StringTypeAdapter;
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.IRequestHandler;
import com.hjq.http.exception.CancelException;
@ -41,11 +30,12 @@ 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;
import java.util.List;
import okhttp3.Headers;
import okhttp3.Response;
import okhttp3.ResponseBody;
@ -59,8 +49,6 @@ public final class RequestHandler implements IRequestHandler {
private final Application mApplication;
private Gson mGson;
public RequestHandler(Application application) {
mApplication = application;
}
@ -77,14 +65,17 @@ public final class RequestHandler implements IRequestHandler {
throw new ResponseException(mApplication.getString(R.string.http_response_error) + "responseCode" + response.code() + "message" + response.message(), response);
}
if (Headers.class.equals(type)) {
return response.headers();
}
ResponseBody body = response.body();
if (body == null) {
return null;
}
if (Bitmap.class.equals(type)) {
// 如果这是一个 Bitmap 对象
return BitmapFactory.decodeStream(body.byteStream());
if (InputStream.class.equals(type)) {
return body.byteStream();
}
String text;
@ -95,61 +86,52 @@ public final class RequestHandler implements IRequestHandler {
throw new DataException(mApplication.getString(R.string.http_data_explain_error), e);
}
// 打印这个 Json
// 打印这个 Json 或者文本
EasyLog.json(text);
final Object result;
if (String.class.equals(type)) {
// 如果这是一个 String 对象
result = text;
} else if (JSONObject.class.equals(type)) {
return text;
}
if (JSONObject.class.equals(type)) {
try {
// 如果这是一个 JSONObject 对象
result = new JSONObject(text);
return new JSONObject(text);
} catch (JSONException e) {
throw new DataException(mApplication.getString(R.string.http_data_explain_error), e);
}
} else if (JSONArray.class.equals(type)) {
}
if (JSONArray.class.equals(type)) {
try {
// 如果这是一个 JSONArray 对象
result = new JSONArray(text);
}catch (JSONException e) {
return new JSONArray(text);
} catch (JSONException e) {
throw new DataException(mApplication.getString(R.string.http_data_explain_error), e);
}
} else {
}
try {
if (mGson == null) {
// Json 容错处理
mGson = new GsonBuilder()
.registerTypeAdapterFactory(TypeAdapters.newFactory(String.class, new StringTypeAdapter()))
.registerTypeAdapterFactory(TypeAdapters.newFactory(boolean.class, Boolean.class, new BooleanTypeAdapter()))
.registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, new IntegerTypeAdapter()))
.registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, new LongTypeAdapter()))
.registerTypeAdapterFactory(TypeAdapters.newFactory(float.class, Float.class, new FloatTypeAdapter()))
.registerTypeAdapterFactory(TypeAdapters.newFactory(double.class, Double.class, new DoubleTypeAdapter()))
.registerTypeHierarchyAdapter(List.class, new ListTypeAdapter())
.create();
}
result = mGson.fromJson(text, type);
} catch (JsonSyntaxException e) {
// 返回结果读取异常
throw new DataException(mApplication.getString(R.string.http_data_explain_error), e);
final Object result;
try {
result = GsonFactory.getSingletonGson().fromJson(text, type);
} catch (JsonSyntaxException e) {
// 返回结果读取异常
throw new DataException(mApplication.getString(R.string.http_data_explain_error), e);
}
if (result instanceof HttpData) {
HttpData<?> model = (HttpData<?>) result;
if (model.getCode() == 0) {
// 代表执行成功
return result;
} else if (model.getCode() == 1001) {
// 代表登录失效需要重新登录
throw new TokenException(mApplication.getString(R.string.http_account_error));
}
if (result instanceof HttpData) {
HttpData model = (HttpData) result;
if (model.getCode() == 0) {
// 代表执行成功
return result;
} else if (model.getCode() == 1001) {
// 代表登录失效需要重新登录
throw new TokenException(mApplication.getString(R.string.http_account_error));
} else {
// 代表执行失败
throw new ResultException(model.getMessage(), model);
}
}
// 代表执行失败
throw new ResultException(model.getMessage(), model);
}
return result;
}
@ -160,33 +142,37 @@ public final class RequestHandler implements IRequestHandler {
if (e instanceof HttpException) {
if (e instanceof TokenException) {
// 登录信息失效跳转到登录页
Application application = ActivityStackManager.getInstance().getApplication();
Application application = ActivityManager.getInstance().getApplication();
Intent intent = new Intent(application, LoginActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
application.startActivity(intent);
// 销毁除了登录页之外的界面
ActivityStackManager.getInstance().finishAllActivities(LoginActivity.class);
}
} else {
if (e instanceof SocketTimeoutException) {
e = new TimeoutException(mApplication.getString(R.string.http_server_out_time), e);
} else if (e instanceof UnknownHostException) {
NetworkInfo info = ((ConnectivityManager) mApplication.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
// 判断网络是否连接
if (info != null && info.isConnected()) {
// 有连接就是服务器的问题
e = new ServerException(mApplication.getString(R.string.http_server_error), e);
} else {
// 没有连接就是网络异常
e = new NetworkException(mApplication.getString(R.string.http_network_error), e);
}
} else if (e instanceof IOException) {
//e = new CancelException(context.getString(R.string.http_request_cancel), e);
e = new CancelException("", e);
} else {
e = new HttpException(e.getMessage(), e);
ActivityManager.getInstance().finishAllActivities(LoginActivity.class);
}
return e;
}
return e;
if (e instanceof SocketTimeoutException) {
return new TimeoutException(mApplication.getString(R.string.http_server_out_time), e);
}
if (e instanceof UnknownHostException) {
NetworkInfo info = ((ConnectivityManager) mApplication.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
// 判断网络是否连接
if (info != null && info.isConnected()) {
// 有连接就是服务器的问题
return new ServerException(mApplication.getString(R.string.http_server_error), e);
}
// 没有连接就是网络异常
return new NetworkException(mApplication.getString(R.string.http_network_error), e);
}
if (e instanceof IOException) {
//e = new CancelException(context.getString(R.string.http_request_cancel), e);
return new CancelException("", e);
}
return new HttpException(e.getMessage(), e);
}
}

View File

@ -0,0 +1,30 @@
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
* time : 2020/10/02
* desc : 服务器配置
*/
public class RequestServer implements IRequestServer {
@Override
public String getHost() {
return AppConfig.getHostUrl();
}
@Override
public String getPath() {
return "api/";
}
@Override
public BodyType getType() {
// 以表单的形式提交参数
return BodyType.FORM;
}
}

View File

@ -1,22 +0,0 @@
package com.hjq.demo.http.server;
import com.hjq.http.config.IRequestServer;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/07
* desc : 正式环境
*/
public class ReleaseServer implements IRequestServer {
@Override
public String getHost() {
return "https://www.baidu.com/";
}
@Override
public String getPath() {
return "api/";
}
}

View File

@ -1,15 +0,0 @@
package com.hjq.demo.http.server;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/07
* desc : 测试环境
*/
public class TestServer extends ReleaseServer {
@Override
public String getHost() {
return "https://www.baidu.com/";
}
}

View File

@ -1,4 +1,4 @@
package com.hjq.demo.helper;
package com.hjq.demo.manager;
import android.app.Activity;
import android.app.Application;
@ -8,31 +8,34 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import timber.log.Timber;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/11/18
* desc : Activity 管理
* desc : Activity 管理
*/
public final class ActivityStackManager implements Application.ActivityLifecycleCallbacks {
public final class ActivityManager implements Application.ActivityLifecycleCallbacks {
private static volatile ActivityStackManager sInstance;
private static volatile ActivityManager sInstance;
private final ArrayMap<String, Activity> mActivitySet = new ArrayMap<>();
/** 当前应用上下文对象 */
private Application mApplication;
/** 当前 Activity 对象标记 */
private String mCurrentTag;
/** 最后一个可见 Activity 标记 */
private String mLastVisibleTag;
/** 最后一个不可见 Activity 标记 */
private String mLastInvisibleTag;
private ActivityStackManager() {}
private ActivityManager() {}
public static ActivityStackManager getInstance() {
// 加入双重校验锁
public static ActivityManager getInstance() {
if(sInstance == null) {
synchronized (ActivityStackManager.class) {
if(sInstance == null){
sInstance = new ActivityStackManager();
synchronized (ActivityManager.class) {
if(sInstance == null) {
sInstance = new ActivityManager();
}
}
}
@ -41,7 +44,7 @@ public final class ActivityStackManager implements Application.ActivityLifecycle
public void init(Application application) {
mApplication = application;
application.registerActivityLifecycleCallbacks(this);
mApplication.registerActivityLifecycleCallbacks(this);
}
/**
@ -55,7 +58,19 @@ public final class ActivityStackManager implements Application.ActivityLifecycle
* 获取栈顶的 Activity
*/
public Activity getTopActivity() {
return mActivitySet.get(mCurrentTag);
return mActivitySet.get(mLastVisibleTag);
}
/**
* 判断当前应用是否处于前台状态
*/
public boolean isForeground() {
// 如果最后一个可见的 Activity 和最后一个不可见的 Activity 是同一个的话
if (mLastVisibleTag.equals(mLastInvisibleTag)) {
return false;
}
Activity activity = getTopActivity();
return activity != null;
}
/**
@ -91,6 +106,53 @@ public final class ActivityStackManager implements Application.ActivityLifecycle
}
}
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
Timber.i("%s - onCreate", activity.getClass().getSimpleName());
mLastVisibleTag = getObjectTag(activity);
mActivitySet.put(getObjectTag(activity), activity);
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
Timber.i("%s - onStart", activity.getClass().getSimpleName());
mLastVisibleTag = getObjectTag(activity);
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
Timber.i("%s - onResume", activity.getClass().getSimpleName());
mLastVisibleTag = getObjectTag(activity);
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
Timber.i("%s - onPause", activity.getClass().getSimpleName());
mLastInvisibleTag = getObjectTag(activity);
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
Timber.i("%s - onStop", activity.getClass().getSimpleName());
mLastInvisibleTag = getObjectTag(activity);
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
Timber.i("%s - onSaveInstanceState", activity.getClass().getSimpleName());
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
Timber.i("%s - onDestroy", activity.getClass().getSimpleName());
mActivitySet.remove(getObjectTag(activity));
mLastInvisibleTag = getObjectTag(activity);
if (getObjectTag(activity).equals(mLastVisibleTag)) {
// 清除当前标记
mLastVisibleTag = null;
}
}
/**
* 获取一个对象的独立无二的标记
*/
@ -98,38 +160,4 @@ public final class ActivityStackManager implements Application.ActivityLifecycle
// 对象所在的包名 + 对象的内存地址
return object.getClass().getName() + Integer.toHexString(object.hashCode());
}
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
mCurrentTag = getObjectTag(activity);
mActivitySet.put(getObjectTag(activity), activity);
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
mCurrentTag = getObjectTag(activity);
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
mCurrentTag = getObjectTag(activity);
}
@Override
public void onActivityPaused(@NonNull Activity activity) {}
@Override
public void onActivityStopped(@NonNull Activity activity) {}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
mActivitySet.remove(getObjectTag(activity));
if (getObjectTag(activity).equals(mCurrentTag)) {
// 清除当前标记
mCurrentTag = null;
}
}
}

View File

@ -1,4 +1,4 @@
package com.hjq.demo.helper;
package com.hjq.demo.manager;
import android.content.Context;
import android.os.Environment;

View File

@ -0,0 +1,94 @@
package com.hjq.demo.manager;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import com.hjq.base.BaseDialog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2021/01/29
* desc : Dialog 显示管理类
*/
public final class DialogManager implements LifecycleEventObserver, BaseDialog.OnDismissListener {
private final static HashMap<LifecycleOwner, DialogManager> DIALOG_MANAGER = new HashMap<>();
public static DialogManager getInstance(LifecycleOwner lifecycleOwner) {
DialogManager manager = DIALOG_MANAGER.get(lifecycleOwner);
if (manager == null) {
manager = new DialogManager(lifecycleOwner);
DIALOG_MANAGER.put(lifecycleOwner, manager);
}
return manager;
}
private final List<BaseDialog> mDialogs = new ArrayList<>();
private DialogManager(LifecycleOwner lifecycleOwner) {
lifecycleOwner.getLifecycle().addObserver(this);
}
/**
* 排队显示 Dialog
*/
public void addShow(BaseDialog dialog) {
if (dialog == null || dialog.isShowing()) {
throw new IllegalStateException("are you ok?");
}
mDialogs.add(dialog);
BaseDialog firstDialog = mDialogs.get(0);
if (!firstDialog.isShowing()) {
firstDialog.addOnDismissListener(this);
firstDialog.show();
}
}
/**
* 取消所有 Dialog 的显示
*/
public void clearShow() {
if (mDialogs.isEmpty()) {
return;
}
BaseDialog firstDialog = mDialogs.get(0);
if (firstDialog.isShowing()) {
firstDialog.removeOnDismissListener(this);
firstDialog.dismiss();
}
mDialogs.clear();
}
@Override
public void onDismiss(BaseDialog dialog) {
dialog.removeOnDismissListener(this);
mDialogs.remove(dialog);
for (BaseDialog nextDialog : mDialogs) {
if (!nextDialog.isShowing()) {
nextDialog.addOnDismissListener(this);
nextDialog.show();
break;
}
}
}
/**
* {@link LifecycleEventObserver}
*/
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event != Lifecycle.Event.ON_DESTROY) {
return;
}
source.getLifecycle().removeObserver(this);
clearShow();
}
}

View File

@ -1,7 +1,8 @@
package com.hjq.demo.helper;
package com.hjq.demo.manager;
import android.app.Activity;
import android.app.Application;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
@ -17,15 +18,15 @@ import java.util.List;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 文本输入辅助通过管理多个 TextView 输入是否为空来启用或者禁用按钮的点击事件
* desc : 文本输入管理通过管理多个 TextView 输入是否为空来启用或者禁用按钮的点击事件
* blog : https://www.jianshu.com/p/fd3795e8a6b3
*/
public final class InputTextHelper implements TextWatcher {
public final class InputTextManager implements TextWatcher {
/** 操作按钮的View */
private View mView;
private final View mView;
/** 是否禁用后设置半透明度 */
private boolean isAlpha;
private final boolean mAlpha;
/** TextView集合 */
private List<TextView> mViewSet;
@ -39,12 +40,12 @@ public final class InputTextHelper implements TextWatcher {
* @param view 跟随 TextView 输入为空来判断启动或者禁用这个 View
* @param alpha 是否需要设置透明度
*/
private InputTextHelper(View view, boolean alpha) {
private InputTextManager(View view, boolean alpha) {
if (view == null) {
throw new IllegalArgumentException("are you ok?");
}
mView = view;
isAlpha = alpha;
mAlpha = alpha;
}
/**
@ -190,14 +191,14 @@ public final class InputTextHelper implements TextWatcher {
if (enabled) {
//启用View的事件
mView.setEnabled(true);
if (isAlpha) {
if (mAlpha) {
//设置不透明
mView.setAlpha(1f);
}
} else {
//禁用View的事件
mView.setEnabled(false);
if (isAlpha) {
if (mAlpha) {
//设置半透明
mView.setAlpha(0.5f);
}
@ -241,11 +242,16 @@ public final class InputTextHelper implements TextWatcher {
return this;
}
public InputTextHelper build(){
InputTextHelper helper = new InputTextHelper(mView, isAlpha);
public InputTextManager build() {
InputTextManager helper = new InputTextManager(mView, isAlpha);
helper.addViews(mViewSet);
helper.setListener(mListener);
mActivity.getApplication().registerActivityLifecycleCallbacks(new TextInputLifecycle(mActivity, helper));
TextInputLifecycle lifecycle = new TextInputLifecycle(mActivity, helper);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mActivity.registerActivityLifecycleCallbacks(lifecycle);
} else {
mActivity.getApplication().registerActivityLifecycleCallbacks(lifecycle);
}
return helper;
}
}
@ -253,11 +259,11 @@ public final class InputTextHelper implements TextWatcher {
private static class TextInputLifecycle implements Application.ActivityLifecycleCallbacks {
private Activity mActivity;
private InputTextHelper mTextHelper;
private InputTextManager mTextHelper;
private TextInputLifecycle(Activity activity, InputTextHelper helper) {
this.mActivity = activity;
this.mTextHelper = helper;
private TextInputLifecycle(Activity activity, InputTextManager helper) {
mActivity = activity;
mTextHelper = helper;
}
@Override
@ -282,7 +288,11 @@ public final class InputTextHelper implements TextWatcher {
public void onActivityDestroyed(@NonNull Activity activity) {
if (mActivity != null && mActivity == activity) {
mTextHelper.removeAllViews();
mActivity.getApplication().registerActivityLifecycleCallbacks(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mActivity.unregisterActivityLifecycleCallbacks(this);
} else {
mActivity.getApplication().unregisterActivityLifecycleCallbacks(this);
}
mTextHelper = null;
mActivity = null;
}
@ -298,6 +308,6 @@ public final class InputTextHelper implements TextWatcher {
* 输入发生了变化
* @return 返回按钮的 Enabled 状态
*/
boolean onInputChange(InputTextHelper helper);
boolean onInputChange(InputTextManager helper);
}
}

View File

@ -1,4 +1,4 @@
package com.hjq.demo.other;
package com.hjq.demo.manager;
import android.content.Context;
import android.view.View;
@ -15,7 +15,7 @@ import androidx.recyclerview.widget.RecyclerView;
* time : 2019/09/11
* desc : 选择器布局管理器
*/
public class PickerLayoutManager extends LinearLayoutManager {
public final class PickerLayoutManager extends LinearLayoutManager {
private final LinearSnapHelper mLinearSnapHelper;
private final int mOrientation;
@ -103,9 +103,9 @@ public class PickerLayoutManager extends LinearLayoutManager {
return;
}
if (mOrientation == HORIZONTAL){
if (mOrientation == HORIZONTAL) {
scaleHorizontalChildView();
} else if (mOrientation == VERTICAL){
} else if (mOrientation == VERTICAL) {
scaleVerticalChildView();
}
}
@ -144,7 +144,7 @@ public class PickerLayoutManager extends LinearLayoutManager {
/**
* 竖向方向上的缩放
*/
private void scaleVerticalChildView(){
private void scaleVerticalChildView() {
float mid = getHeight() / 2.0f;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);

View File

@ -0,0 +1,33 @@
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
* time : 2020/01/11
* desc : 线程池管理类
*/
public final class ThreadPoolManager extends ThreadPoolExecutor {
private static volatile ThreadPoolManager sInstance;
public ThreadPoolManager() {
super(0, Integer.MAX_VALUE,
30L, TimeUnit.SECONDS,
new SynchronousQueue<>());
}
public static ThreadPoolManager getInstance() {
if(sInstance == null) {
synchronized (ActivityManager.class) {
if(sInstance == null) {
sInstance = new ThreadPoolManager();
}
}
}
return sInstance;
}
}

View File

@ -11,12 +11,26 @@ import com.hjq.demo.BuildConfig;
public final class AppConfig {
/**
* 当前是否为 Debug 模式
* 当前是否为调试模式
*/
public static boolean isDebug() {
return BuildConfig.DEBUG;
}
/**
* 获取当前构建的模式
*/
public static String getBuildType() {
return BuildConfig.BUILD_TYPE;
}
/**
* 当前是否要开启日志打印功能
*/
public static boolean isLogEnable() {
return BuildConfig.LOG_ENABLE;
}
/**
* 获取当前应用的包名
*/
@ -39,9 +53,16 @@ public final class AppConfig {
}
/**
* 获取 BuglyId
* 获取 Bugly Id
*/
public static String getBuglyId() {
return BuildConfig.BUGLY_ID;
}
/**
* 获取服务器主机地址
*/
public static String getHostUrl() {
return BuildConfig.HOST_URL;
}
}

View File

@ -29,9 +29,9 @@ import androidx.annotation.Nullable;
@SuppressLint("RtlHardcoded")
public final class ArrowDrawable extends Drawable {
private Builder mBuilder;
private final Builder mBuilder;
private final Paint mPaint;
private Path mPath;
private Paint mPaint;
private ArrowDrawable(Builder builder) {
mBuilder = builder;
@ -181,7 +181,7 @@ public final class ArrowDrawable extends Drawable {
public static final class Builder {
/** 上下文对象 */
private Context mContext;
private final Context mContext;
/** 箭头高度 */
private int mArrowHeight;
/** 背景圆角大小 */

View File

@ -1,10 +1,21 @@
package com.hjq.demo.other;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import com.hjq.demo.ui.activity.CrashActivity;
import com.hjq.demo.ui.activity.HomeActivity;
import com.hjq.demo.ui.activity.LoginActivity;
import com.hjq.demo.ui.activity.RestartActivity;
import com.hjq.demo.ui.activity.SplashActivity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* author : Android 轮子哥
@ -14,32 +25,54 @@ import com.hjq.demo.ui.activity.CrashActivity;
*/
public final class CrashHandler implements Thread.UncaughtExceptionHandler {
/** Crash 文件名 */
private static final String CRASH_FILE_NAME = "crash_file";
/** Crash 时间记录 */
private static final String KEY_CRASH_TIME = "key_crash_time";
/**
* 注册 Crash 监听
*/
public static void register(Application application) {
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(application));
}
private Application mApplication;
private Thread.UncaughtExceptionHandler mOldHandler;
private final Application mApplication;
private final Thread.UncaughtExceptionHandler mNextHandler;
private CrashHandler(Application application) {
mApplication = application;
mOldHandler = Thread.getDefaultUncaughtExceptionHandler();
if (getClass().getName().equals(mOldHandler.getClass().getName())) {
mNextHandler = Thread.getDefaultUncaughtExceptionHandler();
if (getClass().getName().equals(mNextHandler.getClass().getName())) {
// 请不要重复注册 Crash 监听
throw new IllegalStateException("are you ok?");
}
}
@SuppressLint("ApplySharedPref")
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
CrashActivity.start(mApplication, throwable);
// 不去触发系统的崩溃处理com.android.internal.os.RuntimeInit$KillApplicationHandler
if (mOldHandler != null && !mOldHandler.getClass().getName().startsWith("com.android.internal.os")) {
mOldHandler.uncaughtException(thread, throwable);
SharedPreferences sharedPreferences = mApplication.getSharedPreferences(CRASH_FILE_NAME, Context.MODE_PRIVATE);
long currentCrashTime = System.currentTimeMillis();
long lastCrashTime = sharedPreferences.getLong(KEY_CRASH_TIME, 0);
// 记录当前崩溃的时间以便下次崩溃时进行比对
sharedPreferences.edit().putLong(KEY_CRASH_TIME, currentCrashTime).commit();
// 致命异常标记如果上次崩溃的时间距离当前崩溃小于 5 分钟那么判定为致命异常
boolean deadlyCrash = currentCrashTime - lastCrashTime < 1000 * 60 * 5;
// 如果是致命的异常或者是调试模式下
if (deadlyCrash || AppConfig.isDebug()) {
CrashActivity.start(mApplication, throwable);
} else {
RestartActivity.start(mApplication);
}
// 不去触发系统的崩溃处理com.android.internal.os.RuntimeInit$KillApplicationHandler
if (mNextHandler != null && !mNextHandler.getClass().getName().startsWith("com.android.internal.os")) {
mNextHandler.uncaughtException(thread, throwable);
}
// 杀死进程这个事应该是系统干的但是它会多弹出一个崩溃对话框所以需要我们自己手动杀死进程
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);

View File

@ -0,0 +1,31 @@
package com.hjq.demo.other;
import android.os.Build;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/08/12
* desc : 自定义日志打印规则
*/
public final class DebugLoggerTree extends Timber.DebugTree {
private static final int MAX_TAG_LENGTH = 23;
/**
* 创建日志堆栈 TAG
*/
@Override
protected String createStackElementTag(@NotNull StackTraceElement element) {
String tag = "(" + element.getFileName() + ":" + element.getLineNumber() + ")";
// 日志 TAG 长度限制已经在 Android 7.0 被移除
if (tag.length() <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return tag;
}
return tag.substring(0, MAX_TAG_LENGTH);
}
}

View File

@ -1,4 +1,4 @@
package com.hjq.demo.helper;
package com.hjq.demo.other;
import android.os.SystemClock;
@ -6,7 +6,7 @@ import android.os.SystemClock;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 双击判断工具类
* desc : 双击判断工具类
*/
public final class DoubleClickHelper {

View File

@ -40,6 +40,8 @@ public final class IntentKey {
public static final String AMOUNT = "amount";
/** 总数 */
public static final String COUNT = "count";
/** 标记 */
public static final String FLAG = "flag";
/** 其他 */
public static final String OTHER = "other";

View File

@ -3,6 +3,7 @@ package com.hjq.demo.other;
import android.app.Activity;
import android.app.Application;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewTreeObserver;
@ -35,7 +36,11 @@ public final class KeyboardWatcher implements
mActivity = activity;
mContentView = activity.findViewById(Window.ID_ANDROID_CONTENT);
mActivity.getApplication().registerActivityLifecycleCallbacks(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mActivity.registerActivityLifecycleCallbacks(this);
} else {
mActivity.getApplication().registerActivityLifecycleCallbacks(this);
}
mContentView.getViewTreeObserver().addOnGlobalLayoutListener(this);
// 获取 status_bar_height 资源的 ID
@ -109,7 +114,11 @@ public final class KeyboardWatcher implements
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
if (mActivity == activity) {
mActivity.getApplication().unregisterActivityLifecycleCallbacks(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mActivity.unregisterActivityLifecycleCallbacks(this);
} else {
mActivity.getApplication().unregisterActivityLifecycleCallbacks(this);
}
mContentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
mActivity = null;
@ -125,6 +134,7 @@ public final class KeyboardWatcher implements
/**
* 软键盘弹出了
*
* @param keyboardHeight 软键盘高度
*/
void onSoftKeyboardOpened(int keyboardHeight);

View File

@ -0,0 +1,221 @@
package com.hjq.demo.other;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import com.hjq.demo.R;
import com.hjq.demo.manager.ActivityManager;
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.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/10/24
* desc : 权限申请回调封装
*/
public abstract class PermissionCallback implements OnPermissionCallback {
@Override
public void onDenied(List<String> permissions, boolean never) {
if (never) {
showPermissionDialog(permissions);
return;
}
if (permissions.size() == 1 && Permission.ACCESS_BACKGROUND_LOCATION.equals(permissions.get(0))) {
ToastUtils.show(R.string.common_permission_fail_4);
return;
}
ToastUtils.show(R.string.common_permission_fail_1);
}
/**
* 显示授权对话框
*/
protected void showPermissionDialog(List<String> permissions) {
Activity activity = ActivityManager.getInstance().getTopActivity();
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
return;
}
new MessageDialog.Builder(activity)
.setTitle(R.string.common_permission_alert)
.setMessage(getPermissionHint(activity, permissions))
.setConfirm(R.string.common_permission_goto)
.setCancel(null)
.setCancelable(false)
.setListener(dialog -> XXPermissions.startPermissionActivity(activity, permissions))
.show();
}
/**
* 根据权限获取提示
*/
protected String getPermissionHint(Context context, List<String> permissions) {
if (permissions == null || permissions.isEmpty()) {
return context.getString(R.string.common_permission_fail_2);
}
List<String> hints = new ArrayList<>();
for (String permission : permissions) {
switch (permission) {
case Permission.READ_EXTERNAL_STORAGE:
case Permission.WRITE_EXTERNAL_STORAGE:
case Permission.MANAGE_EXTERNAL_STORAGE: {
String hint = context.getString(R.string.common_permission_storage);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.CAMERA: {
String hint = context.getString(R.string.common_permission_camera);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.RECORD_AUDIO: {
String hint = context.getString(R.string.common_permission_microphone);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.ACCESS_FINE_LOCATION:
case Permission.ACCESS_COARSE_LOCATION:
case Permission.ACCESS_BACKGROUND_LOCATION: {
String hint;
if (!permissions.contains(Permission.ACCESS_FINE_LOCATION) &&
!permissions.contains(Permission.ACCESS_COARSE_LOCATION)) {
hint = context.getString(R.string.common_permission_location_background);
} else {
hint = context.getString(R.string.common_permission_location);
}
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.READ_PHONE_STATE:
case Permission.CALL_PHONE:
case Permission.ADD_VOICEMAIL:
case Permission.USE_SIP:
case Permission.READ_PHONE_NUMBERS:
case Permission.ANSWER_PHONE_CALLS: {
String hint = context.getString(R.string.common_permission_phone);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.GET_ACCOUNTS:
case Permission.READ_CONTACTS:
case Permission.WRITE_CONTACTS: {
String hint = context.getString(R.string.common_permission_contacts);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.READ_CALENDAR:
case Permission.WRITE_CALENDAR: {
String hint = context.getString(R.string.common_permission_calendar);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.READ_CALL_LOG:
case Permission.WRITE_CALL_LOG:
case Permission.PROCESS_OUTGOING_CALLS: {
String hint = context.getString(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ?
R.string.common_permission_call_log : R.string.common_permission_phone);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.BODY_SENSORS: {
String hint = context.getString(R.string.common_permission_sensors);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.ACTIVITY_RECOGNITION: {
String hint = context.getString(R.string.common_permission_activity_recognition);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.SEND_SMS:
case Permission.RECEIVE_SMS:
case Permission.READ_SMS:
case Permission.RECEIVE_WAP_PUSH:
case Permission.RECEIVE_MMS: {
String hint = context.getString(R.string.common_permission_sms);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.REQUEST_INSTALL_PACKAGES: {
String hint = context.getString(R.string.common_permission_install);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.NOTIFICATION_SERVICE: {
String hint = context.getString(R.string.common_permission_notification);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.SYSTEM_ALERT_WINDOW: {
String hint = context.getString(R.string.common_permission_window);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
case Permission.WRITE_SETTINGS: {
String hint = context.getString(R.string.common_permission_setting);
if (!hints.contains(hint)) {
hints.add(hint);
}
break;
}
default:
break;
}
}
if (!hints.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (String text : hints) {
if (builder.length() == 0) {
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);
}
}

View File

@ -0,0 +1,175 @@
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.util.TypedValue;
import android.view.animation.AccelerateDecelerateInterpolator;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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 com.scwang.smart.refresh.layout.util.SmartUtil;
/**
* author : 树朾 & Android 轮子哥
* github : https://github.com/scwang90/SmartRefreshLayout/tree/master/refresh-footer-ball
* time : 2020/08/01
* desc : 球脉冲底部加载组件
*/
public final class SmartBallPulseFooter extends SimpleComponent implements RefreshFooter {
private final TimeInterpolator mInterpolator = new AccelerateDecelerateInterpolator();
private boolean mNoMoreData;
private boolean mManualNormalColor;
private boolean mManualAnimationColor;
private final Paint mPaint;
private int mNormalColor = 0xFFEEEEEE;
private int[] mAnimatingColor = {0xFF30B399, 0xFFFF4600, 0xFF142DCC};
private final float mCircleSpacing;
private long mStartTime = 0;
private boolean mStarted = false;
private final float mTextWidth;
public SmartBallPulseFooter(Context context) {
this(context, null);
}
public SmartBallPulseFooter(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
setMinimumHeight(SmartUtil.dp2px(60));
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mSpinnerStyle = SpinnerStyle.Translate;
mCircleSpacing = SmartUtil.dp2px(2);
mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
mTextWidth = mPaint.measureText(getContext().getString(R.string.common_no_more_data));
}
@Override
protected void dispatchDraw(Canvas canvas) {
final int width = getWidth();
final int height = getHeight();
if (mNoMoreData) {
mPaint.setColor(0xFF898989);
canvas.drawText(getContext().getString(R.string.common_no_more_data),(width - mTextWidth) / 2,(height - mPaint.getTextSize()) / 2, mPaint);
} else{
float radius = (Math.min(width, height) - mCircleSpacing * 2) / 7;
float x = width / 2f - (radius * 2 + mCircleSpacing);
float y = height / 2f;
final long now = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
long time = now - mStartTime - 120 * (i + 1);
float percent = time > 0 ? ((time % 750) / 750f) : 0;
percent = mInterpolator.getInterpolation(percent);
canvas.save();
float translateX = x + (radius * 2) * i + mCircleSpacing * i;
if (percent < 0.5) {
float scale = 1 - percent * 2 * 0.7f;
float translateY = y - scale * 10;
canvas.translate(translateX, translateY);
} else {
float scale = percent * 2 * 0.7f - 0.4f;
float translateY = y + scale * 10;
canvas.translate(translateX, translateY);
}
mPaint.setColor(mAnimatingColor[i % mAnimatingColor.length]);
canvas.drawCircle(0, 0, radius / 3, mPaint);
canvas.restore();
}
}
if (mStarted) {
postInvalidate();
}
}
@Override
public void onStartAnimator(@NonNull RefreshLayout layout, int height, int maxDragHeight) {
if (mStarted) {
return;
}
invalidate();
mStarted = true;
mStartTime = System.currentTimeMillis();
}
@Override
public int onFinish(@NonNull RefreshLayout layout, boolean success) {
mStarted = false;
mStartTime = 0;
mPaint.setColor(mNormalColor);
return 0;
}
@Override
public void setPrimaryColors(@ColorInt int... colors) {
if (!mManualAnimationColor && colors.length > 1) {
setAnimatingColor(colors[0]);
mManualAnimationColor = false;
}
if (!mManualNormalColor) {
if (colors.length > 1) {
setNormalColor(colors[1]);
} else if (colors.length > 0) {
setNormalColor(ColorUtils.compositeColors(0x99FFFFFF, colors[0]));
}
mManualNormalColor = false;
}
}
@Override
public boolean setNoMoreData(boolean noMoreData) {
mNoMoreData = noMoreData;
return true;
}
public SmartBallPulseFooter setSpinnerStyle(SpinnerStyle style) {
mSpinnerStyle = style;
return this;
}
public SmartBallPulseFooter setNormalColor(@ColorInt int color) {
mNormalColor = color;
mManualNormalColor = true;
if (!mStarted) {
mPaint.setColor(color);
}
return this;
}
public SmartBallPulseFooter setAnimatingColor(@ColorInt int color) {
mAnimatingColor = new int[]{color};
mManualAnimationColor = true;
if (mStarted) {
mPaint.setColor(color);
}
return this;
}
}

View File

@ -0,0 +1,42 @@
package com.hjq.demo.other;
import android.widget.Toast;
import com.hjq.demo.action.ToastAction;
import com.hjq.toast.IToastInterceptor;
import com.hjq.toast.ToastUtils;
import timber.log.Timber;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/11/04
* desc : 自定义 Toast 拦截器用于追踪 Toast 调用的位置
*/
public final class ToastInterceptor implements IToastInterceptor {
@Override
public boolean intercept(Toast toast, CharSequence text) {
if (AppConfig.isLogEnable()) {
// 获取调用的堆栈信息
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
// 跳过最前面两个堆栈
for (int i = 2; stackTrace.length > 2 && i < stackTrace.length; i++) {
// 获取代码行数
int lineNumber = stackTrace[i].getLineNumber();
// 获取类的全路径
String className = stackTrace[i].getClassName();
if (lineNumber <= 0 || className.startsWith(ToastUtils.class.getName()) ||
className.startsWith(ToastAction.class.getName())) {
continue;
}
Timber.tag("ToastUtils");
Timber.i("(" + stackTrace[i].getFileName() + ":" + lineNumber + ") " + text.toString());
break;
}
}
return false;
}
}

View File

@ -1,7 +1,7 @@
package com.hjq.demo.ui.activity;
import com.hjq.demo.R;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
/**
* author : Android 轮子哥
@ -9,7 +9,7 @@ import com.hjq.demo.common.MyActivity;
* time : 2018/10/18
* desc : 关于界面
*/
public final class AboutActivity extends MyActivity {
public final class AboutActivity extends AppActivity {
@Override
protected int getLayoutId() {

View File

@ -1,8 +1,10 @@
package com.hjq.demo.ui.activity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
@ -15,13 +17,13 @@ import com.hjq.demo.R;
import com.hjq.demo.action.StatusAction;
import com.hjq.demo.aop.CheckNet;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.widget.BrowserView;
import com.hjq.demo.widget.HintLayout;
import com.scwang.smartrefresh.layout.SmartRefreshLayout;
import com.scwang.smartrefresh.layout.api.RefreshLayout;
import com.scwang.smartrefresh.layout.listener.OnRefreshListener;
import com.hjq.demo.widget.StatusLayout;
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 轮子哥
@ -29,7 +31,7 @@ import com.scwang.smartrefresh.layout.listener.OnRefreshListener;
* time : 2018/10/18
* desc : 浏览器界面
*/
public final class BrowserActivity extends MyActivity
public final class BrowserActivity extends AppActivity
implements StatusAction, OnRefreshListener {
@CheckNet
@ -40,10 +42,13 @@ public final class BrowserActivity extends MyActivity
}
Intent intent = new Intent(context, BrowserActivity.class);
intent.putExtra(IntentKey.URL, url);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
private HintLayout mHintLayout;
private StatusLayout mStatusLayout;
private ProgressBar mProgressBar;
private SmartRefreshLayout mRefreshLayout;
private BrowserView mBrowserView;
@ -55,11 +60,13 @@ public final class BrowserActivity extends MyActivity
@Override
protected void initView() {
mHintLayout = findViewById(R.id.hl_browser_hint);
mStatusLayout = findViewById(R.id.hl_browser_hint);
mProgressBar = findViewById(R.id.pb_browser_progress);
mRefreshLayout = findViewById(R.id.sl_browser_refresh);
mBrowserView = findViewById(R.id.wv_browser_view);
// 设置 WebView 生命管控
mBrowserView.setLifecycleOwner(this);
// 设置网页刷新监听
mRefreshLayout.setOnRefreshListener(this);
}
@ -70,18 +77,16 @@ public final class BrowserActivity extends MyActivity
mBrowserView.setBrowserViewClient(new MyBrowserViewClient());
mBrowserView.setBrowserChromeClient(new MyBrowserChromeClient(mBrowserView));
String url = getString(IntentKey.URL);
mBrowserView.loadUrl(url);
mBrowserView.loadUrl(getString(IntentKey.URL));
}
@Override
public HintLayout getHintLayout() {
return mHintLayout;
public StatusLayout getStatusLayout() {
return mStatusLayout;
}
@Override
public void onLeftClick(View v) {
public void onLeftClick(View view) {
finish();
}
@ -95,24 +100,6 @@ public final class BrowserActivity extends MyActivity
return super.onKeyDown(keyCode, event);
}
@Override
protected void onResume() {
mBrowserView.onResume();
super.onResume();
}
@Override
protected void onPause() {
mBrowserView.onPause();
super.onPause();
}
@Override
protected void onDestroy() {
mBrowserView.onDestroy();
super.onDestroy();
}
/**
* 重新加载当前页
*/
@ -127,7 +114,7 @@ public final class BrowserActivity extends MyActivity
@Override
public void onRefresh(@NonNull RefreshLayout refreshLayout) {
mBrowserView.reload();
reload();
}
private class MyBrowserViewClient extends BrowserView.BrowserViewClient {
@ -176,6 +163,13 @@ public final class BrowserActivity extends MyActivity
}
}
@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
if (icon != null) {
setRightIcon(new BitmapDrawable(getResources(), icon));
}
}
/**
* 收到加载进度变化
*/

View File

@ -7,21 +7,19 @@ import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import com.hjq.base.BaseActivity;
import com.hjq.demo.R;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.Permissions;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.other.AppConfig;
import com.hjq.demo.other.IntentKey;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@ -30,18 +28,16 @@ import java.util.Locale;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/12/18
* desc : 照选择摄图片视频
* desc : 摄图片视频
*/
public final class CameraActivity extends MyActivity {
private static final int CAMERA_REQUEST_CODE = 1024;
public final class CameraActivity extends AppActivity {
public static void start(BaseActivity activity, OnCameraListener listener) {
start(activity, false, listener);
}
@DebugLog
@Permissions({Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE, Permission.CAMERA})
@Permissions({Permission.MANAGE_EXTERNAL_STORAGE, Permission.CAMERA})
public static void start(BaseActivity activity, boolean video, OnCameraListener listener) {
File file = createCameraFile(video);
Intent intent = new Intent(activity, CameraActivity.class);
@ -53,16 +49,14 @@ public final class CameraActivity extends MyActivity {
return;
}
if (resultCode == RESULT_OK) {
if (resultCode == RESULT_OK && file.isFile()) {
listener.onSelected(file);
} else {
listener.onCancel();
return;
}
listener.onCancel();
});
}
private File mFile;
@Override
protected int getLayoutId() {
return 0;
@ -84,59 +78,45 @@ public final class CameraActivity extends MyActivity {
// 拍摄照片
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
}
if (XXPermissions.hasPermission(this, Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE, Permission.CAMERA)
if (XXPermissions.isGrantedPermission(this, new String[]{Permission.MANAGE_EXTERNAL_STORAGE, Permission.CAMERA})
&& intent.resolveActivity(getPackageManager()) != null) {
mFile = getSerializable(IntentKey.FILE);
if (mFile != null && mFile.exists()) {
Uri imageUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 通过 FileProvider 创建一个 Content 类型的 Uri 文件
imageUri = FileProvider.getUriForFile(this, AppConfig.getPackageName() + ".provider", mFile);
} else {
imageUri = Uri.fromFile(mFile);
}
// 对目标应用临时授权该 Uri 所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 将拍取的照片保存到指定 Uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, CAMERA_REQUEST_CODE);
} else {
File file = getSerializable(IntentKey.FILE);
if (file == null) {
toast(R.string.camera_image_error);
setResult(RESULT_CANCELED);
finish();
return;
}
Uri imageUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 通过 FileProvider 创建一个 Content 类型的 Uri 文件
imageUri = FileProvider.getUriForFile(this, AppConfig.getPackageName() + ".provider", file);
} else {
imageUri = Uri.fromFile(file);
}
// 对目标应用临时授权该 Uri 所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 将拍取的照片保存到指定 Uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, (resultCode, data) -> {
if (resultCode == RESULT_OK) {
// 通知系统多媒体扫描该文件否则会导致拍摄出来的图片或者视频没有及时显示到相册中而需要通过重启手机才能看到
MediaScannerConnection.scanFile(getApplicationContext(), new String[]{file.getPath()}, null, null);
}
setResult(resultCode);
finish();
});
} else {
toast(R.string.camera_launch_fail);
finish();
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CAMERA_REQUEST_CODE) {
switch (resultCode) {
case RESULT_OK:
// 重新扫描多媒体否则可能扫描不到
MediaScannerConnection.scanFile(getApplicationContext(), new String[]{mFile.getPath()}, null,null);
break;
case RESULT_CANCELED:
// 删除这个文件
mFile.delete();
break;
default:
break;
}
setResult(resultCode);
finish();
}
}
/**
* 创建一个拍照图片文件对象
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private static File createCameraFile(boolean video) {
File folder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera");
if (!folder.exists() || !folder.isDirectory()) {
@ -145,14 +125,9 @@ public final class CameraActivity extends MyActivity {
}
}
try {
File file = new File(folder, (video ? "IMG_" : "VID") + new SimpleDateFormat("_yyyyMMdd_HHmmss.", Locale.getDefault()).format(new Date()) + (video ? "mp4" : "jpg"));
file.createNewFile();
return file;
} catch (IOException e) {
e.printStackTrace();
return null;
}
return new File(folder, (video ? "VID" : "IMG") + "_" +
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()) +
(video ? ".mp4" : ".jpg"));
}
/**

View File

@ -1,7 +1,7 @@
package com.hjq.demo.ui.activity;
import com.hjq.demo.R;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
/**
* author : Android 轮子哥
@ -9,7 +9,7 @@ import com.hjq.demo.common.MyActivity;
* time : 2018/10/18
* desc : 可进行拷贝的副本
*/
public final class CopyActivity extends MyActivity {
public final class CopyActivity extends AppActivity {
@Override
protected int getLayoutId() {

View File

@ -2,19 +2,18 @@ package com.hjq.demo.ui.activity;
import android.Manifest;
import android.app.Application;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.UnderlineSpan;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.core.view.GravityCompat;
@ -22,9 +21,9 @@ import androidx.drawerlayout.widget.DrawerLayout;
import com.gyf.immersionbar.ImmersionBar;
import com.hjq.demo.R;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.manager.ThreadPoolManager;
import com.hjq.demo.other.AppConfig;
import com.hjq.demo.other.IntentKey;
import com.hjq.permissions.Permission;
@ -48,14 +47,11 @@ import java.util.regex.Pattern;
* time : 2019/06/27
* desc : 崩溃捕捉界面
*/
public final class CrashActivity extends MyActivity {
public final class CrashActivity extends AppActivity {
/** 报错代码行数正则表达式 */
private static final Pattern CODE_REGEX = Pattern.compile("\\(\\w+\\.\\w+:\\d+\\)");
/** 显示的时间格式 */
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
@DebugLog
public static void start(Application application, Throwable throwable) {
if (throwable == null) {
return;
@ -94,44 +90,38 @@ public final class CrashActivity extends MyActivity {
@Override
protected void initData() {
Throwable throwable = getSerializable(IntentKey.OTHER);
if (throwable instanceof NullPointerException) {
mTitleView.setText("空指针异常");
} else if (throwable instanceof ClassCastException) {
mTitleView.setText("类型转换异常");
} else if (throwable instanceof ActivityNotFoundException) {
mTitleView.setText("活动跳转异常");
} else if (throwable instanceof IllegalArgumentException) {
mTitleView.setText("非法参数异常");
} else if (throwable instanceof IllegalStateException) {
mTitleView.setText("非法状态异常");
} else if (throwable instanceof WindowManager.BadTokenException) {
mTitleView.setText("窗口添加异常");
} else if (throwable instanceof StackOverflowError) {
mTitleView.setText("栈溢出");
} else if (throwable instanceof OutOfMemoryError) {
mTitleView.setText("内存溢出");
if (throwable == null) {
return;
}
mTitleView.setText(throwable.getClass().getSimpleName());
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
throwable.printStackTrace(printWriter);
Throwable cause = throwable.getCause();
if (cause != null) {
cause.printStackTrace(printWriter);
}
mStackTrace = stringWriter.toString();
Matcher matcher = CODE_REGEX.matcher(mStackTrace);
SpannableString spannable = new SpannableString(mStackTrace);
for (int index = 0; matcher.find(); index++) {
// 不包含左括号
int start = matcher.start() + "(".length();
// 不包含右括号
int end = matcher.end() - ")".length();
// 设置前景
spannable.setSpan(new ForegroundColorSpan(index < 3 ? 0xFF287BDE : 0xFF999999), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置下划线
spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableStringBuilder spannable = new SpannableStringBuilder(mStackTrace);
if (spannable.length() > 0) {
for (int index = 0; matcher.find(); index++) {
// 不包含左括号
int start = matcher.start() + "(".length();
// 不包含右括号
int end = matcher.end() - ")".length();
// 设置前景
spannable.setSpan(new ForegroundColorSpan(index < 3 ? 0xFF287BDE : 0xFF999999), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置下划线
spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mMessageView.setText(spannable);
}
mMessageView.setText(spannable);
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
Resources res = getResources();
DisplayMetrics displayMetrics = res.getDisplayMetrics();
int screenWidth = displayMetrics.widthPixels;
int screenHeight = displayMetrics.heightPixels;
@ -160,44 +150,33 @@ public final class CrashActivity extends MyActivity {
.append("\n目标资源\t").append(targetResource);
builder.append("\n安卓版本\t").append(Build.VERSION.RELEASE)
.append("\nSDK\t版本:\t").append(Build.VERSION.SDK_INT)
.append("\nCPU\t架构:\t").append(Build.SUPPORTED_ABIS[0]);
.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 {
SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
builder.append("\n首次安装\t").append(DATE_FORMAT.format(new Date(packageInfo.firstInstallTime)))
.append("\n最近安装\t").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime)))
.append("\n崩溃时间\t").append(DATE_FORMAT.format(new Date()));
builder.append("\n首次安装\t").append(dateFormat.format(new Date(packageInfo.firstInstallTime)))
.append("\n最近安装\t").append(dateFormat.format(new Date(packageInfo.lastUpdateTime)))
.append("\n崩溃时间\t").append(dateFormat.format(new Date()));
List<String> permissions = Arrays.asList(packageInfo.requestedPermissions);
if (permissions.contains(Permission.READ_EXTERNAL_STORAGE) || permissions.contains(Permission.WRITE_EXTERNAL_STORAGE)) {
builder.append("\n存储权限\t");
if (XXPermissions.hasPermission(this, Permission.Group.STORAGE)) {
builder.append("读、写");
} else {
if (XXPermissions.hasPermission(this, Permission.READ_EXTERNAL_STORAGE)) {
builder.append("");
} else if (XXPermissions.hasPermission(this, Permission.WRITE_EXTERNAL_STORAGE)) {
builder.append("");
} else {
builder.append("未获得");
}
}
if (permissions.contains(Permission.MANAGE_EXTERNAL_STORAGE)) {
builder.append("\n存储权限\t").append(XXPermissions.isGrantedPermission(this, Permission.MANAGE_EXTERNAL_STORAGE) ? "已获得" : "未获得");
}
if (permissions.contains(Permission.ACCESS_FINE_LOCATION) || permissions.contains(Permission.ACCESS_COARSE_LOCATION)) {
builder.append("\n定位权限\t");
if (XXPermissions.hasPermission(this, Permission.Group.LOCATION)) {
if (XXPermissions.isGrantedPermission(this, Permission.Group.LOCATION)) {
builder.append("精确、粗略");
} else {
if (XXPermissions.hasPermission(this, Permission.ACCESS_FINE_LOCATION)) {
if (XXPermissions.isGrantedPermission(this, Permission.ACCESS_FINE_LOCATION)) {
builder.append("精确");
} else if (XXPermissions.hasPermission(this, Permission.ACCESS_COARSE_LOCATION)) {
} else if (XXPermissions.isGrantedPermission(this, Permission.ACCESS_COARSE_LOCATION)) {
builder.append("粗略");
} else {
builder.append("未获得");
@ -206,25 +185,25 @@ public final class CrashActivity extends MyActivity {
}
if (permissions.contains(Permission.CAMERA)) {
builder.append("\n相机权限\t").append(XXPermissions.hasPermission(this, Permission.CAMERA) ? "已获得" : "未获得");
builder.append("\n相机权限\t").append(XXPermissions.isGrantedPermission(this, Permission.CAMERA) ? "已获得" : "未获得");
}
if (permissions.contains(Permission.RECORD_AUDIO)) {
builder.append("\n录音权限\t").append(XXPermissions.hasPermission(this, Permission.RECORD_AUDIO) ? "已获得" : "未获得");
builder.append("\n录音权限\t").append(XXPermissions.isGrantedPermission(this, Permission.RECORD_AUDIO) ? "已获得" : "未获得");
}
if (permissions.contains(Permission.SYSTEM_ALERT_WINDOW)) {
builder.append("\n悬浮窗权限\t").append(XXPermissions.hasPermission(this, Permission.SYSTEM_ALERT_WINDOW) ? "已获得" : "未获得");
builder.append("\n悬浮窗权限\t").append(XXPermissions.isGrantedPermission(this, Permission.SYSTEM_ALERT_WINDOW) ? "已获得" : "未获得");
}
if (permissions.contains(Permission.REQUEST_INSTALL_PACKAGES)) {
builder.append("\n安装包权限\t").append(XXPermissions.hasPermission(this, Permission.REQUEST_INSTALL_PACKAGES) ? "已获得" : "未获得");
builder.append("\n安装包权限\t").append(XXPermissions.isGrantedPermission(this, Permission.REQUEST_INSTALL_PACKAGES) ? "已获得" : "未获得");
}
if (permissions.contains(Manifest.permission.INTERNET)) {
builder.append("\n当前网络访问\t");
new Thread(() -> {
ThreadPoolManager.getInstance().execute(() -> {
try {
InetAddress.getByName("www.baidu.com");
builder.append("正常");
@ -232,7 +211,7 @@ public final class CrashActivity extends MyActivity {
builder.append("异常");
}
post(() -> mInfoView.setText(builder));
}).start();
});
} else {
mInfoView.setText(builder);
@ -243,25 +222,20 @@ public final class CrashActivity extends MyActivity {
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.iv_crash_info:
mDrawerLayout.openDrawer(GravityCompat.START);
break;
case R.id.iv_crash_share:
// 分享文本
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, mStackTrace);
startActivity(Intent.createChooser(intent, ""));
break;
case R.id.iv_crash_restart:
// 重启应用
startActivity(HomeActivity.class);
finish();
break;
default:
break;
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.iv_crash_info) {
mDrawerLayout.openDrawer(GravityCompat.START);
} else if (viewId == R.id.iv_crash_share) {
// 分享文本
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, mStackTrace);
startActivity(Intent.createChooser(intent, ""));
} else if (viewId == R.id.iv_crash_restart) {
// 重启应用
RestartActivity.restart(this);
finish();
}
}

View File

@ -10,7 +10,8 @@ import androidx.annotation.Nullable;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.manager.DialogManager;
import com.hjq.demo.ui.dialog.AddressDialog;
import com.hjq.demo.ui.dialog.DateDialog;
import com.hjq.demo.ui.dialog.HintDialog;
@ -41,7 +42,10 @@ import java.util.List;
* time : 2018/12/02
* desc : 对话框使用案例
*/
public final class DialogActivity extends MyActivity {
public final class DialogActivity extends AppActivity {
/** 等待对话框 */
private BaseDialog mWaitDialog;
@Override
protected int getLayoutId() {
@ -58,7 +62,8 @@ public final class DialogActivity extends MyActivity {
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_safe, R.id.btn_dialog_custom,
R.id.btn_dialog_multi);
}
@Override
@ -68,401 +73,441 @@ public final class DialogActivity extends MyActivity {
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_dialog_message:
// 消息对话框
new MessageDialog.Builder(this)
// 标题可以不用填写
.setTitle("我是标题")
// 内容必须要填写
.setMessage("我是内容")
// 确定按钮文本
.setConfirm(getString(R.string.common_confirm))
// 设置 null 表示不显示取消按钮
.setCancel(getString(R.string.common_cancel))
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setListener(new MessageDialog.OnListener() {
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.btn_dialog_message) {
@Override
public void onConfirm(BaseDialog dialog) {
toast("确定了");
}
// 消息对话框
new MessageDialog.Builder(getActivity())
// 标题可以不用填写
.setTitle("我是标题")
// 内容必须要填写
.setMessage("我是内容")
// 确定按钮文本
.setConfirm(getString(R.string.common_confirm))
// 设置 null 表示不显示取消按钮
.setCancel(getString(R.string.common_cancel))
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setListener(new MessageDialog.OnListener() {
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_input:
// 输入对话框
new InputDialog.Builder(this)
// 标题可以不用填写
.setTitle("我是标题")
// 内容可以不用填写
.setContent("我是内容")
// 提示可以不用填写
.setHint("我是提示")
// 确定按钮文本
.setConfirm(getString(R.string.common_confirm))
// 设置 null 表示不显示取消按钮
.setCancel(getString(R.string.common_cancel))
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setListener(new InputDialog.OnListener() {
@Override
public void onConfirm(BaseDialog dialog) {
toast("确定了");
}
@Override
public void onConfirm(BaseDialog dialog, String content) {
toast("确定了:" + content);
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_bottom_menu:
List<String> data = new ArrayList<>();
for (int i = 0; i < 10; i++) {
data.add("我是数据" + (i + 1));
}
// 底部选择框
new MenuDialog.Builder(this)
// 设置 null 表示不显示取消按钮
//.setCancel(getString(R.string.common_cancel))
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setList(data)
.setListener(new MenuDialog.OnListener<String>() {
} else if (viewId == R.id.btn_dialog_input) {
@Override
public void onSelected(BaseDialog dialog, int position, String string) {
toast("位置:" + position + ",文本:" + string);
}
// 输入对话框
new InputDialog.Builder(this)
// 标题可以不用填写
.setTitle("我是标题")
// 内容可以不用填写
.setContent("我是内容")
// 提示可以不用填写
.setHint("我是提示")
// 确定按钮文本
.setConfirm(getString(R.string.common_confirm))
// 设置 null 表示不显示取消按钮
.setCancel(getString(R.string.common_cancel))
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setListener(new InputDialog.OnListener() {
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_center_menu:
List<String> data1 = new ArrayList<>();
for (int i = 0; i < 10; i++) {
data1.add("我是数据" + (i + 1));
}
// 居中选择框
new MenuDialog.Builder(this)
.setGravity(Gravity.CENTER)
// 设置 null 表示不显示取消按钮
//.setCancel(null)
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setList(data1)
.setListener(new MenuDialog.OnListener<String>() {
@Override
public void onConfirm(BaseDialog dialog, String content) {
toast("确定了:" + content);
}
@Override
public void onSelected(BaseDialog dialog, int position, String string) {
toast("位置:" + position + ",文本:" + string);
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_single_select:
// 单选对话框
new SelectDialog.Builder(this)
.setTitle("请选择你的性别")
.setList("", "")
// 设置单选模式
.setSingleSelect()
// 设置默认选中
.setSelect(0)
.setListener(new SelectDialog.OnListener<String>() {
} else if (viewId == R.id.btn_dialog_bottom_menu) {
@Override
public void onSelected(BaseDialog dialog, HashMap<Integer, String> data) {
toast("确定了:" + data.toString());
}
List<String> data = new ArrayList<>();
for (int i = 0; i < 10; i++) {
data.add("我是数据" + (i + 1));
}
// 底部选择框
new MenuDialog.Builder(this)
// 设置 null 表示不显示取消按钮
//.setCancel(getString(R.string.common_cancel))
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setList(data)
.setListener(new MenuDialog.OnListener<String>() {
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_more_select:
// 多选对话框
new SelectDialog.Builder(this)
.setTitle("请选择工作日")
.setList("星期一", "星期二", "星期三", "星期四", "星期五")
// 设置最大选择数
.setMaxSelect(3)
// 设置默认选中
.setSelect(2, 3, 4)
.setListener(new SelectDialog.OnListener<String>() {
@Override
public void onSelected(BaseDialog dialog, int position, String string) {
toast("位置:" + position + ",文本:" + string);
}
@Override
public void onSelected(BaseDialog dialog, HashMap<Integer, String> data) {
toast("确定了:" + data.toString());
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_succeed_toast:
// 成功对话框
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_FINISH)
.setMessage("完成")
.show();
break;
case R.id.btn_dialog_fail_toast:
// 失败对话框
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_ERROR)
.setMessage("错误")
.show();
break;
case R.id.btn_dialog_warn_toast:
// 警告对话框
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_WARNING)
.setMessage("警告")
.show();
break;
case R.id.btn_dialog_wait:
// 等待对话框
final BaseDialog waitDialog = new WaitDialog.Builder(this)
} else if (viewId == R.id.btn_dialog_center_menu) {
List<String> data = new ArrayList<>();
for (int i = 0; i < 10; i++) {
data.add("我是数据" + (i + 1));
}
// 居中选择框
new MenuDialog.Builder(this)
.setGravity(Gravity.CENTER)
// 设置 null 表示不显示取消按钮
//.setCancel(null)
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setList(data)
.setListener(new MenuDialog.OnListener<String>() {
@Override
public void onSelected(BaseDialog dialog, int position, String string) {
toast("位置:" + position + ",文本:" + string);
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
} else if (viewId == R.id.btn_dialog_single_select) {
// 单选对话框
new SelectDialog.Builder(this)
.setTitle("请选择你的性别")
.setList("", "")
// 设置单选模式
.setSingleSelect()
// 设置默认选中
.setSelect(0)
.setListener(new SelectDialog.OnListener<String>() {
@Override
public void onSelected(BaseDialog dialog, HashMap<Integer, String> data) {
toast("确定了:" + data.toString());
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
} else if (viewId == R.id.btn_dialog_more_select) {
// 多选对话框
new SelectDialog.Builder(this)
.setTitle("请选择工作日")
.setList("星期一", "星期二", "星期三", "星期四", "星期五")
// 设置最大选择数
.setMaxSelect(3)
// 设置默认选中
.setSelect(2, 3, 4)
.setListener(new SelectDialog.OnListener<String>() {
@Override
public void onSelected(BaseDialog dialog, HashMap<Integer, String> data) {
toast("确定了:" + data.toString());
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
} else if (viewId == R.id.btn_dialog_succeed_toast) {
// 成功对话框
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_FINISH)
.setMessage("完成")
.show();
} else if (viewId == R.id.btn_dialog_fail_toast) {
// 失败对话框
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_ERROR)
.setMessage("错误")
.show();
} else if (viewId == R.id.btn_dialog_warn_toast) {
// 警告对话框
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_WARNING)
.setMessage("警告")
.show();
} else if (viewId == R.id.btn_dialog_wait) {
if (mWaitDialog == null) {
mWaitDialog = new WaitDialog.Builder(this)
// 消息文本可以不用填写
.setMessage(getString(R.string.common_loading))
.show();
postDelayed(waitDialog::dismiss, 2000);
break;
case R.id.btn_dialog_pay:
// 支付密码输入对话框
new PayPasswordDialog.Builder(this)
.setTitle(getString(R.string.pay_title))
.setSubTitle("用于购买一个女盆友")
.setMoney("¥ 100.00")
//.setAutoDismiss(false) // 设置点击按钮后不关闭对话框
.setListener(new PayPasswordDialog.OnListener() {
.create();
}
if (!mWaitDialog.isShowing()) {
mWaitDialog.show();
postDelayed(mWaitDialog::dismiss, 2000);
}
@Override
public void onCompleted(BaseDialog dialog, String password) {
toast(password);
}
} else if (viewId == R.id.btn_dialog_pay) {
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_address:
// 选择地区对话框
new AddressDialog.Builder(this)
.setTitle(getString(R.string.address_title))
// 设置默认省份
//.setProvince("广东省")
// 设置默认城市必须要先设置默认省份
//.setCity("广州市")
// 不选择县级区域
//.setIgnoreArea()
.setListener(new AddressDialog.OnListener() {
// 支付密码输入对话框
new PayPasswordDialog.Builder(this)
.setTitle(getString(R.string.pay_title))
.setSubTitle("用于购买一个女盆友")
.setMoney("¥ 100.00")
//.setAutoDismiss(false) // 设置点击按钮后不关闭对话框
.setListener(new PayPasswordDialog.OnListener() {
@Override
public void onSelected(BaseDialog dialog, String province, String city, String area) {
toast(province + city + area);
}
@Override
public void onCompleted(BaseDialog dialog, String password) {
toast(password);
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_date:
// 日期选择对话框
new 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(new DateDialog.OnListener() {
@Override
public void onSelected(BaseDialog dialog, int year, int month, int day) {
toast(year + getString(R.string.common_year) + month + getString(R.string.common_month) + day + getString(R.string.common_day));
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
// 如果不指定时分秒则默认为现在的时间
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.getTimeInMillis());
//toast(new SimpleDateFormat("yyyy年MM月dd日 kk:mm:ss").format(calendar.getTime()));
}
} else if (viewId == R.id.btn_dialog_address) {
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_time:
// 时间选择对话框
new 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(new TimeDialog.OnListener() {
// 选择地区对话框
new AddressDialog.Builder(this)
.setTitle(getString(R.string.address_title))
// 设置默认省份
//.setProvince("广东省")
// 设置默认城市必须要先设置默认省份
//.setCity("广州市")
// 不选择县级区域
//.setIgnoreArea()
.setListener(new AddressDialog.OnListener() {
@Override
public void onSelected(BaseDialog dialog, int hour, int minute, int second) {
toast(hour + getString(R.string.common_hour) + minute + getString(R.string.common_minute) + second + getString(R.string.common_second));
@Override
public void onSelected(BaseDialog dialog, String province, String city, String area) {
toast(province + city + area);
}
// 如果不指定年月日则默认为今天的日期
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
toast("时间戳:" + calendar.getTimeInMillis());
//toast(new SimpleDateFormat("yyyy年MM月dd日 kk:mm:ss").format(calendar.getTime()));
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_share:
toast("记得改好第三方 AppID 和 AppKey否则会调不起来哦");
toast("也别忘了改微信 " + WXEntryActivity.class.getSimpleName() + " 类所在的包名哦");
// 分享对话框
new ShareDialog.Builder(this)
// 分享标题
.setShareTitle("Github")
// 分享描述
.setShareDescription("AndroidProject")
// 分享缩略图
.setShareLogo(R.mipmap.launcher_ic)
// 分享链接
.setShareUrl("https://github.com/getActivity/AndroidProject")
.setListener(new UmengShare.OnShareListener() {
} else if (viewId == R.id.btn_dialog_date) {
@Override
public void onSucceed(Platform platform) {
toast("分享成功");
}
// 日期选择对话框
new 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(new DateDialog.OnListener() {
@Override
public void onSelected(BaseDialog dialog, int year, int month, int day) {
toast(year + getString(R.string.common_year) + month + getString(R.string.common_month) + day + getString(R.string.common_day));
@Override
public void onError(Platform platform, Throwable t) {
toast("分享出错");
}
// 如果不指定时分秒则默认为现在的时间
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.getTimeInMillis());
//toast(new SimpleDateFormat("yyyy年MM月dd日 kk:mm:ss").format(calendar.getTime()));
}
@Override
public void onCancel(Platform platform) {
toast("分享取消");
}
})
.show();
break;
case R.id.btn_dialog_update:
// 升级对话框
new UpdateDialog.Builder(this)
// 版本名
.setVersionName("5.2.0")
// 是否强制更新
.setForceUpdate(false)
// 更新日志
.setUpdateLog("到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥")
// 下载 URL
.setDownloadUrl("https://dldir1.qq.com/weixin/android/weixin7014android1660.apk")
// 文件 MD5
.setFileMd5("6ec99cb762ffd9158e8b27dc33d9680d")
.show();
break;
case R.id.btn_dialog_safe:
new SafeDialog.Builder(this)
.setListener(new SafeDialog.OnListener() {
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
@Override
public void onConfirm(BaseDialog dialog, String phone, String code) {
toast("手机号:" + phone + "\n验证码" + code);
}
} else if (viewId == R.id.btn_dialog_time) {
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
break;
case R.id.btn_dialog_custom:
// 自定义对话框
new BaseDialog.Builder(this)
.setContentView(R.layout.custom_dialog)
.setAnimStyle(BaseDialog.ANIM_SCALE)
//.setText(id, "我是预设置的文本")
.setOnClickListener(R.id.btn_dialog_custom_ok, (BaseDialog.OnClickListener<Button>) (dialog, view) -> dialog.dismiss())
.addOnShowListener(dialog -> toast("Dialog 显示了"))
.addOnCancelListener(dialog -> toast("Dialog 取消了"))
.addOnDismissListener(dialog -> toast("Dialog 销毁了"))
.setOnKeyListener((dialog, event) -> {
toast("按键代码:" + event.getKeyCode());
return false;
})
.show();
break;
default:
break;
// 时间选择对话框
new 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(new TimeDialog.OnListener() {
@Override
public void onSelected(BaseDialog dialog, int hour, int minute, int second) {
toast(hour + getString(R.string.common_hour) + minute + getString(R.string.common_minute) + second + getString(R.string.common_second));
// 如果不指定年月日则默认为今天的日期
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
toast("时间戳:" + calendar.getTimeInMillis());
//toast(new SimpleDateFormat("yyyy年MM月dd日 kk:mm:ss").format(calendar.getTime()));
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
} else if (viewId == R.id.btn_dialog_share) {
toast("记得改好第三方 AppID 和 AppKey否则会调不起来哦");
toast("也别忘了改微信 " + WXEntryActivity.class.getSimpleName() + " 类所在的包名哦");
// 分享对话框
new ShareDialog.Builder(this)
// 分享标题
.setShareTitle("Github")
// 分享描述
.setShareDescription("AndroidProject")
// 分享缩略图
.setShareLogo(R.mipmap.launcher_ic)
// 分享链接
.setShareUrl("https://github.com/getActivity/AndroidProject")
.setListener(new UmengShare.OnShareListener() {
@Override
public void onSucceed(Platform platform) {
toast("分享成功");
}
@Override
public void onError(Platform platform, Throwable t) {
toast("分享出错");
}
@Override
public void onCancel(Platform platform) {
toast("分享取消");
}
})
.show();
} else if (viewId == R.id.btn_dialog_update) {
// 升级对话框
new UpdateDialog.Builder(this)
// 版本名
.setVersionName("5.2.0")
// 是否强制更新
.setForceUpdate(false)
// 更新日志
.setUpdateLog("到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥")
// 下载 URL
.setDownloadUrl("https://dldir1.qq.com/weixin/android/weixin7014android1660.apk")
// 文件 MD5
.setFileMd5("6ec99cb762ffd9158e8b27dc33d9680d")
.show();
} else if (viewId == R.id.btn_dialog_safe) {
// 身份校验对话框
new SafeDialog.Builder(this)
.setListener(new SafeDialog.OnListener() {
@Override
public void onConfirm(BaseDialog dialog, String phone, String code) {
toast("手机号:" + phone + "\n验证码" + code);
}
@Override
public void onCancel(BaseDialog dialog) {
toast("取消了");
}
})
.show();
} else if (viewId == R.id.btn_dialog_custom) {
// 自定义对话框
new BaseDialog.Builder<>(this)
.setContentView(R.layout.custom_dialog)
.setAnimStyle(BaseDialog.ANIM_SCALE)
//.setText(id, "我是预设置的文本")
.setOnClickListener(R.id.btn_dialog_custom_ok, (BaseDialog.OnClickListener<Button>) (dialog, button) -> dialog.dismiss())
.setOnCreateListener(dialog -> toast("Dialog 创建了"))
.addOnShowListener(dialog -> toast("Dialog 显示了"))
.addOnCancelListener(dialog -> toast("Dialog 取消了"))
.addOnDismissListener(dialog -> toast("Dialog 销毁了"))
.setOnKeyListener((dialog, event) -> {
toast("按键代码:" + event.getKeyCode());
return false;
})
.show();
} else if (viewId == R.id.btn_dialog_multi) {
BaseDialog dialog1 = new MessageDialog.Builder(getActivity())
.setTitle("温馨提示")
.setMessage("我是第一个弹出的对话框")
.setConfirm(getString(R.string.common_confirm))
.setCancel(getString(R.string.common_cancel))
.create();
BaseDialog dialog2 = new MessageDialog.Builder(getActivity())
.setTitle("温馨提示")
.setMessage("我是第二个弹出的对话框")
.setConfirm(getString(R.string.common_confirm))
.setCancel(getString(R.string.common_cancel))
.create();
DialogManager.getInstance(this).addShow(dialog1);
DialogManager.getInstance(this).addShow(dialog2);
}
}
@ -474,13 +519,13 @@ public final class DialogActivity extends MyActivity {
}
@Override
public void onRightClick(View v) {
public void onRightClick(View view) {
// 菜单弹窗
new ListPopup.Builder(this)
.setList("选择拍照", "选取相册")
.addOnShowListener(popupWindow -> toast("PopupWindow 显示了"))
.addOnDismissListener(popupWindow -> toast("PopupWindow 销毁了"))
.setListener((ListPopup.OnListener<String>) (popupWindow, position, s) -> toast("点击了:" + s))
.showAsDropDown(v);
.showAsDropDown(view);
}
}

View File

@ -2,28 +2,28 @@ package com.hjq.demo.ui.activity;
import android.view.View;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager2.widget.ViewPager2;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.ui.pager.GuidePagerAdapter;
import com.rd.PageIndicatorView;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.ui.adapter.GuideAdapter;
import me.relex.circleindicator.CircleIndicator3;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2019/09/21
* desc : APP 引导页
* desc : 应用引导页
*/
public final class GuideActivity extends MyActivity
implements ViewPager.OnPageChangeListener {
public final class GuideActivity extends AppActivity {
private ViewPager mViewPager;
private PageIndicatorView mIndicatorView;
private ViewPager2 mViewPager;
private CircleIndicator3 mIndicatorView;
private View mCompleteView;
private GuidePagerAdapter mPagerAdapter;
private GuideAdapter mAdapter;
@Override
protected int getLayoutId() {
@ -33,52 +33,59 @@ public final class GuideActivity extends MyActivity
@Override
protected void initView() {
mViewPager = findViewById(R.id.vp_guide_pager);
mIndicatorView = findViewById(R.id.pv_guide_indicator);
mIndicatorView = findViewById(R.id.cv_guide_indicator);
mCompleteView = findViewById(R.id.btn_guide_complete);
setOnClickListener(mCompleteView);
mIndicatorView.setViewPager(mViewPager);
}
@Override
protected void initData() {
mPagerAdapter = new GuidePagerAdapter();
mViewPager.setAdapter(mPagerAdapter);
mViewPager.addOnPageChangeListener(this);
}
@SingleClick
@Override
public void onClick(View v) {
if (v == mCompleteView) {
startActivity(HomeActivity.class);
finish();
}
}
/**
* {@link ViewPager.OnPageChangeListener}
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (mViewPager.getCurrentItem() == mPagerAdapter.getCount() - 1 && positionOffsetPixels > 0) {
mCompleteView.setVisibility(View.INVISIBLE);
}
}
@Override
public void onPageSelected(int position) {}
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
mCompleteView.setVisibility(mViewPager.getCurrentItem() == mPagerAdapter.getCount() - 1 ? View.VISIBLE : View.INVISIBLE);
}
mAdapter = new GuideAdapter(this);
mAdapter.addItem(R.drawable.guide_1_bg);
mAdapter.addItem(R.drawable.guide_2_bg);
mAdapter.addItem(R.drawable.guide_3_bg);
mViewPager.setAdapter(mAdapter);
mViewPager.registerOnPageChangeCallback(mCallback);
mIndicatorView.setViewPager(mViewPager);
}
@Override
public boolean isSwipeEnable() {
return false;
}
@SingleClick
@Override
public void onClick(View view) {
if (view == mCompleteView) {
HomeActivity.start(getContext());
finish();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mViewPager.unregisterOnPageChangeCallback(mCallback);
}
private final ViewPager2.OnPageChangeCallback mCallback = new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (mViewPager.getCurrentItem() == mAdapter.getItemCount() - 1 && positionOffsetPixels > 0) {
mIndicatorView.setVisibility(View.VISIBLE);
mCompleteView.setVisibility(View.INVISIBLE);
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager2.SCROLL_STATE_IDLE) {
boolean last = mViewPager.getCurrentItem() == mAdapter.getItemCount() - 1;
mIndicatorView.setVisibility(last ? View.INVISIBLE : View.VISIBLE);
mCompleteView.setVisibility(last ? View.VISIBLE : View.INVISIBLE);
}
}
};
}

View File

@ -1,20 +1,22 @@
package com.hjq.demo.ui.activity;
import android.view.KeyEvent;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.hjq.base.BaseFragmentAdapter;
import com.hjq.base.FragmentPagerAdapter;
import com.hjq.demo.R;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.common.MyFragment;
import com.hjq.demo.helper.ActivityStackManager;
import com.hjq.demo.helper.DoubleClickHelper;
import com.hjq.demo.other.KeyboardWatcher;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.app.AppFragment;
import com.hjq.demo.manager.ActivityManager;
import com.hjq.demo.other.DoubleClickHelper;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.ui.fragment.FindFragment;
import com.hjq.demo.ui.fragment.HomeFragment;
import com.hjq.demo.ui.fragment.MeFragment;
@ -24,16 +26,28 @@ import com.hjq.demo.ui.fragment.MessageFragment;
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 页界面
* desc : 页界面
*/
public final class HomeActivity extends MyActivity
implements KeyboardWatcher.SoftKeyboardStateListener,
BottomNavigationView.OnNavigationItemSelectedListener {
public final class HomeActivity extends AppActivity
implements BottomNavigationView.OnNavigationItemSelectedListener {
private ViewPager mViewPager;
private BottomNavigationView mBottomNavigationView;
private BaseFragmentAdapter<MyFragment> mPagerAdapter;
private FragmentPagerAdapter<AppFragment<?>> mPagerAdapter;
public static void start(Context context) {
start(context, HomeFragment.class);
}
public static void start(Context context, Class<? extends AppFragment<?>> fragmentClass) {
Intent intent = new Intent(context, HomeActivity.class);
intent.putExtra(IntentKey.INDEX, fragmentClass);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
@Override
protected int getLayoutId() {
@ -47,22 +61,53 @@ public final class HomeActivity extends MyActivity
// 不使用图标默认变色
mBottomNavigationView.setItemIconTintList(null);
// 设置导航栏条目点击事件
mBottomNavigationView.setOnNavigationItemSelectedListener(this);
KeyboardWatcher.with(this)
.setListener(this);
// 屏蔽底部导航栏长按文本提示
Menu menu = mBottomNavigationView.getMenu();
for (int i = 0; i < menu.size(); i++) {
mBottomNavigationView.findViewById(menu.getItem(i).getItemId()).setOnLongClickListener(v -> true);
}
}
@Override
protected void initData() {
mPagerAdapter = new BaseFragmentAdapter<>(this);
mPagerAdapter = new FragmentPagerAdapter<>(this);
mPagerAdapter.addFragment(HomeFragment.newInstance());
mPagerAdapter.addFragment(FindFragment.newInstance());
mPagerAdapter.addFragment(MessageFragment.newInstance());
mPagerAdapter.addFragment(MeFragment.newInstance());
// 设置成懒加载模式
mPagerAdapter.setLazyMode(true);
mViewPager.setAdapter(mPagerAdapter);
onNewIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
int fragmentIndex = mPagerAdapter.getFragmentIndex(getSerializable(IntentKey.INDEX));
if (fragmentIndex == -1) {
return;
}
mViewPager.setCurrentItem(fragmentIndex);
switch (fragmentIndex) {
case 0:
mBottomNavigationView.setSelectedItemId(R.id.menu_home);
break;
case 1:
mBottomNavigationView.setSelectedItemId(R.id.home_found);
break;
case 2:
mBottomNavigationView.setSelectedItemId(R.id.home_message);
break;
case 3:
mBottomNavigationView.setSelectedItemId(R.id.home_me);
break;
default:
break;
}
}
/**
@ -71,70 +116,45 @@ public final class HomeActivity extends MyActivity
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_home:
mViewPager.setCurrentItem(0);
return true;
case R.id.home_found:
mViewPager.setCurrentItem(1);
return true;
case R.id.home_message:
mViewPager.setCurrentItem(2);
return true;
case R.id.home_me:
mViewPager.setCurrentItem(3);
return true;
default:
break;
int itemId = item.getItemId();
if (itemId == R.id.menu_home) {
mViewPager.setCurrentItem(0);
return true;
} else if (itemId == R.id.home_found) {
mViewPager.setCurrentItem(1);
return true;
} else if (itemId == R.id.home_message) {
mViewPager.setCurrentItem(2);
return true;
} else if (itemId == R.id.home_me) {
mViewPager.setCurrentItem(3);
return true;
}
return false;
}
/**
* {@link KeyboardWatcher.SoftKeyboardStateListener}
*/
@Override
public void onSoftKeyboardOpened(int keyboardHeight) {
mBottomNavigationView.setVisibility(View.GONE);
}
@Override
public void onSoftKeyboardClosed() {
mBottomNavigationView.setVisibility(View.VISIBLE);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 回调当前 Fragment onKeyDown 方法
if (mPagerAdapter.getShowFragment().onKeyDown(keyCode, event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onBackPressed() {
if (DoubleClickHelper.isOnDoubleClick()) {
// 移动到上一个任务栈避免侧滑引起的不良反应
moveTaskToBack(false);
postDelayed(() -> {
// 进行内存优化销毁掉所有的界面
ActivityStackManager.getInstance().finishAllActivities();
// 销毁进程注意调用此 API 可能导致当前 Activity onDestroy 方法无法正常回调
// System.exit(0);
}, 300);
} else {
if (!DoubleClickHelper.isOnDoubleClick()) {
toast(R.string.home_exit_hint);
return;
}
// 移动到上一个任务栈避免侧滑引起的不良反应
moveTaskToBack(false);
postDelayed(() -> {
// 进行内存优化销毁掉所有的界面
ActivityManager.getInstance().finishAllActivities();
// 销毁进程注意调用此 API 可能导致当前 Activity onDestroy 方法无法正常回调
// System.exit(0);
}, 300);
}
@Override
protected void onDestroy() {
super.onDestroy();
mViewPager.setAdapter(null);
mBottomNavigationView.setOnNavigationItemSelectedListener(null);
super.onDestroy();
}
@Override

View File

@ -1,22 +1,32 @@
package com.hjq.demo.ui.activity;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import com.gyf.immersionbar.BarHide;
import com.gyf.immersionbar.ImmersionBar;
import com.hjq.base.BaseAdapter;
import com.hjq.base.RecyclerPagerAdapter;
import com.hjq.demo.R;
import com.hjq.demo.aop.CheckNet;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.ui.pager.ImagePagerAdapter;
import com.rd.PageIndicatorView;
import com.hjq.demo.ui.adapter.ImagePreviewAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.relex.circleindicator.CircleIndicator;
/**
* author : Android 轮子哥
@ -24,7 +34,9 @@ import java.util.ArrayList;
* time : 2019/03/05
* desc : 查看大图
*/
public final class ImagePreviewActivity extends MyActivity {
public final class ImagePreviewActivity extends AppActivity
implements ViewPager.OnPageChangeListener,
BaseAdapter.OnItemClickListener {
public static void start(Context context, String url) {
ArrayList<String> images = new ArrayList<>(1);
@ -32,21 +44,44 @@ public final class ImagePreviewActivity extends MyActivity {
start(context, images);
}
public static void start(Context context, ArrayList<String> urls) {
public static void start(Context context, List<String> urls) {
start(context, urls, 0);
}
@CheckNet
@DebugLog
public static void start(Context context, ArrayList<String> urls, int index) {
public static void start(Context context, List<String> urls, int index) {
if (urls == null || urls.isEmpty()) {
return;
}
Intent intent = new Intent(context, ImagePreviewActivity.class);
intent.putExtra(IntentKey.IMAGE, urls);
if (urls.size() > 2500) {
// 请注意如果传输的数据量过大会抛出此异常并且这种异常是不能被捕获的
// 所以当图片数量过多的时候我们应当只显示一张这种一般是手机图片过多导致的
// 经过测试传入 3121 张图片集合的时候会抛出此异常所以保险值应当是 2500
// android.os.TransactionTooLargeException: data parcel size 521984 bytes
urls = Collections.singletonList(urls.get(index));
}
if (urls instanceof ArrayList) {
intent.putExtra(IntentKey.IMAGE, (ArrayList<String>) urls);
} else {
intent.putExtra(IntentKey.IMAGE, new ArrayList<>(urls));
}
intent.putExtra(IntentKey.INDEX, index);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
private ViewPager mViewPager;
private PageIndicatorView mIndicatorView;
private ImagePreviewAdapter mAdapter;
/** 圆圈指示器 */
private CircleIndicator mCircleIndicatorView;
/** 文本指示器 */
private TextView mTextIndicatorView;
@Override
protected int getLayoutId() {
@ -56,21 +91,37 @@ public final class ImagePreviewActivity extends MyActivity {
@Override
protected void initView() {
mViewPager = findViewById(R.id.vp_image_preview_pager);
mIndicatorView = findViewById(R.id.pv_image_preview_indicator);
mIndicatorView.setViewPager(mViewPager);
mCircleIndicatorView = findViewById(R.id.ci_image_preview_indicator);
mTextIndicatorView = findViewById(R.id.tv_image_preview_indicator);
}
@Override
protected void initData() {
ArrayList<String> images = getStringArrayList(IntentKey.IMAGE);
int index = getInt(IntentKey.INDEX);
if (images != null && images.size() > 0) {
mViewPager.setAdapter(new ImagePagerAdapter(this, images));
if (index != 0 && index <= images.size()) {
mViewPager.setCurrentItem(index);
}
} else {
if (images == null || images.isEmpty()) {
finish();
return;
}
mAdapter = new ImagePreviewAdapter(this);
mAdapter.setData(images);
mAdapter.setOnItemClickListener(this);
mViewPager.setAdapter(new RecyclerPagerAdapter(mAdapter));
if (images.size() != 1) {
if (images.size() < 10) {
// 如果是 10 张以内的图片那么就显示圆圈指示器
mCircleIndicatorView.setVisibility(View.VISIBLE);
mCircleIndicatorView.setViewPager(mViewPager);
} else {
// 如果超过 10 张图片那么就显示文字指示器
mTextIndicatorView.setVisibility(View.VISIBLE);
mViewPager.addOnPageChangeListener(this);
}
int index = getInt(IntentKey.INDEX);
if (index < images.size()) {
mViewPager.setCurrentItem(index);
onPageSelected(index);
}
}
}
@ -91,4 +142,40 @@ public final class ImagePreviewActivity extends MyActivity {
public boolean isSwipeEnable() {
return false;
}
/**
* {@link ViewPager.OnPageChangeListener}
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@SuppressLint("SetTextI18n")
@Override
public void onPageSelected(int position) {
mTextIndicatorView.setText((position + 1) + "/" + mAdapter.getItemCount());
}
@Override
public void onPageScrollStateChanged(int state) {}
@Override
protected void onDestroy() {
super.onDestroy();
mViewPager.removeOnPageChangeListener(this);
}
/**
* {@link BaseAdapter.OnItemClickListener}
* @param recyclerView RecyclerView 对象
* @param itemView 被点击的条目对象
* @param position 被点击的条目位置
*/
@Override
public void onItemClick(RecyclerView recyclerView, View itemView, int position) {
// 单击图片退出当前的 Activity
if (!isFinishing()) {
finish();
}
}
}

View File

@ -20,18 +20,20 @@ import com.hjq.demo.action.StatusAction;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.Permissions;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.manager.ThreadPoolManager;
import com.hjq.demo.other.GridSpaceDecoration;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.ui.adapter.ImageSelectAdapter;
import com.hjq.demo.ui.dialog.AlbumDialog;
import com.hjq.demo.widget.HintLayout;
import com.hjq.demo.widget.StatusLayout;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@ -41,7 +43,7 @@ import java.util.Set;
* time : 2019/07/24
* desc : 选择图片
*/
public final class ImageSelectActivity extends MyActivity
public final class ImageSelectActivity extends AppActivity
implements StatusAction, Runnable,
BaseAdapter.OnItemClickListener,
BaseAdapter.OnItemLongClickListener,
@ -52,7 +54,7 @@ public final class ImageSelectActivity extends MyActivity
}
@DebugLog
@Permissions({Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE})
@Permissions({Permission.MANAGE_EXTERNAL_STORAGE})
public static void start(BaseActivity activity, int maxSelect, OnPhotoSelectListener listener) {
if (maxSelect < 1) {
// 最少要选择一个图片
@ -66,15 +68,28 @@ public final class ImageSelectActivity extends MyActivity
return;
}
if (resultCode == RESULT_OK) {
listener.onSelected(data.getStringArrayListExtra(IntentKey.IMAGE));
} else {
ArrayList<String> list = data.getStringArrayListExtra(IntentKey.IMAGE);
if (list == null || list.isEmpty()) {
listener.onCancel();
return;
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
if (!new File(iterator.next()).isFile()) {
iterator.remove();
}
}
if (resultCode == RESULT_OK && !list.isEmpty()) {
listener.onSelected(list);
return;
}
listener.onCancel();
});
}
private HintLayout mHintLayout;
private StatusLayout mStatusLayout;
private RecyclerView mRecyclerView;
private FloatingActionButton mFloatingView;
@ -90,6 +105,9 @@ public final class ImageSelectActivity extends MyActivity
/** 图片专辑 */
private final HashMap<String, List<String>> mAllAlbum = new HashMap<>();
/** 专辑选择对话框 */
private AlbumDialog.Builder mAlbumDialog;
@Override
protected int getLayoutId() {
return R.layout.image_select_activity;
@ -97,7 +115,7 @@ public final class ImageSelectActivity extends MyActivity
@Override
protected void initView() {
mHintLayout = findViewById(R.id.hl_image_select_hint);
mStatusLayout = findViewById(R.id.hl_image_select_hint);
mRecyclerView = findViewById(R.id.rv_image_select_list);
mFloatingView = findViewById(R.id.fab_image_select_floating);
setOnClickListener(mFloatingView);
@ -113,7 +131,6 @@ public final class ImageSelectActivity extends MyActivity
mRecyclerView.addItemDecoration(new GridSpaceDecoration((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics())));
}
@SuppressWarnings("all")
@Override
protected void initData() {
// 获取最大的选择数
@ -122,74 +139,82 @@ public final class ImageSelectActivity extends MyActivity
// 显示加载进度条
showLoading();
// 加载图片列表
new Thread(ImageSelectActivity.this).start();
ThreadPoolManager.getInstance().execute(this);
}
@Override
public HintLayout getHintLayout() {
return mHintLayout;
public StatusLayout getStatusLayout() {
return mStatusLayout;
}
@SingleClick
@Override
public void onRightClick(View v) {
public void onRightClick(View view) {
if (mAllImage.isEmpty()) {
return;
}
ArrayList<AlbumDialog.AlbumInfo> data = new ArrayList<>(mAllAlbum.size() + 1);
data.add(new AlbumDialog.AlbumInfo(mAllImage.get(0), getString(R.string.image_select_all), String.format(getString(R.string.image_select_total), mAllAlbum.size()), mAdapter.getData() == mAllImage));
int count = 0;
Set<String> keys = mAllAlbum.keySet();
for (String key : keys) {
List<String> temp = mAllAlbum.get(key);
if (temp != null && !temp.isEmpty()) {
data.add(new AlbumDialog.AlbumInfo(temp.get(0), key, String.format(getString(R.string.image_select_total), temp.size()), mAdapter.getData() == temp));
List<String> list = mAllAlbum.get(key);
if (list != null && !list.isEmpty()) {
count += list.size();
data.add(new AlbumDialog.AlbumInfo(list.get(0), key, String.format(getString(R.string.image_select_total), list.size()), mAdapter.getData() == list));
}
}
data.add(0, new AlbumDialog.AlbumInfo(mAllImage.get(0), getString(R.string.image_select_all), String.format(getString(R.string.image_select_total), count), mAdapter.getData() == mAllImage));
new AlbumDialog.Builder(this)
.setData(data)
.setListener((dialog, position, bean) -> {
if (mAlbumDialog == null) {
mAlbumDialog = new AlbumDialog.Builder(this)
.setListener((dialog, position, bean) -> {
setRightTitle(bean.getName());
// 滚动回第一个位置
mRecyclerView.scrollToPosition(0);
if (position == 0) {
mAdapter.setData(mAllImage);
} else {
mAdapter.setData(mAllAlbum.get(bean.getName()));
}
// 执行列表动画
mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.from_right_layout));
mRecyclerView.scheduleLayoutAnimation();
})
setRightTitle(bean.getName());
// 滚动回第一个位置
mRecyclerView.scrollToPosition(0);
if (position == 0) {
mAdapter.setData(mAllImage);
} else {
mAdapter.setData(mAllAlbum.get(bean.getName()));
}
// 执行列表动画
mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.from_right_layout));
mRecyclerView.scheduleLayoutAnimation();
});
}
mAlbumDialog.setData(data)
.show();
}
@Override
protected void onRestart() {
super.onRestart();
Iterator<String> iterator = mSelectImage.iterator();
// 遍历判断选择了的图片是否被删除了
for (String path : mSelectImage) {
while (iterator.hasNext()) {
String path = iterator.next();
File file = new File(path);
if (!file.isFile()) {
if (file.isFile()) {
continue;
}
mSelectImage.remove(path);
mAllImage.remove(path);
iterator.remove();
mAllImage.remove(path);
File parentFile = file.getParentFile();
if (parentFile != null) {
List<String> data = mAllAlbum.get(parentFile.getName());
if (data != null) {
data.remove(path);
}
mAdapter.notifyDataSetChanged();
File parentFile = file.getParentFile();
if (parentFile != null) {
List<String> data = mAllAlbum.get(parentFile.getName());
if (data != null) {
data.remove(path);
}
mAdapter.notifyDataSetChanged();
if (mSelectImage.isEmpty()) {
mFloatingView.setImageResource(R.drawable.camera_ic);
} else {
mFloatingView.setImageResource(R.drawable.succeed_ic);
}
if (mSelectImage.isEmpty()) {
mFloatingView.setImageResource(R.drawable.camera_ic);
} else {
mFloatingView.setImageResource(R.drawable.succeed_ic);
}
}
}
@ -197,8 +222,8 @@ public final class ImageSelectActivity extends MyActivity
@SingleClick
@Override
public void onClick(View v) {
if (v.getId() == R.id.fab_image_select_floating) {
public void onClick(View view) {
if (view.getId() == R.id.fab_image_select_floating) {
if (mSelectImage.isEmpty()) {
// 点击拍照
CameraActivity.start(this, file -> {
@ -211,14 +236,15 @@ public final class ImageSelectActivity extends MyActivity
// 这里需要延迟刷新否则可能会找不到拍照的图片
postDelayed(() -> {
// 重新加载图片列表
new Thread(ImageSelectActivity.this).start();
ThreadPoolManager.getInstance().execute(ImageSelectActivity.this);
}, 1000);
});
} else {
// 完成选择
setResult(RESULT_OK, new Intent().putStringArrayListExtra(IntentKey.IMAGE, mSelectImage));
finish();
return;
}
// 完成选择
setResult(RESULT_OK, new Intent().putStringArrayListExtra(IntentKey.IMAGE, mSelectImage));
finish();
}
}
@ -230,11 +256,7 @@ public final class ImageSelectActivity extends MyActivity
*/
@Override
public void onItemClick(RecyclerView recyclerView, View itemView, int position) {
if (mSelectImage.contains(mAdapter.getItem(position))) {
ImagePreviewActivity.start(getActivity(), mSelectImage, mSelectImage.indexOf(mAdapter.getItem(position)));
} else {
ImagePreviewActivity.start(getActivity(), mAdapter.getItem(position));
}
ImagePreviewActivity.start(getActivity(), mAdapter.getData(), position);
}
/**
@ -248,9 +270,8 @@ public final class ImageSelectActivity extends MyActivity
if (mSelectImage.size() < mMaxSelect) {
// 长按的时候模拟选中
return itemView.findViewById(R.id.fl_image_select_check).performClick();
} else {
return false;
}
return false;
}
/**
@ -262,8 +283,17 @@ public final class ImageSelectActivity extends MyActivity
@Override
public void onChildClick(RecyclerView recyclerView, View childView, int position) {
if (childView.getId() == R.id.fl_image_select_check) {
if (mSelectImage.contains(mAdapter.getItem(position))) {
mSelectImage.remove(mAdapter.getItem(position));
String path = mAdapter.getItem(position);
File file = new File(path);
if (!file.isFile()) {
mAdapter.removeItem(position);
toast(R.string.image_select_error);
return;
}
if (mSelectImage.contains(path)) {
mSelectImage.remove(path);
if (mSelectImage.isEmpty()) {
mFloatingView.hide();
@ -273,34 +303,34 @@ public final class ImageSelectActivity extends MyActivity
}, 200);
}
} else {
mAdapter.notifyItemChanged(position);
return;
}
if (mMaxSelect == 1 && mSelectImage.size() == 1) {
if (mMaxSelect == 1 && mSelectImage.size() == 1) {
List<String> data = mAdapter.getData();
if (data != null) {
int index = data.indexOf(mSelectImage.get(0));
if (index != -1) {
mSelectImage.remove(0);
mAdapter.notifyItemChanged(index);
}
List<String> data = mAdapter.getData();
if (data != null) {
int index = data.indexOf(mSelectImage.remove(0));
if (index != -1) {
mAdapter.notifyItemChanged(index);
}
mSelectImage.add(mAdapter.getItem(position));
} else if (mSelectImage.size() < mMaxSelect) {
mSelectImage.add(mAdapter.getItem(position));
if (mSelectImage.size() == 1) {
mFloatingView.hide();
postDelayed(() -> {
mFloatingView.setImageResource(R.drawable.succeed_ic);
mFloatingView.show();
}, 200);
}
} else {
toast(String.format(getString(R.string.image_select_max_hint), mMaxSelect));
}
mSelectImage.add(path);
} else if (mSelectImage.size() < mMaxSelect) {
mSelectImage.add(path);
if (mSelectImage.size() == 1) {
mFloatingView.hide();
postDelayed(() -> {
mFloatingView.setImageResource(R.drawable.succeed_ic);
mFloatingView.show();
}, 200);
}
} else {
toast(String.format(getString(R.string.image_select_max_hint), mMaxSelect));
}
mAdapter.notifyItemChanged(position);
}
@ -322,7 +352,7 @@ public final class ImageSelectActivity extends MyActivity
MediaStore.MediaColumns.HEIGHT, MediaStore.MediaColumns.SIZE};
Cursor cursor = null;
if (XXPermissions.hasPermission(this, Permission.READ_EXTERNAL_STORAGE)) {
if (XXPermissions.isGrantedPermission(this, Permission.MANAGE_EXTERNAL_STORAGE)) {
cursor = contentResolver.query(contentUri, projections, selection, new String[]{String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE)}, sortOrder);
}
if (cursor != null && cursor.moveToFirst()) {
@ -333,8 +363,8 @@ public final class ImageSelectActivity extends MyActivity
do {
long size = cursor.getLong(sizeIndex);
// 图片大小不得小于 10 KB
if (size < 1024 * 10) {
// 图片大小不得小于 1 KB
if (size < 1024) {
continue;
}
@ -383,15 +413,16 @@ public final class ImageSelectActivity extends MyActivity
mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.fall_down_layout));
mRecyclerView.scheduleLayoutAnimation();
// 设置右标提
setRightTitle(R.string.image_select_all);
if (mAllImage.isEmpty()) {
// 显示空布局
showEmpty();
// 设置右标题
setRightTitle(null);
} else {
// 显示加载完成
showComplete();
// 设置右标题
setRightTitle(R.string.image_select_all);
}
}, 500);
}

View File

@ -2,28 +2,33 @@ package com.hjq.demo.ui.activity;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Button;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.hjq.demo.R;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.helper.InputTextHelper;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.glide.GlideApp;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.LoginApi;
import com.hjq.demo.http.response.LoginBean;
import com.hjq.demo.manager.InputTextManager;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.other.KeyboardWatcher;
import com.hjq.demo.ui.fragment.MeFragment;
import com.hjq.demo.wxapi.WXEntryActivity;
import com.hjq.http.EasyConfig;
import com.hjq.http.EasyHttp;
@ -31,6 +36,9 @@ import com.hjq.http.listener.HttpCallback;
import com.hjq.umeng.Platform;
import com.hjq.umeng.UmengClient;
import com.hjq.umeng.UmengLogin;
import com.hjq.widget.view.SubmitButton;
import okhttp3.Call;
/**
* author : Android 轮子哥
@ -38,15 +46,19 @@ import com.hjq.umeng.UmengLogin;
* time : 2018/10/18
* desc : 登录界面
*/
public final class LoginActivity extends MyActivity
public final class LoginActivity extends AppActivity
implements UmengLogin.OnLoginListener,
KeyboardWatcher.SoftKeyboardStateListener {
KeyboardWatcher.SoftKeyboardStateListener,
TextView.OnEditorActionListener {
@DebugLog
public static void start(Context context, String phone, String password) {
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(IntentKey.PHONE, phone);
intent.putExtra(IntentKey.PASSWORD, password);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
@ -57,9 +69,7 @@ public final class LoginActivity extends MyActivity
private EditText mPasswordView;
private View mForgetView;
private Button mCommitView;
private View mBlankView;
private SubmitButton mCommitView;
private View mOtherView;
private View mQQView;
@ -83,13 +93,15 @@ public final class LoginActivity extends MyActivity
mPasswordView = findViewById(R.id.et_login_password);
mForgetView = findViewById(R.id.tv_login_forget);
mCommitView = findViewById(R.id.btn_login_commit);
mBlankView = findViewById(R.id.v_login_blank);
mOtherView = findViewById(R.id.ll_login_other);
mQQView = findViewById(R.id.iv_login_qq);
mWeChatView = findViewById(R.id.iv_login_wechat);
setOnClickListener(mForgetView, mCommitView, mQQView, mWeChatView);
InputTextHelper.with(this)
mPasswordView.setOnEditorActionListener(this);
InputTextManager.with(this)
.addView(mPhoneView)
.addView(mPasswordView)
.setMain(mCommitView)
@ -98,14 +110,9 @@ public final class LoginActivity extends MyActivity
@Override
protected void initData() {
postDelayed(() -> {
// 因为在小屏幕手机上面因为计算规则的因素会导致动画效果特别夸张所以不在小屏幕手机上面展示这个动画效果
if (mBlankView.getHeight() > mBodyLayout.getHeight()) {
// 只有空白区域的高度大于登录框区域的高度才展示动画
KeyboardWatcher.with(LoginActivity.this)
.setListener(LoginActivity.this);
}
KeyboardWatcher.with(LoginActivity.this)
.setListener(LoginActivity.this);
}, 500);
// 判断用户当前有没有安装 QQ
@ -123,42 +130,51 @@ public final class LoginActivity extends MyActivity
mOtherView.setVisibility(View.GONE);
}
// 填充传入的手机号和密码
// 自动填充手机号和密码
mPhoneView.setText(getString(IntentKey.PHONE));
mPasswordView.setText(getString(IntentKey.PASSWORD));
}
@Override
public void onRightClick(View v) {
public void onRightClick(View view) {
// 跳转到注册界面
startActivityForResult(RegisterActivity.class, (resultCode, data) -> {
RegisterActivity.start(this, mPhoneView.getText().toString(), mPasswordView.getText().toString(), (phone, password) -> {
// 如果已经注册成功就执行登录操作
if (resultCode == RESULT_OK && data != null) {
mPhoneView.setText(data.getStringExtra(IntentKey.PHONE));
mPasswordView.setText(data.getStringExtra(IntentKey.PASSWORD));
mPasswordView.setSelection(mPasswordView.getText().length());
onClick(mCommitView);
}
mPhoneView.setText(phone);
mPasswordView.setText(password);
mPasswordView.requestFocus();
mPasswordView.setSelection(mPasswordView.getText().length());
onClick(mCommitView);
});
}
@SingleClick
@Override
public void onClick(View v) {
if (v == mForgetView) {
public void onClick(View view) {
if (view == mForgetView) {
startActivity(PasswordForgetActivity.class);
} else if (v == mCommitView) {
return;
}
if (view == mCommitView) {
if (mPhoneView.getText().toString().length() != 11) {
mPhoneView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
mCommitView.showError(3000);
toast(R.string.common_phone_input_error);
return;
}
// 隐藏软键盘
hideKeyboard(getCurrentFocus());
if (true) {
showDialog();
mCommitView.showProgress();
postDelayed(() -> {
hideDialog();
startActivity(HomeActivity.class);
finish();
mCommitView.showSucceed();
postDelayed(() -> {
HomeActivity.start(getContext(), MeFragment.class);
finish();
}, 1000);
}, 2000);
return;
}
@ -169,22 +185,46 @@ public final class LoginActivity extends MyActivity
.setPassword(mPasswordView.getText().toString()))
.request(new HttpCallback<HttpData<LoginBean>>(this) {
@Override
public void onStart(Call call) {
mCommitView.showProgress();
}
@Override
public void onEnd(Call call) {}
@Override
public void onSucceed(HttpData<LoginBean> data) {
// 更新 Token
EasyConfig.getInstance()
.addParam("token", data.getData().getToken());
// 跳转到主页
startActivity(HomeActivity.class);
finish();
postDelayed(() -> {
mCommitView.showSucceed();
postDelayed(() -> {
// 跳转到首页
HomeActivity.start(getContext(), MeFragment.class);
finish();
}, 1000);
}, 1000);
}
@Override
public void onFail(Exception e) {
super.onFail(e);
postDelayed(() -> {
mCommitView.showError(3000);
}, 1000);
}
});
} else if (v == mQQView || v == mWeChatView) {
return;
}
if (view == mQQView || view == mWeChatView) {
toast("记得改好第三方 AppID 和 AppKey否则会调不起来哦");
Platform platform;
if (v == mQQView) {
if (view == mQQView) {
platform = Platform.QQ;
} else if (v == mWeChatView) {
} else if (view == mWeChatView) {
platform = Platform.WECHAT;
toast("也别忘了改微信 " + WXEntryActivity.class.getSimpleName() + " 类所在的包名哦");
} else {
@ -213,6 +253,11 @@ public final class LoginActivity extends MyActivity
*/
@Override
public void onSucceed(Platform platform, UmengLogin.LoginData data) {
if (isFinishing() || isDestroyed()) {
// GlideYou cannot start a load for a destroyed activity
return;
}
// 判断第三方登录的平台
switch (platform) {
case QQ:
@ -260,56 +305,61 @@ public final class LoginActivity extends MyActivity
@Override
public void onSoftKeyboardOpened(int keyboardHeight) {
int screenHeight = getResources().getDisplayMetrics().heightPixels;
int[] location = new int[2];
// 获取这个 View 在屏幕中的坐标左上角
mBodyLayout.getLocationOnScreen(location);
//int x = location[0];
int y = location[1];
int bottom = screenHeight - (y + mBodyLayout.getHeight());
if (keyboardHeight > bottom){
// 执行位移动画
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mBodyLayout, "translationY", 0, -(keyboardHeight - bottom));
objectAnimator.setDuration(mAnimTime);
objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
objectAnimator.start();
// 执行位移动画
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mBodyLayout, "translationY", 0, -mCommitView.getHeight());
objectAnimator.setDuration(mAnimTime);
objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
objectAnimator.start();
// 执行缩小动画
mLogoView.setPivotX(mLogoView.getWidth() / 2f);
mLogoView.setPivotY(mLogoView.getHeight());
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogoView, "scaleX", 1.0f, mLogoScale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogoView, "scaleY", 1.0f, mLogoScale);
ObjectAnimator translationY = ObjectAnimator.ofFloat(mLogoView, "translationY", 0.0f, -(keyboardHeight - bottom));
animatorSet.play(translationY).with(scaleX).with(scaleY);
animatorSet.setDuration(mAnimTime);
animatorSet.start();
}
// 执行缩小动画
mLogoView.setPivotX(mLogoView.getWidth() / 2f);
mLogoView.setPivotY(mLogoView.getHeight());
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogoView, "scaleX", 1f, mLogoScale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogoView, "scaleY", 1f, mLogoScale);
ObjectAnimator translationY = ObjectAnimator.ofFloat(mLogoView, "translationY", 0f, -mCommitView.getHeight());
animatorSet.play(translationY).with(scaleX).with(scaleY);
animatorSet.setDuration(mAnimTime);
animatorSet.start();
}
@Override
public void onSoftKeyboardClosed() {
// 执行位移动画
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mBodyLayout, "translationY", mBodyLayout.getTranslationY(), 0);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mBodyLayout, "translationY", mBodyLayout.getTranslationY(), 0f);
objectAnimator.setDuration(mAnimTime);
objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
objectAnimator.start();
if (mLogoView.getTranslationY() == 0){
if (mLogoView.getTranslationY() == 0) {
return;
}
// 执行放大动画
mLogoView.setPivotX(mLogoView.getWidth() / 2f);
mLogoView.setPivotY(mLogoView.getHeight());
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogoView, "scaleX", mLogoScale, 1.0f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogoView, "scaleY", mLogoScale, 1.0f);
ObjectAnimator translationY = ObjectAnimator.ofFloat(mLogoView, "translationY", mLogoView.getTranslationY(), 0);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogoView, "scaleX", mLogoScale, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogoView, "scaleY", mLogoScale, 1f);
ObjectAnimator translationY = ObjectAnimator.ofFloat(mLogoView, "translationY", mLogoView.getTranslationY(), 0f);
animatorSet.play(translationY).with(scaleX).with(scaleY);
animatorSet.setDuration(mAnimTime);
animatorSet.start();
}
/**
* {@link TextView.OnEditorActionListener}
*/
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE && mCommitView.isEnabled()) {
// 模拟点击登录按钮
onClick(mCommitView);
return true;
}
return false;
}
@Override
public boolean isSwipeEnable() {
return false;

View File

@ -1,19 +1,22 @@
package com.hjq.demo.ui.activity;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.helper.InputTextHelper;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.GetCodeApi;
import com.hjq.demo.http.request.VerifyCodeApi;
import com.hjq.demo.manager.InputTextManager;
import com.hjq.http.EasyHttp;
import com.hjq.http.listener.HttpCallback;
import com.hjq.toast.ToastUtils;
import com.hjq.widget.view.CountdownView;
/**
@ -22,7 +25,8 @@ import com.hjq.widget.view.CountdownView;
* time : 2019/02/27
* desc : 忘记密码
*/
public final class PasswordForgetActivity extends MyActivity {
public final class PasswordForgetActivity extends AppActivity
implements TextView.OnEditorActionListener {
private EditText mPhoneView;
private EditText mCodeView;
@ -40,9 +44,12 @@ public final class PasswordForgetActivity extends MyActivity {
mCodeView = findViewById(R.id.et_password_forget_code);
mCountdownView = findViewById(R.id.cv_password_forget_countdown);
mCommitView = findViewById(R.id.btn_password_forget_commit);
setOnClickListener(mCountdownView, mCommitView);
InputTextHelper.with(this)
mCodeView.setOnEditorActionListener(this);
InputTextManager.with(this)
.addView(mPhoneView)
.addView(mCodeView)
.setMain(mCommitView)
@ -56,9 +63,10 @@ public final class PasswordForgetActivity extends MyActivity {
@SingleClick
@Override
public void onClick(View v) {
if (v == mCountdownView) {
public void onClick(View view) {
if (view == mCountdownView) {
if (mPhoneView.getText().toString().length() != 11) {
mPhoneView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
toast(R.string.common_phone_input_error);
return;
}
@ -69,6 +77,9 @@ public final class PasswordForgetActivity extends MyActivity {
return;
}
// 隐藏软键盘
hideKeyboard(getCurrentFocus());
// 获取验证码
EasyHttp.post(this)
.api(new GetCodeApi()
@ -81,15 +92,17 @@ public final class PasswordForgetActivity extends MyActivity {
mCountdownView.start();
}
});
} else if (v == mCommitView) {
} else if (view == mCommitView) {
if (mPhoneView.getText().toString().length() != 11) {
mPhoneView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
toast(R.string.common_phone_input_error);
return;
}
if (mCodeView.getText().toString().length() != getResources().getInteger(R.integer.sms_code_length)) {
ToastUtils.show(R.string.common_code_error_hint);
mCodeView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
toast(R.string.common_code_error_hint);
return;
}
@ -114,4 +127,17 @@ public final class PasswordForgetActivity extends MyActivity {
});
}
}
/**
* {@link TextView.OnEditorActionListener}
*/
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE && mCommitView.isEnabled()) {
// 模拟点击下一步按钮
onClick(mCommitView);
return true;
}
return false;
}
}

View File

@ -1,19 +1,25 @@
package com.hjq.demo.ui.activity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.hjq.demo.R;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.helper.InputTextHelper;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.PasswordApi;
import com.hjq.demo.manager.InputTextManager;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.ui.dialog.HintDialog;
import com.hjq.http.EasyHttp;
import com.hjq.http.listener.HttpCallback;
@ -23,18 +29,22 @@ import com.hjq.http.listener.HttpCallback;
* time : 2019/02/27
* desc : 重置密码
*/
public final class PasswordResetActivity extends MyActivity {
public final class PasswordResetActivity extends AppActivity
implements TextView.OnEditorActionListener {
@DebugLog
public static void start(Context context, String phone, String code) {
Intent intent = new Intent(context, PasswordResetActivity.class);
intent.putExtra(IntentKey.PHONE, phone);
intent.putExtra(IntentKey.CODE, code);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
private EditText mPasswordView1;
private EditText mPasswordView2;
private EditText mFirstPassword;
private EditText mSecondPassword;
private Button mCommitView;
/** 手机号 */
@ -49,14 +59,17 @@ public final class PasswordResetActivity extends MyActivity {
@Override
protected void initView() {
mPasswordView1 = findViewById(R.id.et_password_reset_password1);
mPasswordView2 = findViewById(R.id.et_password_reset_password2);
mFirstPassword = findViewById(R.id.et_password_reset_password1);
mSecondPassword = findViewById(R.id.et_password_reset_password2);
mCommitView = findViewById(R.id.btn_password_reset_commit);
setOnClickListener(mCommitView);
InputTextHelper.with(this)
.addView(mPasswordView1)
.addView(mPasswordView2)
mSecondPassword.setOnEditorActionListener(this);
InputTextManager.with(this)
.addView(mFirstPassword)
.addView(mSecondPassword)
.setMain(mCommitView)
.build();
}
@ -69,17 +82,26 @@ public final class PasswordResetActivity extends MyActivity {
@SingleClick
@Override
public void onClick(View v) {
if (v == mCommitView) {
public void onClick(View view) {
if (view == mCommitView) {
if (!mPasswordView1.getText().toString().equals(mPasswordView2.getText().toString())) {
if (!mFirstPassword.getText().toString().equals(mSecondPassword.getText().toString())) {
mFirstPassword.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
mSecondPassword.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
toast(R.string.common_password_input_unlike);
return;
}
// 隐藏软键盘
hideKeyboard(getCurrentFocus());
if (true) {
toast(R.string.password_reset_success);
finish();
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_FINISH)
.setMessage(R.string.password_reset_success)
.setDuration(2000)
.addOnDismissListener(dialog -> finish())
.show();
return;
}
@ -88,15 +110,32 @@ public final class PasswordResetActivity extends MyActivity {
.api(new PasswordApi()
.setPhone(mPhoneNumber)
.setCode(mVerifyCode)
.setPassword(mPasswordView1.getText().toString()))
.setPassword(mFirstPassword.getText().toString()))
.request(new HttpCallback<HttpData<Void>>(this) {
@Override
public void onSucceed(HttpData<Void> data) {
toast(R.string.password_reset_success);
finish();
new HintDialog.Builder(getActivity())
.setIcon(HintDialog.ICON_FINISH)
.setMessage(R.string.password_reset_success)
.setDuration(2000)
.addOnDismissListener(dialog -> finish())
.show();
}
});
}
}
/**
* {@link TextView.OnEditorActionListener}
*/
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE && mCommitView.isEnabled()) {
// 模拟点击提交按钮
onClick(mCommitView);
return true;
}
return false;
}
}

View File

@ -1,16 +1,27 @@
package com.hjq.demo.ui.activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.core.content.FileProvider;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.glide.GlideApp;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.UpdateImageApi;
import com.hjq.demo.other.AppConfig;
import com.hjq.demo.ui.dialog.AddressDialog;
import com.hjq.demo.ui.dialog.InputDialog;
import com.hjq.http.EasyHttp;
@ -18,6 +29,9 @@ import com.hjq.http.listener.HttpCallback;
import com.hjq.widget.layout.SettingBar;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* author : Android 轮子哥
@ -25,11 +39,11 @@ import java.io.File;
* time : 2019/04/20
* desc : 个人资料
*/
public final class PersonalDataActivity extends MyActivity {
public final class PersonalDataActivity extends AppActivity {
private ViewGroup mAvatarLayout;
private ImageView mAvatarView;
private SettingBar mIDView;
private SettingBar mIdView;
private SettingBar mNameView;
private SettingBar mAddressView;
@ -52,7 +66,7 @@ public final class PersonalDataActivity extends MyActivity {
protected void initView() {
mAvatarLayout = findViewById(R.id.fl_person_data_avatar);
mAvatarView = findViewById(R.id.iv_person_data_avatar);
mIDView = findViewById(R.id.sb_person_data_id);
mIdView = findViewById(R.id.sb_person_data_id);
mNameView = findViewById(R.id.sb_person_data_name);
mAddressView = findViewById(R.id.sb_person_data_address);
setOnClickListener(mAvatarLayout, mAvatarView, mNameView, mAddressView);
@ -64,42 +78,25 @@ public final class PersonalDataActivity extends MyActivity {
.load(R.drawable.avatar_placeholder_ic)
.placeholder(R.drawable.avatar_placeholder_ic)
.error(R.drawable.avatar_placeholder_ic)
.circleCrop()
.transform(new MultiTransformation<>(new CenterCrop(), new CircleCrop()))
.into(mAvatarView);
mIdView.setRightText("880634");
mNameView.setRightText("Android 轮子哥");
String address = mProvince + mCity + mArea;
mAddressView.setRightText(address);
}
@SingleClick
@Override
public void onClick(View v) {
if (v == mAvatarLayout) {
public void onClick(View view) {
if (view == mAvatarLayout) {
ImageSelectActivity.start(this, data -> {
if (true) {
mAvatarUrl = data.get(0);
GlideApp.with(getActivity())
.load(mAvatarUrl)
.into(mAvatarView);
return;
}
// 上传头像
EasyHttp.post(this)
.api(new UpdateImageApi()
.setImage(new File(data.get(0))))
.request(new HttpCallback<HttpData<String>>(PersonalDataActivity.this) {
@Override
public void onSucceed(HttpData<String> data) {
mAvatarUrl = data.getData();
GlideApp.with(getActivity())
.load(mAvatarUrl)
.into(mAvatarView);
}
});
// 裁剪头像
cropImage(new File(data.get(0)));
});
} else if (v == mAvatarView) {
} else if (view == mAvatarView) {
if (!TextUtils.isEmpty(mAvatarUrl)) {
// 查看头像
ImagePreviewActivity.start(getActivity(), mAvatarUrl);
@ -107,7 +104,7 @@ public final class PersonalDataActivity extends MyActivity {
// 选择头像
onClick(mAvatarLayout);
}
} else if (v == mNameView) {
} else if (view == mNameView) {
new InputDialog.Builder(this)
// 标题可以不用填写
.setTitle(getString(R.string.personal_data_name_hint))
@ -124,7 +121,7 @@ public final class PersonalDataActivity extends MyActivity {
}
})
.show();
} else if (v == mAddressView) {
} else if (view == mAddressView) {
new AddressDialog.Builder(this)
//.setTitle("选择地区")
// 设置默认省份
@ -145,4 +142,114 @@ public final class PersonalDataActivity extends MyActivity {
.show();
}
}
/**
* 裁剪图片
*/
private void cropImage(File sourceFile) {
Intent intent = new Intent("com.android.camera.action.CROP");
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(getContext(), AppConfig.getPackageName() + ".provider", sourceFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
uri = Uri.fromFile(sourceFile);
}
String regex = "^(.+)(\\..+)$";
String fileName = sourceFile.getName().replaceFirst(regex, "$1_crop_" + new SimpleDateFormat("HHmmss", Locale.getDefault()).format(new Date())+ "$2");
File outputFile = new File(sourceFile.getParent(), fileName);
if (outputFile.exists()) {
outputFile.delete();
}
intent.setDataAndType(uri, "image/*");
// 是否进行裁剪
intent.putExtra("crop", String.valueOf(true));
// 宽高裁剪比例
if (Build.MANUFACTURER.toUpperCase().contains("HUAWEI")) {
// 华为手机特殊处理否则不会显示正方形裁剪区域而是显示圆形裁剪区域
// https://blog.csdn.net/wapchief/article/details/80669647
intent.putExtra("aspectX", 9998);
intent.putExtra("aspectY", 9999);
} else {
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
}
// 是否裁剪成圆形在华为手机上没有任何效果
// intent.putExtra("circleCrop", false);
// 宽高裁剪大小
// intent.putExtra("outputX", 200);
// intent.putExtra("outputY", 200);
// 是否保持比例不变
intent.putExtra("scale", true);
// 裁剪区域小于输出大小时是否放大图像
intent.putExtra("scaleUpIfNeeded", true);
// 是否将数据以 Bitmap 的形式保存
intent.putExtra("return-data", false);
// 设置裁剪后保存的文件路径
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
// 设置裁剪后保存的文件格式
intent.putExtra("outputFormat", getImageFormat(sourceFile).toString());
// 判断手机是否有裁剪功能
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, (resultCode, data) -> {
if (resultCode == RESULT_OK) {
updateImage(outputFile, true);
}
});
return;
}
// 没有的话就不裁剪直接上传原图片
// 但是这种情况极其少见可以忽略不计
updateImage(sourceFile, false);
}
/**
* 上传图片
*/
private void updateImage(File file, boolean deleteFile) {
if (true) {
mAvatarUrl = file.getPath();
GlideApp.with(getActivity())
.load(mAvatarUrl)
.transform(new MultiTransformation<>(new CenterCrop(), new CircleCrop()))
.into(mAvatarView);
return;
}
EasyHttp.post(this)
.api(new UpdateImageApi()
.setImage(file))
.request(new HttpCallback<HttpData<String>>(PersonalDataActivity.this) {
@Override
public void onSucceed(HttpData<String> data) {
mAvatarUrl = data.getData();
GlideApp.with(getActivity())
.load(mAvatarUrl)
.transform(new MultiTransformation<>(new CenterCrop(), new CircleCrop()))
.into(mAvatarView);
if (deleteFile) {
file.delete();
}
}
});
}
/**
* 获取图片文件的格式
*/
private Bitmap.CompressFormat getImageFormat(File file) {
String fileName = file.getName().toLowerCase();
if (fileName.endsWith(".png")) {
return Bitmap.CompressFormat.PNG;
} else if (fileName.endsWith(".webp")) {
return Bitmap.CompressFormat.WEBP;
}
return Bitmap.CompressFormat.JPEG;
}
}

View File

@ -1,20 +1,26 @@
package com.hjq.demo.ui.activity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.hjq.demo.R;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.helper.InputTextHelper;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.GetCodeApi;
import com.hjq.demo.http.request.PhoneApi;
import com.hjq.demo.manager.InputTextManager;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.ui.dialog.HintDialog;
import com.hjq.http.EasyHttp;
import com.hjq.http.listener.HttpCallback;
import com.hjq.toast.ToastUtils;
@ -26,12 +32,16 @@ import com.hjq.widget.view.CountdownView;
* time : 2019/04/20
* desc : 设置手机号
*/
public final class PhoneResetActivity extends MyActivity {
public final class PhoneResetActivity extends AppActivity
implements TextView.OnEditorActionListener {
@DebugLog
public static void start(Context context, String code) {
Intent intent = new Intent(context, PhoneResetActivity.class);
intent.putExtra(IntentKey.CODE, code);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
@ -54,9 +64,12 @@ public final class PhoneResetActivity extends MyActivity {
mCodeView = findViewById(R.id.et_phone_reset_code);
mCountdownView = findViewById(R.id.cv_phone_reset_countdown);
mCommitView = findViewById(R.id.btn_phone_reset_commit);
setOnClickListener(mCountdownView, mCommitView);
InputTextHelper.with(this)
mCodeView.setOnEditorActionListener(this);
InputTextManager.with(this)
.addView(mPhoneView)
.addView(mCodeView)
.setMain(mCommitView)
@ -70,10 +83,11 @@ public final class PhoneResetActivity extends MyActivity {
@SingleClick
@Override
public void onClick(View v) {
if (v == mCountdownView) {
public void onClick(View view) {
if (view == mCountdownView) {
if (mPhoneView.getText().toString().length() != 11) {
mPhoneView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
toast(R.string.common_phone_input_error);
return;
}
@ -96,9 +110,10 @@ public final class PhoneResetActivity extends MyActivity {
mCountdownView.start();
}
});
} else if (v == mCommitView) {
} else if (view == mCommitView) {
if (mPhoneView.getText().toString().length() != 11) {
mPhoneView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
toast(R.string.common_phone_input_error);
return;
}
@ -108,9 +123,16 @@ public final class PhoneResetActivity extends MyActivity {
return;
}
// 隐藏软键盘
hideKeyboard(getCurrentFocus());
if (true) {
toast(R.string.phone_reset_commit_succeed);
finish();
new HintDialog.Builder(this)
.setIcon(HintDialog.ICON_FINISH)
.setMessage(R.string.phone_reset_commit_succeed)
.setDuration(2000)
.addOnDismissListener(dialog -> finish())
.show();
return;
}
@ -124,10 +146,27 @@ public final class PhoneResetActivity extends MyActivity {
@Override
public void onSucceed(HttpData<Void> data) {
toast(R.string.phone_reset_commit_succeed);
finish();
new HintDialog.Builder(getActivity())
.setIcon(HintDialog.ICON_FINISH)
.setMessage(R.string.phone_reset_commit_succeed)
.setDuration(2000)
.addOnDismissListener(dialog -> finish())
.show();
}
});
}
}
/**
* {@link TextView.OnEditorActionListener}
*/
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE && mCommitView.isEnabled()) {
// 模拟点击提交按钮
onClick(mCommitView);
return true;
}
return false;
}
}

View File

@ -1,25 +1,33 @@
package com.hjq.demo.ui.activity;
import android.content.Intent;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.gyf.immersionbar.ImmersionBar;
import com.hjq.base.BaseActivity;
import com.hjq.demo.R;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.helper.InputTextHelper;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.GetCodeApi;
import com.hjq.demo.http.request.RegisterApi;
import com.hjq.demo.http.response.RegisterBean;
import com.hjq.demo.manager.InputTextManager;
import com.hjq.demo.other.IntentKey;
import com.hjq.http.EasyHttp;
import com.hjq.http.listener.HttpCallback;
import com.hjq.widget.view.CountdownView;
import com.hjq.widget.view.SubmitButton;
import okhttp3.Call;
/**
* author : Android 轮子哥
@ -27,17 +35,37 @@ import com.hjq.widget.view.CountdownView;
* time : 2018/10/18
* desc : 注册界面
*/
public final class RegisterActivity extends MyActivity {
public final class RegisterActivity extends AppActivity
implements TextView.OnEditorActionListener {
@DebugLog
public static void start(BaseActivity activity, String phone, String password, OnRegisterListener listener) {
Intent intent = new Intent(activity, RegisterActivity.class);
intent.putExtra(IntentKey.PHONE, phone);
intent.putExtra(IntentKey.PASSWORD, password);
activity.startActivityForResult(intent, (resultCode, data) -> {
if (listener == null || data == null) {
return;
}
if (resultCode == RESULT_OK) {
listener.onSucceed(data.getStringExtra(IntentKey.PHONE), data.getStringExtra(IntentKey.PASSWORD));
} else {
listener.onCancel();
}
});
}
private EditText mPhoneView;
private CountdownView mCountdownView;
private EditText mCodeView;
private EditText mPasswordView1;
private EditText mPasswordView2;
private EditText mFirstPassword;
private EditText mSecondPassword;
private Button mCommitView;
private SubmitButton mCommitView;
@Override
protected int getLayoutId() {
@ -49,33 +77,40 @@ public final class RegisterActivity extends MyActivity {
mPhoneView = findViewById(R.id.et_register_phone);
mCountdownView = findViewById(R.id.cv_register_countdown);
mCodeView = findViewById(R.id.et_register_code);
mPasswordView1 = findViewById(R.id.et_register_password1);
mPasswordView2 = findViewById(R.id.et_register_password2);
mFirstPassword = findViewById(R.id.et_register_password1);
mSecondPassword = findViewById(R.id.et_register_password2);
mCommitView = findViewById(R.id.btn_register_commit);
setOnClickListener(mCountdownView, mCommitView);
mSecondPassword.setOnEditorActionListener(this);
// 给这个 View 设置沉浸式避免状态栏遮挡
ImmersionBar.setTitleBar(this, findViewById(R.id.tv_register_title));
InputTextHelper.with(this)
InputTextManager.with(this)
.addView(mPhoneView)
.addView(mCodeView)
.addView(mPasswordView1)
.addView(mPasswordView2)
.addView(mFirstPassword)
.addView(mSecondPassword)
.setMain(mCommitView)
.build();
}
@Override
protected void initData() {
// 自动填充手机号和密码
mPhoneView.setText(getString(IntentKey.PHONE));
mFirstPassword.setText(getString(IntentKey.PASSWORD));
mSecondPassword.setText(getString(IntentKey.PASSWORD));
}
@SingleClick
@Override
public void onClick(View v) {
if (v == mCountdownView) {
public void onClick(View view) {
if (view == mCountdownView) {
if (mPhoneView.getText().toString().length() != 11) {
mPhoneView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
toast(R.string.common_phone_input_error);
return;
}
@ -104,23 +139,43 @@ public final class RegisterActivity extends MyActivity {
mCountdownView.start();
}
});
} else if (v == mCommitView) {
} else if (view == mCommitView) {
if (mPhoneView.getText().toString().length() != 11) {
mPhoneView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
mCommitView.showError(3000);
toast(R.string.common_phone_input_error);
return;
}
if (!mPasswordView1.getText().toString().equals(mPasswordView2.getText().toString())) {
if (mCodeView.getText().toString().length() != getResources().getInteger(R.integer.sms_code_length)) {
mCodeView.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
mCommitView.showError(3000);
toast(R.string.common_code_error_hint);
return;
}
if (!mFirstPassword.getText().toString().equals(mSecondPassword.getText().toString())) {
mFirstPassword.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
mSecondPassword.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.shake_anim));
mCommitView.showError(3000);
toast(R.string.common_password_input_unlike);
return;
}
// 隐藏软键盘
hideKeyboard(getCurrentFocus());
if (true) {
toast(R.string.register_succeed);
setResult(RESULT_OK, new Intent()
.putExtra(IntentKey.PHONE, mPhoneView.getText().toString())
.putExtra(IntentKey.PASSWORD, mPasswordView1.getText().toString()));
finish();
mCommitView.showProgress();
postDelayed(() -> {
mCommitView.showSucceed();
postDelayed(() -> {
setResult(RESULT_OK, new Intent()
.putExtra(IntentKey.PHONE, mPhoneView.getText().toString())
.putExtra(IntentKey.PASSWORD, mFirstPassword.getText().toString()));
finish();
}, 1000);
}, 2000);
return;
}
@ -129,16 +184,36 @@ public final class RegisterActivity extends MyActivity {
.api(new RegisterApi()
.setPhone(mPhoneView.getText().toString())
.setCode(mCodeView.getText().toString())
.setPassword(mPasswordView1.getText().toString()))
.setPassword(mFirstPassword.getText().toString()))
.request(new HttpCallback<HttpData<RegisterBean>>(this) {
@Override
public void onStart(Call call) {
mCommitView.showProgress();
}
@Override
public void onEnd(Call call) {}
@Override
public void onSucceed(HttpData<RegisterBean> data) {
toast(R.string.register_succeed);
setResult(RESULT_OK, new Intent()
.putExtra(IntentKey.PHONE, mPhoneView.getText().toString())
.putExtra(IntentKey.PASSWORD, mPasswordView1.getText().toString()));
finish();
postDelayed(() -> {
mCommitView.showSucceed();
postDelayed(() -> {
setResult(RESULT_OK, new Intent()
.putExtra(IntentKey.PHONE, mPhoneView.getText().toString())
.putExtra(IntentKey.PASSWORD, mFirstPassword.getText().toString()));
finish();
}, 1000);
}, 1000);
}
@Override
public void onFail(Exception e) {
super.onFail(e);
postDelayed(() -> {
mCommitView.showError(3000);
}, 1000);
}
});
}
@ -152,8 +227,40 @@ public final class RegisterActivity extends MyActivity {
.keyboardEnable(true);
}
/**
* {@link TextView.OnEditorActionListener}
*/
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE && mCommitView.isEnabled()) {
// 模拟点击注册按钮
onClick(mCommitView);
return true;
}
return false;
}
@Override
public boolean isSwipeEnable() {
return false;
}
/**
* 注册监听
*/
public interface OnRegisterListener {
/**
* 注册成功
*
* @param phone 手机号
* @param password 密码
*/
void onSucceed(String phone, String password);
/**
* 取消注册
*/
default void onCancel() {}
}
}

View File

@ -0,0 +1,58 @@
package com.hjq.demo.ui.activity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import com.hjq.demo.R;
import com.hjq.demo.app.AppActivity;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/11/29
* desc : 重启应用
*/
public final class RestartActivity extends AppActivity {
public static void start(Context context) {
Intent intent = new Intent(context, RestartActivity.class);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
@Override
protected int getLayoutId() {
return 0;
}
@Override
protected void initView() {
}
@Override
protected void initData() {
restart(this);
finish();
toast(R.string.common_crash_hint);
}
public static void restart(Context context) {
Intent intent;
if (true) {
// 如果是未登录的情况下跳转到闪屏页
intent = new Intent(context, SplashActivity.class);
} else {
// 如果是已登录的情况下跳转到首页
intent = new Intent(context, HomeActivity.class);
}
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
}

View File

@ -6,12 +6,13 @@ import android.view.View;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.helper.ActivityStackManager;
import com.hjq.demo.helper.CacheDataManager;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.glide.GlideApp;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.LogoutApi;
import com.hjq.demo.manager.ActivityManager;
import com.hjq.demo.manager.ThreadPoolManager;
import com.hjq.demo.manager.CacheDataManager;
import com.hjq.demo.other.AppConfig;
import com.hjq.demo.ui.dialog.MenuDialog;
import com.hjq.demo.ui.dialog.SafeDialog;
@ -27,7 +28,7 @@ import com.hjq.widget.view.SwitchButton;
* time : 2019/03/01
* desc : 设置界面
*/
public final class SettingActivity extends MyActivity
public final class SettingActivity extends AppActivity
implements SwitchButton.OnCheckedChangeListener {
private SettingBar mLanguageView;
@ -69,95 +70,99 @@ public final class SettingActivity extends MyActivity
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.sb_setting_language:
// 底部选择框
new MenuDialog.Builder(this)
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setList(R.string.setting_language_simple, R.string.setting_language_complex)
.setListener((MenuDialog.OnListener<String>) (dialog, position, string) -> {
mLanguageView.setRightText(string);
BrowserActivity.start(getActivity(), "https://github.com/getActivity/MultiLanguages");
})
.setGravity(Gravity.BOTTOM)
.setAnimStyle(BaseDialog.ANIM_BOTTOM)
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.sb_setting_language) {
// 底部选择框
new MenuDialog.Builder(this)
// 设置点击按钮后不关闭对话框
//.setAutoDismiss(false)
.setList(R.string.setting_language_simple, R.string.setting_language_complex)
.setListener((MenuDialog.OnListener<String>) (dialog, position, string) -> {
mLanguageView.setRightText(string);
BrowserActivity.start(getActivity(), "https://github.com/getActivity/MultiLanguages");
})
.setGravity(Gravity.BOTTOM)
.setAnimStyle(BaseDialog.ANIM_BOTTOM)
.show();
} else if (viewId == R.id.sb_setting_update) {
// 本地的版本码和服务器的进行比较
if (20 > AppConfig.getVersionCode()) {
new UpdateDialog.Builder(this)
.setVersionName("2.0")
.setForceUpdate(false)
.setUpdateLog("修复Bug\n优化用户体验")
.setDownloadUrl("https://down.qq.com/qqweb/QQ_1/android_apk/Android_8.5.0.5025_537066738.apk")
.setFileMd5("560017dc94e8f9b65f4ca997c7feb326")
.show();
break;
case R.id.sb_setting_update:
// 本地的版本码和服务器的进行比较
if (20 > AppConfig.getVersionCode()) {
new UpdateDialog.Builder(this)
// 版本名
.setVersionName("2.0")
// 是否强制更新
.setForceUpdate(false)
// 更新日志
.setUpdateLog("修复Bug\n优化用户体验")
// 下载 url
.setDownloadUrl("https://raw.githubusercontent.com/getActivity/AndroidProject/master/AndroidProject.apk")
.show();
} else {
toast(R.string.update_no_update);
}
break;
case R.id.sb_setting_phone:
new SafeDialog.Builder(this)
.setListener((dialog, phone, code) -> PhoneResetActivity.start(getActivity(), code))
.show();
break;
case R.id.sb_setting_password:
new SafeDialog.Builder(this)
.setListener((dialog, phone, code) -> PasswordResetActivity.start(getActivity(), phone, code))
.show();
break;
case R.id.sb_setting_agreement:
BrowserActivity.start(this, "https://github.com/getActivity/Donate");
break;
case R.id.sb_setting_about:
startActivity(AboutActivity.class);
break;
case R.id.sb_setting_auto:
// 自动登录
mAutoSwitchView.setChecked(!mAutoSwitchView.isChecked());
break;
case R.id.sb_setting_cache:
// 清除内存缓存必须在主线程
GlideApp.get(getActivity()).clearMemory();
new Thread(() -> {
CacheDataManager.clearAllCache(this);
// 清除本地缓存必须在子线程
GlideApp.get(getActivity()).clearDiskCache();
post(() -> {
// 重新获取应用缓存大小
mCleanCacheView.setRightText(CacheDataManager.getTotalCacheSize(getActivity()));
} else {
toast(R.string.update_no_update);
}
} else if (viewId == R.id.sb_setting_phone) {
new SafeDialog.Builder(this)
.setListener((dialog, phone, code) -> PhoneResetActivity.start(getActivity(), code))
.show();
} else if (viewId == R.id.sb_setting_password) {
new SafeDialog.Builder(this)
.setListener((dialog, phone, code) -> PasswordResetActivity.start(getActivity(), phone, code))
.show();
} else if (viewId == R.id.sb_setting_agreement) {
BrowserActivity.start(this, "https://github.com/getActivity/AndroidProject");
} else if (viewId == R.id.sb_setting_about) {
startActivity(AboutActivity.class);
} else if (viewId == R.id.sb_setting_auto) {
// 自动登录
mAutoSwitchView.setChecked(!mAutoSwitchView.isChecked());
} else if (viewId == R.id.sb_setting_cache) {
// 清除内存缓存必须在主线程
GlideApp.get(getActivity()).clearMemory();
ThreadPoolManager.getInstance().execute(() -> {
CacheDataManager.clearAllCache(this);
// 清除本地缓存必须在子线程
GlideApp.get(getActivity()).clearDiskCache();
post(() -> {
// 重新获取应用缓存大小
mCleanCacheView.setRightText(CacheDataManager.getTotalCacheSize(getActivity()));
});
});
} else if (viewId == R.id.sb_setting_exit) {
if (true) {
startActivity(LoginActivity.class);
// 进行内存优化销毁除登录页之外的所有界面
ActivityManager.getInstance().finishAllActivities(LoginActivity.class);
return;
}
// 退出登录
EasyHttp.post(this)
.api(new LogoutApi())
.request(new HttpCallback<HttpData<Void>>(this) {
@Override
public void onSucceed(HttpData<Void> data) {
startActivity(LoginActivity.class);
// 进行内存优化销毁除登录页之外的所有界面
ActivityManager.getInstance().finishAllActivities(LoginActivity.class);
}
});
}).start();
break;
case R.id.sb_setting_exit:
if (true) {
startActivity(LoginActivity.class);
// 进行内存优化销毁除登录页之外的所有界面
ActivityStackManager.getInstance().finishAllActivities(LoginActivity.class);
return;
}
// 退出登录
EasyHttp.post(this)
.api(new LogoutApi())
.request(new HttpCallback<HttpData<Void>>(this) {
@Override
public void onSucceed(HttpData<Void> data) {
startActivity(LoginActivity.class);
// 进行内存优化销毁除登录页之外的所有界面
ActivityStackManager.getInstance().finishAllActivities(LoginActivity.class);
}
});
break;
default:
break;
}
}
@ -166,7 +171,7 @@ public final class SettingActivity extends MyActivity
*/
@Override
public void onCheckedChanged(SwitchButton button, boolean isChecked) {
toast(isChecked);
public void onCheckedChanged(SwitchButton button, boolean checked) {
toast(checked);
}
}

View File

@ -2,6 +2,7 @@ package com.hjq.demo.ui.activity;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Intent;
import android.view.View;
import androidx.annotation.NonNull;
@ -10,13 +11,14 @@ import com.airbnb.lottie.LottieAnimationView;
import com.gyf.immersionbar.BarHide;
import com.gyf.immersionbar.ImmersionBar;
import com.hjq.demo.R;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.http.model.HttpData;
import com.hjq.demo.http.request.UserInfoApi;
import com.hjq.demo.http.response.UserInfoBean;
import com.hjq.demo.other.AppConfig;
import com.hjq.http.EasyHttp;
import com.hjq.http.listener.HttpCallback;
import com.hjq.widget.view.SlantedTextView;
/**
* author : Android 轮子哥
@ -24,10 +26,10 @@ import com.hjq.http.listener.HttpCallback;
* time : 2018/10/18
* desc : 闪屏界面
*/
public final class SplashActivity extends MyActivity {
public final class SplashActivity extends AppActivity {
private LottieAnimationView mLottieView;
private View mDebugView;
private SlantedTextView mDebugView;
@Override
protected int getLayoutId() {
@ -36,14 +38,15 @@ public final class SplashActivity extends MyActivity {
@Override
protected void initView() {
mLottieView = findViewById(R.id.iv_splash_lottie);
mLottieView = findViewById(R.id.lav_splash_lottie);
mDebugView = findViewById(R.id.iv_splash_debug);
// 设置动画监听
mLottieView.addAnimatorListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
startActivity(HomeActivity.class);
mLottieView.removeAnimatorListener(this);
HomeActivity.start(getContext());
finish();
}
});
@ -51,6 +54,7 @@ public final class SplashActivity extends MyActivity {
@Override
protected void initData() {
mDebugView.setText(AppConfig.getBuildType().toUpperCase());
if (AppConfig.isDebug()) {
mDebugView.setVisibility(View.VISIBLE);
} else {
@ -91,9 +95,28 @@ public final class SplashActivity extends MyActivity {
return false;
}
@Override
protected void initActivity() {
// 问题及方案https://www.cnblogs.com/net168/p/5722752.html
// 如果当前 Activity 不是任务栈中的第一个 Activity
if (!isTaskRoot()) {
Intent intent = getIntent();
// 如果当前 Activity 是通过桌面图标启动进入的
if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)
&& Intent.ACTION_MAIN.equals(intent.getAction())) {
// 对当前 Activity 执行销毁操作避免重复实例化入口
finish();
return;
}
}
super.initActivity();
}
@Deprecated
@Override
protected void onDestroy() {
mLottieView.removeAllAnimatorListeners();
// 因为修复了一个启动页被重复启动的问题所以有可能 Activity 还没有初始化完成就已经销毁了
// 所以如果需要在此处释放对象资源需要先对这个对象进行判空否则可能会导致空指针异常
super.onDestroy();
}
}

View File

@ -4,9 +4,9 @@ import androidx.core.content.ContextCompat;
import com.hjq.demo.R;
import com.hjq.demo.action.StatusAction;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.ui.dialog.MenuDialog;
import com.hjq.demo.widget.HintLayout;
import com.hjq.demo.widget.StatusLayout;
/**
* author : Android 轮子哥
@ -14,10 +14,10 @@ import com.hjq.demo.widget.HintLayout;
* time : 2019/04/17
* desc : 加载使用案例
*/
public final class StatusActivity extends MyActivity
public final class StatusActivity extends AppActivity
implements StatusAction {
private HintLayout mHintLayout;
private StatusLayout mStatusLayout;
@Override
protected int getLayoutId() {
@ -26,7 +26,7 @@ public final class StatusActivity extends MyActivity
@Override
protected void initView() {
mHintLayout = findViewById(R.id.hl_status_hint);
mStatusLayout = findViewById(R.id.hl_status_hint);
}
@Override
@ -50,7 +50,7 @@ public final class StatusActivity extends MyActivity
showEmpty();
break;
case 3:
showLayout(ContextCompat.getDrawable(getActivity(), R.drawable.hint_order_ic), "暂无订单", null);
showLayout(ContextCompat.getDrawable(getActivity(), R.drawable.status_order_ic), "暂无订单", null);
break;
default:
break;
@ -60,7 +60,7 @@ public final class StatusActivity extends MyActivity
}
@Override
public HintLayout getHintLayout() {
return mHintLayout;
public StatusLayout getStatusLayout() {
return mStatusLayout;
}
}

View File

@ -1,8 +1,10 @@
package com.hjq.demo.ui.activity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
@ -10,8 +12,7 @@ import com.gyf.immersionbar.BarHide;
import com.gyf.immersionbar.ImmersionBar;
import com.hjq.demo.R;
import com.hjq.demo.action.SwipeAction;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.widget.PlayerView;
@ -23,28 +24,11 @@ import java.io.File;
* time : 2020/02/16
* desc : 视频播放界面
*/
public final class VideoPlayActivity extends MyActivity
implements SwipeAction, PlayerView.onGoBackListener {
public static void start(Context context, File file) {
if (file == null || !file.isFile()) {
return;
}
start(context, file.getPath(), file.getName());
}
@DebugLog
public static void start(Context context, String url, String title) {
if (TextUtils.isEmpty(url)) {
return;
}
Intent intent = new Intent(context, VideoPlayActivity.class);
intent.putExtra(IntentKey.VIDEO, url);
intent.putExtra(IntentKey.TITLE, title);
context.startActivity(intent);
}
public final class VideoPlayActivity extends AppActivity
implements SwipeAction, PlayerView.onPlayListener {
private PlayerView mPlayerView;
private VideoPlayActivity.Builder mBuilder;
@Override
protected int getLayoutId() {
@ -54,41 +38,48 @@ public final class VideoPlayActivity extends MyActivity
@Override
protected void initView() {
mPlayerView = findViewById(R.id.pv_video_play_view);
mPlayerView.setOnGoBackListener(this);
mPlayerView.setGestureEnabled(true);
mPlayerView.setLifecycleOwner(this);
mPlayerView.setOnPlayListener(this);
}
@Override
protected void initData() {
mPlayerView.setVideoTitle(getString(IntentKey.TITLE));
mPlayerView.setVideoSource(getString(IntentKey.VIDEO));
mPlayerView.start();
mBuilder = getParcelable(IntentKey.VIDEO);
if (mBuilder == null) {
throw new IllegalArgumentException("are you ok?");
}
mPlayerView.setVideoTitle(mBuilder.getVideoTitle());
mPlayerView.setVideoSource(mBuilder.getVideoSource());
mPlayerView.setGestureEnabled(mBuilder.isGestureEnabled());
if (mBuilder.isAutoPlay()) {
mPlayerView.start();
}
}
/**
* {@link PlayerView.onGoBackListener}
* {@link PlayerView.onPlayListener}
*/
@Override
public void onClickGoBack(PlayerView view) {
public void onClickBack(PlayerView view) {
onBackPressed();
}
@Override
protected void onResume() {
mPlayerView.onResume();
super.onResume();
public void onPlayStart(PlayerView view) {
int progress = mBuilder.getPlayProgress();
if (progress > 0) {
mPlayerView.setProgress(progress);
}
}
@Override
protected void onPause() {
mPlayerView.onPause();
super.onPause();
}
@Override
protected void onDestroy() {
mPlayerView.onDestroy();
super.onDestroy();
public void onPlayEnd(PlayerView view) {
if (mBuilder.isLoopPlay()) {
mPlayerView.setProgress(0);
mPlayerView.start();
} else if (mBuilder.isAutoOver()) {
finish();
}
}
@NonNull
@ -103,4 +94,146 @@ public final class VideoPlayActivity extends MyActivity
public boolean isSwipeEnable() {
return false;
}
/**
* 播放参数构建
*/
public static final class Builder implements Parcelable {
/** 视频源 */
private String mVideoSource;
/** 视频标题 */
private String mVideoTitle;
/** 播放进度 */
private int mPlayProgress;
/** 手势开关 */
private boolean mGestureEnabled = true;
/** 循环播放 */
private boolean mLoopPlay = false;
/** 自动播放 */
private boolean mAutoPlay = true;
/** 播放完关闭 */
private boolean mAutoOver = true;
public Builder() {}
protected Builder(Parcel in) {
mVideoSource = in.readString();
mVideoTitle = in.readString();
mPlayProgress = in.readInt();
mGestureEnabled = in.readByte() != 0;
mLoopPlay = in.readByte() != 0;
mAutoPlay = in.readByte() != 0;
mAutoOver = in.readByte() != 0;
}
public Builder setVideoSource(File file) {
mVideoSource = file.getPath();
if (mVideoTitle == null) {
mVideoTitle = file.getName();
}
return this;
}
public Builder setVideoSource(String url) {
mVideoSource = url;
return this;
}
private String getVideoSource() {
return mVideoSource;
}
public Builder setVideoTitle(String title) {
mVideoTitle = title;
return this;
}
private String getVideoTitle() {
return mVideoTitle;
}
public Builder setPlayProgress(int progress) {
mPlayProgress = progress;
return this;
}
private int getPlayProgress() {
return mPlayProgress;
}
public Builder setGestureEnabled(boolean enabled) {
mGestureEnabled = enabled;
return this;
}
private boolean isGestureEnabled() {
return mGestureEnabled;
}
public Builder setLoopPlay(boolean enabled) {
mLoopPlay = enabled;
return this;
}
private boolean isLoopPlay() {
return mLoopPlay;
}
public Builder setAutoPlay(boolean enabled) {
mAutoPlay = enabled;
return this;
}
public boolean isAutoPlay() {
return mAutoPlay;
}
public Builder setAutoOver(boolean enabled) {
mAutoOver = enabled;
return this;
}
private boolean isAutoOver() {
return mAutoOver;
}
public void start(Context context) {
Intent intent = new Intent(context, VideoPlayActivity.class);
intent.putExtra(IntentKey.VIDEO, this);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mVideoSource);
dest.writeString(mVideoTitle);
dest.writeInt(mPlayProgress);
dest.writeByte(mGestureEnabled ? (byte) 1 : (byte) 0);
dest.writeByte(mLoopPlay ? (byte) 1 : (byte) 0);
dest.writeByte(mAutoPlay ? (byte) 1 : (byte) 0);
dest.writeByte(mAutoOver ? (byte) 1 : (byte) 0);
}
public static final Parcelable.Creator<Builder> CREATOR = new Parcelable.Creator<Builder>() {
@Override
public Builder createFromParcel(Parcel source) {
return new Builder(source);
}
@Override
public Builder[] newArray(int size) {
return new Builder[size];
}
};
}
}

View File

@ -14,6 +14,7 @@ import android.view.View;
import android.view.animation.AnimationUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@ -24,18 +25,20 @@ import com.hjq.demo.action.StatusAction;
import com.hjq.demo.aop.DebugLog;
import com.hjq.demo.aop.Permissions;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyActivity;
import com.hjq.demo.app.AppActivity;
import com.hjq.demo.manager.ThreadPoolManager;
import com.hjq.demo.other.GridSpaceDecoration;
import com.hjq.demo.other.IntentKey;
import com.hjq.demo.ui.adapter.VideoSelectAdapter;
import com.hjq.demo.ui.dialog.AlbumDialog;
import com.hjq.demo.widget.HintLayout;
import com.hjq.demo.widget.StatusLayout;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@ -45,7 +48,7 @@ import java.util.Set;
* time : 2020/03/01
* desc : 选择视频
*/
public final class VideoSelectActivity extends MyActivity
public final class VideoSelectActivity extends AppActivity
implements StatusAction, Runnable,
BaseAdapter.OnItemClickListener,
BaseAdapter.OnItemLongClickListener,
@ -56,7 +59,7 @@ public final class VideoSelectActivity extends MyActivity
}
@DebugLog
@Permissions({Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE})
@Permissions({Permission.MANAGE_EXTERNAL_STORAGE})
public static void start(BaseActivity activity, int maxSelect, OnVideoSelectListener listener) {
if (maxSelect < 1) {
// 最少要选择一个视频
@ -70,15 +73,28 @@ public final class VideoSelectActivity extends MyActivity
return;
}
if (resultCode == RESULT_OK) {
listener.onSelected(data.getParcelableArrayListExtra(IntentKey.IMAGE));
} else {
ArrayList<VideoBean> list = data.getParcelableArrayListExtra(IntentKey.VIDEO);
if (list == null || list.isEmpty()) {
listener.onCancel();
return;
}
Iterator<VideoBean> iterator = list.iterator();
while (iterator.hasNext()) {
if (!new File(iterator.next().getVideoPath()).isFile()) {
iterator.remove();
}
}
if (resultCode == RESULT_OK && !list.isEmpty()) {
listener.onSelected(list);
return;
}
listener.onCancel();
});
}
private HintLayout mHintLayout;
private StatusLayout mStatusLayout;
private RecyclerView mRecyclerView;
private FloatingActionButton mFloatingView;
@ -94,6 +110,9 @@ public final class VideoSelectActivity extends MyActivity
/** 视频专辑 */
private final HashMap<String, List<VideoBean>> mAllAlbum = new HashMap<>();
/** 专辑选择对话框 */
private AlbumDialog.Builder mAlbumDialog;
@Override
protected int getLayoutId() {
return R.layout.video_select_activity;
@ -101,7 +120,7 @@ public final class VideoSelectActivity extends MyActivity
@Override
protected void initView() {
mHintLayout = findViewById(R.id.hl_video_select_hint);
mStatusLayout = findViewById(R.id.hl_video_select_hint);
mRecyclerView = findViewById(R.id.rv_video_select_list);
mFloatingView = findViewById(R.id.fab_video_select_floating);
setOnClickListener(mFloatingView);
@ -126,74 +145,83 @@ public final class VideoSelectActivity extends MyActivity
// 显示加载进度条
showLoading();
// 加载视频列表
new Thread(VideoSelectActivity.this).start();
ThreadPoolManager.getInstance().execute(VideoSelectActivity.this);
}
@Override
public HintLayout getHintLayout() {
return mHintLayout;
public StatusLayout getStatusLayout() {
return mStatusLayout;
}
@SingleClick
@Override
public void onRightClick(View v) {
public void onRightClick(View view) {
if (mAllVideo.isEmpty()) {
return;
}
ArrayList<AlbumDialog.AlbumInfo> data = new ArrayList<>(mAllAlbum.size() + 1);
data.add(new AlbumDialog.AlbumInfo(mAllVideo.get(0).getVideoPath(), getString(R.string.video_select_all), String.format(getString(R.string.video_select_total), mAllAlbum.size()), mAdapter.getData() == mAllVideo));
int count = 0;
Set<String> keys = mAllAlbum.keySet();
for (String key : keys) {
List<VideoBean> temp = mAllAlbum.get(key);
if (temp != null && !temp.isEmpty()) {
data.add(new AlbumDialog.AlbumInfo(temp.get(0).getVideoPath(), key, String.format(getString(R.string.video_select_total), temp.size()), mAdapter.getData() == temp));
List<VideoBean> list = mAllAlbum.get(key);
if (list != null && !list.isEmpty()) {
count += list.size();
data.add(new AlbumDialog.AlbumInfo(list.get(0).getVideoPath(), key, String.format(getString(R.string.video_select_total), list.size()), mAdapter.getData() == list));
}
}
data.add(0, new AlbumDialog.AlbumInfo(mAllVideo.get(0).getVideoPath(), getString(R.string.video_select_all), String.format(getString(R.string.video_select_total), count), mAdapter.getData() == mAllVideo));
new AlbumDialog.Builder(this)
.setData(data)
.setListener((dialog, position, bean) -> {
if (mAlbumDialog == null) {
mAlbumDialog = new AlbumDialog.Builder(this)
.setListener((dialog, position, bean) -> {
setRightTitle(bean.getName());
// 滚动回第一个位置
mRecyclerView.scrollToPosition(0);
if (position == 0) {
mAdapter.setData(mAllVideo);
} else {
mAdapter.setData(mAllAlbum.get(bean.getName()));
}
// 执行列表动画
mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.from_right_layout));
mRecyclerView.scheduleLayoutAnimation();
})
setRightTitle(bean.getName());
// 滚动回第一个位置
mRecyclerView.scrollToPosition(0);
if (position == 0) {
mAdapter.setData(mAllVideo);
} else {
mAdapter.setData(mAllAlbum.get(bean.getName()));
}
// 执行列表动画
mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.from_right_layout));
mRecyclerView.scheduleLayoutAnimation();
});
}
mAlbumDialog.setData(data)
.show();
}
@Override
protected void onRestart() {
super.onRestart();
Iterator<VideoBean> iterator = mSelectVideo.iterator();
// 遍历判断选择了的视频是否被删除了
for (VideoBean bean : mSelectVideo) {
while (iterator.hasNext()) {
VideoBean bean = iterator.next();
File file = new File(bean.getVideoPath());
if (!file.isFile()) {
if (file.isFile()) {
continue;
}
mSelectVideo.remove(bean);
mAllVideo.remove(bean);
iterator.remove();
mAllVideo.remove(bean);
File parentFile = file.getParentFile();
if (parentFile != null) {
List<VideoBean> data = mAllAlbum.get(parentFile.getName());
if (data != null) {
data.remove(bean);
}
mAdapter.notifyDataSetChanged();
File parentFile = file.getParentFile();
if (parentFile != null) {
List<VideoBean> data = mAllAlbum.get(parentFile.getName());
if (data != null) {
data.remove(bean);
}
mAdapter.notifyDataSetChanged();
if (mSelectVideo.isEmpty()) {
mFloatingView.setImageResource(R.drawable.videocam_ic);
} else {
mFloatingView.setImageResource(R.drawable.succeed_ic);
}
if (mSelectVideo.isEmpty()) {
mFloatingView.setImageResource(R.drawable.videocam_ic);
} else {
mFloatingView.setImageResource(R.drawable.succeed_ic);
}
}
}
@ -201,28 +229,29 @@ public final class VideoSelectActivity extends MyActivity
@SingleClick
@Override
public void onClick(View v) {
if (v.getId() == R.id.fab_video_select_floating) {
public void onClick(View view) {
if (view.getId() == R.id.fab_video_select_floating) {
if (mSelectVideo.isEmpty()) {
// 点击拍照
CameraActivity.start(this, true, file -> {
// 当前选中视频的数量必须小于最大选中数
if (mSelectVideo.size() < mMaxSelect) {
mSelectVideo.add(VideoBean.getInstance(file.getPath()));
mSelectVideo.add(VideoBean.newInstance(file.getPath()));
}
// 这里需要延迟刷新否则可能会找不到拍照的视频
postDelayed(() -> {
// 重新加载视频列表
new Thread(VideoSelectActivity.this).start();
ThreadPoolManager.getInstance().execute(VideoSelectActivity.this);
}, 1000);
});
} else {
// 完成选择
setResult(RESULT_OK, new Intent().putParcelableArrayListExtra(IntentKey.IMAGE, mSelectVideo));
finish();
return;
}
// 完成选择
setResult(RESULT_OK, new Intent().putParcelableArrayListExtra(IntentKey.VIDEO, mSelectVideo));
finish();
}
}
@ -234,7 +263,9 @@ public final class VideoSelectActivity extends MyActivity
*/
@Override
public void onItemClick(RecyclerView recyclerView, View itemView, int position) {
VideoPlayActivity.start(getActivity(), new File(mAdapter.getItem(position).getVideoPath()));
new VideoPlayActivity.Builder()
.setVideoSource(new File(mAdapter.getItem(position).getVideoPath()))
.start(getActivity());
}
/**
@ -248,9 +279,8 @@ public final class VideoSelectActivity extends MyActivity
if (mSelectVideo.size() < mMaxSelect) {
// 长按的时候模拟选中
return itemView.findViewById(R.id.fl_video_select_check).performClick();
} else {
return false;
}
return false;
}
/**
@ -262,8 +292,17 @@ public final class VideoSelectActivity extends MyActivity
@Override
public void onChildClick(RecyclerView recyclerView, View childView, int position) {
if (childView.getId() == R.id.fl_video_select_check) {
if (mSelectVideo.contains(mAdapter.getItem(position))) {
mSelectVideo.remove(mAdapter.getItem(position));
VideoBean bean = mAdapter.getItem(position);
File file = new File(bean.getVideoPath());
if (!file.isFile()) {
mAdapter.removeItem(position);
toast(R.string.video_select_error);
return;
}
if (mSelectVideo.contains(bean)) {
mSelectVideo.remove(bean);
if (mSelectVideo.isEmpty()) {
mFloatingView.hide();
@ -273,34 +312,34 @@ public final class VideoSelectActivity extends MyActivity
}, 200);
}
} else {
mAdapter.notifyItemChanged(position);
return;
}
if (mMaxSelect == 1 && mSelectVideo.size() == 1) {
if (mMaxSelect == 1 && mSelectVideo.size() == 1) {
List<VideoBean> data = mAdapter.getData();
if (data != null) {
int index = data.indexOf(mSelectVideo.get(0));
if (index != -1) {
mSelectVideo.remove(0);
mAdapter.notifyItemChanged(index);
}
List<VideoBean> data = mAdapter.getData();
if (data != null) {
int index = data.indexOf(mSelectVideo.remove(0));
if (index != -1) {
mAdapter.notifyItemChanged(index);
}
mSelectVideo.add(mAdapter.getItem(position));
} else if (mSelectVideo.size() < mMaxSelect) {
mSelectVideo.add(mAdapter.getItem(position));
if (mSelectVideo.size() == 1) {
mFloatingView.hide();
postDelayed(() -> {
mFloatingView.setImageResource(R.drawable.succeed_ic);
mFloatingView.show();
}, 200);
}
} else {
toast(String.format(getString(R.string.video_select_max_hint), mMaxSelect));
}
mSelectVideo.add(bean);
} else if (mSelectVideo.size() < mMaxSelect) {
mSelectVideo.add(bean);
if (mSelectVideo.size() == 1) {
mFloatingView.hide();
postDelayed(() -> {
mFloatingView.setImageResource(R.drawable.succeed_ic);
mFloatingView.show();
}, 200);
}
} else {
toast(String.format(getString(R.string.video_select_max_hint), mMaxSelect));
}
mAdapter.notifyItemChanged(position);
}
@ -322,7 +361,7 @@ public final class VideoSelectActivity extends MyActivity
MediaStore.MediaColumns.HEIGHT, MediaStore.MediaColumns.SIZE, MediaStore.Video.Media.DURATION};
Cursor cursor = null;
if (XXPermissions.hasPermission(this, Permission.READ_EXTERNAL_STORAGE)) {
if (XXPermissions.isGrantedPermission(this, Permission.MANAGE_EXTERNAL_STORAGE)) {
cursor = contentResolver.query(contentUri, projections, selection, new String[]{String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)}, sortOrder);
}
if (cursor != null && cursor.moveToFirst()) {
@ -330,18 +369,18 @@ public final class VideoSelectActivity extends MyActivity
int pathIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
int mimeTypeIndex = cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE);
int sizeIndex = cursor.getColumnIndex(MediaStore.MediaColumns.SIZE);
int durationIndex = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
int durationIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DURATION);
do {
long duration = cursor.getLong(durationIndex);
// 视频时长不得小于 2
if (duration < 1000 * 2) {
// 视频时长不得小于 1
if (duration < 1000) {
continue;
}
long size = cursor.getLong(sizeIndex);
// 视频大小不得小于 100 KB
if (size < 1024 * 100) {
// 视频大小不得小于 10 KB
if (size < 1024 * 10) {
continue;
}
@ -392,15 +431,16 @@ public final class VideoSelectActivity extends MyActivity
mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.fall_down_layout));
mRecyclerView.scheduleLayoutAnimation();
// 设置右标提
setRightTitle(R.string.video_select_all);
if (mAllVideo.isEmpty()) {
// 显示空布局
showEmpty();
// 设置右标题
setRightTitle(null);
} else {
// 显示加载完成
showComplete();
// 设置右标题
setRightTitle(R.string.video_select_all);
}
}, 500);
}
@ -414,7 +454,7 @@ public final class VideoSelectActivity extends MyActivity
private final long duration;
private final long size;
public static VideoBean getInstance(String videoPath) {
public static VideoBean newInstance(String videoPath) {
int duration = 0;
try {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
@ -449,6 +489,14 @@ public final class VideoSelectActivity extends MyActivity
return size;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof VideoBean) {
return videoPath.equals(((VideoBean) obj).videoPath);
}
return false;
}
@NonNull
@Override
public String toString() {

View File

@ -6,7 +6,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.hjq.demo.R;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
/**
* author : Android 轮子哥
@ -14,7 +14,7 @@ import com.hjq.demo.common.MyAdapter;
* time : 2018/11/05
* desc : 可进行拷贝的副本
*/
public final class CopyAdapter extends MyAdapter<String> {
public final class CopyAdapter extends AppAdapter<String> {
public CopyAdapter(Context context) {
super(context);
@ -31,7 +31,7 @@ public final class CopyAdapter extends MyAdapter<String> {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private ViewHolder() {
super(R.layout.copy_item);

View File

@ -0,0 +1,44 @@
package com.hjq.demo.ui.adapter;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import com.hjq.demo.R;
import com.hjq.demo.app.AppAdapter;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/08/28
* desc : 引导页适配器
*/
public final class GuideAdapter extends AppAdapter<Integer> {
public GuideAdapter(Context context) {
super(context);
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder();
}
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final ImageView mImageView;
private ViewHolder() {
super(R.layout.guide_item);
mImageView = (ImageView) getItemView();
}
@Override
public void onBindView(int position) {
mImageView.setImageResource(getItem(position));
}
}
}

View File

@ -0,0 +1,47 @@
package com.hjq.demo.ui.adapter;
import android.content.Context;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.github.chrisbanes.photoview.PhotoView;
import com.hjq.demo.R;
import com.hjq.demo.app.AppAdapter;
import com.hjq.demo.http.glide.GlideApp;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2020/08/28
* desc : 图片预览适配器
*/
public final class ImagePreviewAdapter extends AppAdapter<String> {
public ImagePreviewAdapter(Context context) {
super(context);
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder();
}
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final PhotoView mPhotoView;
private ViewHolder() {
super(R.layout.image_preview_item);
mPhotoView = (PhotoView) getItemView();
}
@Override
public void onBindView(int position) {
GlideApp.with(getContext())
.load(getItem(position))
.into(mPhotoView);
}
}
}

View File

@ -10,7 +10,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.hjq.demo.R;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
import com.hjq.demo.http.glide.GlideApp;
import java.util.List;
@ -21,7 +21,7 @@ import java.util.List;
* time : 2019/07/24
* desc : 图片选择适配器
*/
public final class ImageSelectAdapter extends MyAdapter<String> {
public final class ImageSelectAdapter extends AppAdapter<String> {
private final List<String> mSelectImages;
@ -36,21 +36,22 @@ public final class ImageSelectAdapter extends MyAdapter<String> {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private ImageView mImageView;
private CheckBox mCheckBox;
private final ImageView mImageView;
private final CheckBox mCheckBox;
private ViewHolder() {
super(R.layout.image_select_item);
mImageView = (ImageView) findViewById(R.id.iv_image_select_image);
mCheckBox = (CheckBox) findViewById(R.id.iv_image_select_check);
mImageView = findViewById(R.id.iv_image_select_image);
mCheckBox = findViewById(R.id.iv_image_select_check);
}
@Override
public void onBindView(int position) {
String imagePath = getItem(position);
GlideApp.with(getContext())
.asBitmap()
.load(imagePath)
.into(mImageView);

View File

@ -7,7 +7,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import com.hjq.demo.R;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
/**
* author : Android 轮子哥
@ -15,7 +15,7 @@ import com.hjq.demo.common.MyAdapter;
* time : 2019/09/22
* desc : 状态数据列表
*/
public final class StatusAdapter extends MyAdapter<String> {
public final class StatusAdapter extends AppAdapter<String> {
public StatusAdapter(Context context) {
super(context);
@ -27,13 +27,13 @@ public final class StatusAdapter extends MyAdapter<String> {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private TextView mTextView;
private final TextView mTextView;
private ViewHolder() {
super(R.layout.status_item);
mTextView = (TextView) findViewById(R.id.tv_status_text);
mTextView = findViewById(R.id.tv_status_text);
}
@Override

View File

@ -11,9 +11,9 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.hjq.demo.R;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.helper.CacheDataManager;
import com.hjq.demo.app.AppAdapter;
import com.hjq.demo.http.glide.GlideApp;
import com.hjq.demo.manager.CacheDataManager;
import com.hjq.demo.ui.activity.VideoSelectActivity;
import com.hjq.demo.widget.PlayerView;
@ -25,7 +25,7 @@ import java.util.List;
* time : 2020/03/01
* desc : 视频选择适配器
*/
public final class VideoSelectAdapter extends MyAdapter<VideoSelectActivity.VideoBean> {
public final class VideoSelectAdapter extends AppAdapter<VideoSelectActivity.VideoBean> {
private final List<VideoSelectActivity.VideoBean> mSelectVideo;
@ -40,19 +40,19 @@ public final class VideoSelectAdapter extends MyAdapter<VideoSelectActivity.Vide
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private ImageView mImageView;
private CheckBox mCheckBox;
private TextView mDurationView;
private TextView mSizeView;
private final ImageView mImageView;
private final CheckBox mCheckBox;
private final TextView mDurationView;
private final TextView mSizeView;
private ViewHolder() {
super(R.layout.video_select_item);
mImageView = (ImageView) findViewById(R.id.iv_video_select_image);
mCheckBox = (CheckBox) findViewById(R.id.iv_video_select_check);
mDurationView = (TextView) findViewById(R.id.tv_video_select_duration);
mSizeView = (TextView) findViewById(R.id.tv_video_select_size);
mImageView = findViewById(R.id.iv_video_select_image);
mCheckBox = findViewById(R.id.iv_video_select_check);
mDurationView = findViewById(R.id.tv_video_select_duration);
mSizeView = findViewById(R.id.tv_video_select_size);
}
@Override
@ -67,7 +67,7 @@ public final class VideoSelectAdapter extends MyAdapter<VideoSelectActivity.Vide
// 获取视频的总时长
mDurationView.setText(PlayerView.conversionTime((int) bean.getVideoDuration()));
// 获取视频的总大小
// 获取视频文件大小
mSizeView.setText(CacheDataManager.getFormatSize(bean.getVideoSize()));
}
}

View File

@ -2,12 +2,10 @@ package com.hjq.demo.ui.dialog;
import android.content.Context;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
@ -20,7 +18,7 @@ import com.google.android.material.tabs.TabLayout;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
import org.json.JSONArray;
import org.json.JSONException;
@ -71,12 +69,9 @@ public final class AddressDialog {
public Builder(Context context) {
super(context);
setContentView(R.layout.address_dialog);
setHeight(getResources().getDisplayMetrics().heightPixels / 2);
DisplayMetrics displayMetrics = new DisplayMetrics();
getSystemService(WindowManager.class).getDefaultDisplay().getMetrics(displayMetrics);
setHeight(displayMetrics.heightPixels / 2);
mViewPager = findViewById(R.id.vp_address_province);
mViewPager = findViewById(R.id.vp_address_pager);
mAdapter = new RecyclerViewAdapter(context);
mAdapter.setOnSelectListener(this);
mViewPager.setAdapter(mAdapter);
@ -134,7 +129,7 @@ public final class AddressDialog {
if (data != null && !data.isEmpty()) {
for (int i = 0; i < data.size(); i++) {
if (province.equals(data.get(i).getName())) {
onSelected(0, i);
selectedAddress(0, i, false);
break;
}
}
@ -158,7 +153,7 @@ public final class AddressDialog {
if (city.equals(data.get(i).getName())) {
// 避开直辖市因为选择省的时候已经自动跳过市区了
if (mAdapter.getItem(1).size() > 1) {
onSelected(1, i);
selectedAddress(1, i, false);
}
break;
}
@ -188,28 +183,38 @@ public final class AddressDialog {
/**
* {@link RecyclerViewAdapter.OnSelectListener}
*/
@SuppressWarnings("all")
@Override
public void onSelected(int recyclerViewPosition, int clickItemPosition) {
switch (recyclerViewPosition) {
selectedAddress(recyclerViewPosition, clickItemPosition, true);
}
/**
* 选择地区
*
* @param type 类型
* @param position 点击的位置
* @param smoothScroll 是否需要平滑滚动
*/
@SuppressWarnings("all")
private void selectedAddress(int type, int position, boolean smoothScroll) {
switch (type) {
case 0:
// 记录当前选择的省份
mProvince = mAdapter.getItem(recyclerViewPosition).get(clickItemPosition).getName();
mProvince = mAdapter.getItem(type).get(position).getName();
mTabLayout.getTabAt(mTabLayout.getSelectedTabPosition()).setText(mProvince);
mTabLayout.addTab(mTabLayout.newTab().setText(getString(R.string.address_hint)), true);
mAdapter.addItem(AddressManager.getCityList(mAdapter.getItem(recyclerViewPosition).get(clickItemPosition).getNext()));
mViewPager.setCurrentItem(recyclerViewPosition + 1);
mAdapter.addItem(AddressManager.getCityList(mAdapter.getItem(type).get(position).getNext()));
mViewPager.setCurrentItem(type + 1, smoothScroll);
// 如果当前选择的是直辖市就直接跳过选择城市直接选择区域
if (mAdapter.getItem(recyclerViewPosition + 1).size() == 1) {
onSelected(recyclerViewPosition + 1, 0);
if (mAdapter.getItem(type + 1).size() == 1) {
selectedAddress(type + 1, 0, false);
}
break;
case 1:
// 记录当前选择的城市
mCity = mAdapter.getItem(recyclerViewPosition).get(clickItemPosition).getName();
mCity = mAdapter.getItem(type).get(position).getName();
mTabLayout.getTabAt(mTabLayout.getSelectedTabPosition()).setText(mCity);
if (mIgnoreArea) {
@ -223,14 +228,14 @@ public final class AddressDialog {
} else {
mTabLayout.addTab(mTabLayout.newTab().setText(getString(R.string.address_hint)), true);
mAdapter.addItem(AddressManager.getAreaList(mAdapter.getItem(recyclerViewPosition).get(clickItemPosition).getNext()));
mViewPager.setCurrentItem(recyclerViewPosition + 1);
mAdapter.addItem(AddressManager.getAreaList(mAdapter.getItem(type).get(position).getNext()));
mViewPager.setCurrentItem(type + 1, smoothScroll);
}
break;
case 2:
// 记录当前选择的区域
mArea = mAdapter.getItem(recyclerViewPosition).get(clickItemPosition).getName();
mArea = mAdapter.getItem(type).get(position).getName();
mTabLayout.getTabAt(mTabLayout.getSelectedTabPosition()).setText(mArea);
if (mListener != null) {
@ -254,8 +259,8 @@ public final class AddressDialog {
@SingleClick
@Override
public void onClick(View v) {
if (v == mCloseView) {
public void onClick(View view) {
if (view == mCloseView) {
dismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
@ -330,7 +335,7 @@ public final class AddressDialog {
}
}
private final static class RecyclerViewAdapter extends MyAdapter<List<AddressBean>> {
private final static class RecyclerViewAdapter extends AppAdapter<List<AddressBean>> {
private OnSelectListener mListener;
@ -344,13 +349,14 @@ public final class AddressDialog {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder implements OnItemClickListener {
private final class ViewHolder extends AppAdapter<?>.ViewHolder implements OnItemClickListener {
private final AddressAdapter mAdapter;
ViewHolder() {
super(new RecyclerView(getContext()));
RecyclerView recyclerView = (RecyclerView) getItemView();
recyclerView.setNestedScrollingEnabled(true);
recyclerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
mAdapter = new AddressAdapter(getContext());
mAdapter.setOnItemClickListener(this);
@ -380,7 +386,7 @@ public final class AddressDialog {
}
}
private static final class AddressAdapter extends MyAdapter<AddressBean> {
private static final class AddressAdapter extends AppAdapter<AddressBean> {
private AddressAdapter(Context context) {
super(context);
@ -402,7 +408,7 @@ public final class AddressDialog {
return new ViewHolder(textView);
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final TextView mTextView;

View File

@ -12,8 +12,9 @@ import androidx.recyclerview.widget.RecyclerView;
import com.hjq.base.BaseAdapter;
import com.hjq.base.BaseDialog;
import com.hjq.base.BottomSheetDialog;
import com.hjq.demo.R;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
import com.hjq.demo.http.glide.GlideApp;
import java.util.List;
@ -39,7 +40,6 @@ public final class AlbumDialog {
super(context);
setContentView(R.layout.album_dialog);
setHeight(getResources().getDisplayMetrics().heightPixels / 2);
mRecyclerView = findViewById(R.id.rv_album_list);
mAdapter = new AlbumAdapter(context);
@ -53,6 +53,7 @@ public final class AlbumDialog {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).isSelect()) {
mRecyclerView.scrollToPosition(i);
break;
}
}
return this;
@ -89,9 +90,17 @@ public final class AlbumDialog {
}, 300);
}
@NonNull
@Override
protected BaseDialog createDialog(Context context, int themeId) {
BottomSheetDialog dialog = new BottomSheetDialog(context, themeId);
dialog.getBottomSheetBehavior().setPeekHeight(getResources().getDisplayMetrics().heightPixels / 2);
return dialog;
}
}
private static final class AlbumAdapter extends MyAdapter<AlbumInfo> {
private static final class AlbumAdapter extends AppAdapter<AlbumInfo> {
private AlbumAdapter(Context context) {
super(context);
@ -103,7 +112,7 @@ public final class AlbumDialog {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final ImageView mIconView;
private final TextView mNameView;
@ -112,10 +121,10 @@ public final class AlbumDialog {
private ViewHolder() {
super(R.layout.album_item);
mIconView = (ImageView) findViewById(R.id.iv_album_icon);
mNameView = (TextView) findViewById(R.id.tv_album_name);
mRemarkView = (TextView) findViewById(R.id.tv_album_remark);
mCheckBox = (CheckBox) findViewById(R.id.rb_album_check);
mIconView = findViewById(R.id.iv_album_icon);
mNameView = findViewById(R.id.tv_album_name);
mRemarkView = findViewById(R.id.tv_album_remark);
mCheckBox = findViewById(R.id.rb_album_check);
}
@Override
@ -123,6 +132,7 @@ public final class AlbumDialog {
AlbumInfo info = getItem(position);
GlideApp.with(getContext())
.asBitmap()
.load(info.getIcon())
.into(mIconView);

View File

@ -19,10 +19,10 @@ import com.hjq.demo.R;
* time : 2019/09/21
* desc : 项目通用 Dialog 布局封装
*/
public final class UIDialog {
public final class CommonDialog {
@SuppressWarnings("unchecked")
public static class Builder<B extends UIDialog.Builder>
public static class Builder<B extends CommonDialog.Builder<?>>
extends BaseDialog.Builder<B> {
private boolean mAutoDismiss = true;

View File

@ -11,8 +11,8 @@ import androidx.recyclerview.widget.RecyclerView;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.other.PickerLayoutManager;
import com.hjq.demo.app.AppAdapter;
import com.hjq.demo.manager.PickerLayoutManager;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -29,11 +29,10 @@ import java.util.Locale;
public final class DateDialog {
public static final class Builder
extends UIDialog.Builder<Builder>
extends CommonDialog.Builder<Builder>
implements PickerLayoutManager.OnPickerListener {
private final int mStartYear = Calendar.getInstance().get(Calendar.YEAR) - 100;
private final int mEndYear = Calendar.getInstance().get(Calendar.YEAR);
private final int mStartYear;
private final RecyclerView mYearView;
private final RecyclerView mMonthView;
@ -50,7 +49,16 @@ public final class DateDialog {
private OnListener mListener;
public Builder(Context context) {
this(context, Calendar.getInstance(Locale.CHINA).get(Calendar.YEAR) - 100);
}
public Builder(Context context, int startYear) {
this(context, startYear, Calendar.getInstance(Locale.CHINA).get(Calendar.YEAR));
}
public Builder(Context context, int startYear, int endYear) {
super(context);
mStartYear = startYear;
setCustomView(R.layout.date_dialog);
setTitle(R.string.time_title);
@ -65,7 +73,7 @@ public final class DateDialog {
// 生产年份
ArrayList<String> yearData = new ArrayList<>(10);
for (int i = mStartYear; i <= mEndYear; i++) {
for (int i = mStartYear; i <= endYear; i++) {
yearData.add(i + " " + getString(R.string.common_year));
}
@ -132,13 +140,13 @@ public final class DateDialog {
}
public Builder setDate(String date) {
// 20190519
if (date.matches("\\d{8}")) {
// 20190519
setYear(date.substring(0, 4));
setMonth(date.substring(4, 6));
setDay(date.substring(6, 8));
// 2019-05-19
} else if (date.matches("\\d{4}-\\d{2}-\\d{2}")) {
// 2019-05-19
setYear(date.substring(0, 4));
setMonth(date.substring(5, 7));
setDay(date.substring(8, 10));
@ -158,6 +166,7 @@ public final class DateDialog {
index = mYearAdapter.getItemCount() - 1;
}
mYearView.scrollToPosition(index);
onPicked(mYearView, index);
return this;
}
@ -173,6 +182,7 @@ public final class DateDialog {
index = mMonthAdapter.getItemCount() - 1;
}
mMonthView.scrollToPosition(index);
onPicked(mMonthView, index);
return this;
}
@ -188,6 +198,7 @@ public final class DateDialog {
index = mDayAdapter.getItemCount() - 1;
}
mDayView.scrollToPosition(index);
onPicked(mDayView, index);
return this;
}
@ -201,11 +212,7 @@ public final class DateDialog {
public void onPicked(RecyclerView recyclerView, int position) {
// 获取这个月最多有多少天
Calendar calendar = Calendar.getInstance(Locale.CHINA);
if (recyclerView == mYearView) {
calendar.set(mStartYear + position, mMonthManager.getPickedPosition(), 1);
} else if (recyclerView == mMonthView) {
calendar.set(mStartYear + mYearManager.getPickedPosition(), position, 1);
}
calendar.set(mStartYear + mYearManager.getPickedPosition(), mMonthManager.getPickedPosition(), 1);
int day = calendar.getActualMaximum(Calendar.DATE);
if (mDayAdapter.getItemCount() != day) {
@ -219,26 +226,22 @@ public final class DateDialog {
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_ui_confirm:
autoDismiss();
if (mListener != null) {
mListener.onSelected(getDialog(), mStartYear + mYearManager.getPickedPosition(), mMonthManager.getPickedPosition() + 1, mDayManager.getPickedPosition() + 1);
}
break;
case R.id.tv_ui_cancel:
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
break;
default:
break;
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.tv_ui_confirm) {
autoDismiss();
if (mListener != null) {
mListener.onSelected(getDialog(), mStartYear + mYearManager.getPickedPosition(), mMonthManager.getPickedPosition() + 1, mDayManager.getPickedPosition() + 1);
}
} else if (viewId == R.id.tv_ui_cancel) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
}
}
private static final class PickerAdapter extends MyAdapter<String> {
private static final class PickerAdapter extends AppAdapter<String> {
private PickerAdapter(Context context) {
super(context);
@ -250,7 +253,7 @@ public final class DateDialog {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final TextView mPickerView;

View File

@ -40,7 +40,7 @@ public final class HintDialog {
setCancelable(false);
mMessageView = findViewById(R.id.tv_hint_message);
mIconView = findViewById(R.id.iv_hint_icon);
mIconView = findViewById(R.id.iv_status_icon);
addOnShowListener(this);
}

View File

@ -1,9 +1,11 @@
package com.hjq.demo.ui.dialog;
import android.content.Context;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.StringRes;
@ -20,8 +22,9 @@ import com.hjq.demo.aop.SingleClick;
public final class InputDialog {
public static final class Builder
extends UIDialog.Builder<Builder>
implements BaseDialog.OnShowListener {
extends CommonDialog.Builder<Builder>
implements BaseDialog.OnShowListener,
TextView.OnEditorActionListener {
private OnListener mListener;
private final EditText mInputView;
@ -31,6 +34,7 @@ public final class InputDialog {
setCustomView(R.layout.input_dialog);
mInputView = findViewById(R.id.tv_input_message);
mInputView.setOnEditorActionListener(this);
addOnShowListener(this);
}
@ -66,29 +70,38 @@ public final class InputDialog {
*/
@Override
public void onShow(BaseDialog dialog) {
postDelayed(() -> getSystemService(InputMethodManager.class).showSoftInput(mInputView, 0), 500);
postDelayed(() -> showKeyboard(mInputView), 500);
}
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_ui_confirm:
autoDismiss();
if (mListener != null) {
mListener.onConfirm(getDialog(), mInputView.getText().toString());
}
break;
case R.id.tv_ui_cancel:
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
break;
default:
break;
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.tv_ui_confirm) {
autoDismiss();
if (mListener != null) {
mListener.onConfirm(getDialog(), mInputView.getText().toString());
}
} else if (viewId == R.id.tv_ui_cancel) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
}
}
/**
* {@link TextView.OnEditorActionListener}
*/
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
// 模拟点击确认按钮
onClick(findViewById(R.id.tv_ui_confirm));
return true;
}
return false;
}
}
public interface OnListener {

View File

@ -1,11 +1,11 @@
package com.hjq.demo.ui.dialog;
import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
@ -16,7 +16,7 @@ import com.hjq.base.BaseAdapter;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
import java.util.ArrayList;
import java.util.Arrays;
@ -35,6 +35,7 @@ public final class MenuDialog {
implements BaseAdapter.OnItemClickListener,
View.OnLayoutChangeListener, Runnable {
@SuppressWarnings("rawtypes")
private OnListener mListener;
private boolean mAutoDismiss = true;
@ -107,6 +108,7 @@ public final class MenuDialog {
return this;
}
@SuppressWarnings("rawtypes")
public Builder setListener(OnListener listener) {
mListener = listener;
return this;
@ -114,12 +116,12 @@ public final class MenuDialog {
@SingleClick
@Override
public void onClick(View v) {
public void onClick(View view) {
if (mAutoDismiss) {
dismiss();
}
if (v == mCancelView) {
if (view == mCancelView) {
if (mListener != null) {
mListener.onCancel(getDialog());
}
@ -172,14 +174,13 @@ public final class MenuDialog {
* 获取屏幕的高度
*/
private int getScreenHeight() {
WindowManager manager = getSystemService(WindowManager.class);
DisplayMetrics outMetrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(outMetrics);
Resources resources = getResources();
DisplayMetrics outMetrics = resources.getDisplayMetrics();
return outMetrics.heightPixels;
}
}
private static final class MenuAdapter extends MyAdapter<Object> {
private static final class MenuAdapter extends AppAdapter<Object> {
private MenuAdapter(Context context) {
super(context);
@ -191,7 +192,7 @@ public final class MenuDialog {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final TextView mTextView;
private final View mLineView;

View File

@ -19,7 +19,7 @@ import com.hjq.demo.aop.SingleClick;
public final class MessageDialog {
public static final class Builder
extends UIDialog.Builder<Builder> {
extends CommonDialog.Builder<Builder> {
private OnListener mListener;
@ -55,22 +55,18 @@ public final class MessageDialog {
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_ui_confirm:
autoDismiss();
if (mListener != null) {
mListener.onConfirm(getDialog());
}
break;
case R.id.tv_ui_cancel:
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
break;
default:
break;
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.tv_ui_confirm) {
autoDismiss();
if (mListener != null) {
mListener.onConfirm(getDialog());
}
} else if (viewId == R.id.tv_ui_cancel) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
}
}
}

View File

@ -15,7 +15,7 @@ import com.hjq.base.BaseAdapter;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
import com.hjq.demo.widget.PasswordView;
import java.util.Arrays;
@ -130,21 +130,19 @@ public final class PayPasswordDialog {
// 判断密码是否已经输入完毕
if (mRecordList.size() == PasswordView.PASSWORD_COUNT) {
if (mListener != null) {
postDelayed(() -> {
if (mAutoDismiss) {
dismiss();
}
// 获取输入的支付密码
StringBuilder password = new StringBuilder();
for (String s : mRecordList) {
password.append(s);
}
postDelayed(() -> {
if (mAutoDismiss) {
dismiss();
}
// 获取输入的支付密码
StringBuilder password = new StringBuilder();
for (String s : mRecordList) {
password.append(s);
}
if (mListener != null) {
mListener.onCompleted(getDialog(), password.toString());
}, 300);
}
}
}, 300);
}
break;
}
@ -153,8 +151,8 @@ public final class PayPasswordDialog {
@SingleClick
@Override
public void onClick(View v) {
if (v == mCloseView) {
public void onClick(View view) {
if (view == mCloseView) {
if (mAutoDismiss) {
dismiss();
}
@ -166,7 +164,7 @@ public final class PayPasswordDialog {
}
}
private static final class KeyboardAdapter extends MyAdapter<String> {
private static final class KeyboardAdapter extends AppAdapter<String> {
/** 数字按钮条目 */
private static final int TYPE_NORMAL = 0;
@ -193,18 +191,18 @@ public final class PayPasswordDialog {
@NonNull
@Override
public MyAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
public AppAdapter<?>.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_DELETE:
return new MyAdapter.SimpleHolder(R.layout.pay_password_delete_item);
return new AppAdapter<?>.SimpleHolder(R.layout.pay_password_delete_item);
case TYPE_EMPTY:
return new MyAdapter.SimpleHolder(R.layout.pay_password_empty_item);
return new AppAdapter<?>.SimpleHolder(R.layout.pay_password_empty_item);
default:
return new KeyboardAdapter.ViewHolder();
}
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final TextView mTextView;

View File

@ -25,7 +25,7 @@ import com.hjq.widget.view.CountdownView;
public final class SafeDialog {
public static final class Builder
extends UIDialog.Builder<Builder> {
extends CommonDialog.Builder<Builder> {
private final TextView mPhoneView;
private final EditText mCodeView;
@ -62,78 +62,73 @@ public final class SafeDialog {
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.cv_safe_countdown:
if (true) {
ToastUtils.show(R.string.common_code_send_hint);
mCountdownView.start();
setCancelable(false);
return;
}
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.cv_safe_countdown) {
if (true) {
ToastUtils.show(R.string.common_code_send_hint);
mCountdownView.start();
setCancelable(false);
return;
}
// 获取验证码
EasyHttp.post(this)
.api(new GetCodeApi()
.setPhone(mPhoneNumber))
.request(new OnHttpListener<HttpData<Void>>() {
// 获取验证码
EasyHttp.post(getDialog())
.api(new GetCodeApi()
.setPhone(mPhoneNumber))
.request(new OnHttpListener<HttpData<Void>>() {
@Override
public void onSucceed(HttpData<Void> data) {
ToastUtils.show(R.string.common_code_send_hint);
mCountdownView.start();
setCancelable(false);
}
@Override
public void onSucceed(HttpData<Void> data) {
ToastUtils.show(R.string.common_code_send_hint);
mCountdownView.start();
setCancelable(false);
}
@Override
public void onFail(Exception e) {
ToastUtils.show(e.getMessage());
}
});
break;
case R.id.tv_ui_confirm:
if (mCodeView.getText().toString().length() != getResources().getInteger(R.integer.sms_code_length)) {
ToastUtils.show(R.string.common_code_error_hint);
return;
}
@Override
public void onFail(Exception e) {
ToastUtils.show(e.getMessage());
}
});
} else if (viewId == R.id.tv_ui_confirm) {
if (mCodeView.getText().toString().length() != getResources().getInteger(R.integer.sms_code_length)) {
ToastUtils.show(R.string.common_code_error_hint);
return;
}
if (true) {
autoDismiss();
if (mListener != null) {
mListener.onConfirm(getDialog(), mPhoneNumber, mCodeView.getText().toString());
}
return;
}
// 验证码校验
EasyHttp.post(this)
.api(new VerifyCodeApi()
.setPhone(mPhoneNumber)
.setCode(mCodeView.getText().toString()))
.request(new OnHttpListener<HttpData<Void>>() {
@Override
public void onSucceed(HttpData<Void> data) {
autoDismiss();
if (mListener != null) {
mListener.onConfirm(getDialog(), mPhoneNumber, mCodeView.getText().toString());
}
}
@Override
public void onFail(Exception e) {
ToastUtils.show(e.getMessage());
}
});
break;
case R.id.tv_ui_cancel:
if (true) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
mListener.onConfirm(getDialog(), mPhoneNumber, mCodeView.getText().toString());
}
break;
default:
break;
return;
}
// 验证码校验
EasyHttp.post(getDialog())
.api(new VerifyCodeApi()
.setPhone(mPhoneNumber)
.setCode(mCodeView.getText().toString()))
.request(new OnHttpListener<HttpData<Void>>() {
@Override
public void onSucceed(HttpData<Void> data) {
autoDismiss();
if (mListener != null) {
mListener.onConfirm(getDialog(), mPhoneNumber, mCodeView.getText().toString());
}
}
@Override
public void onFail(Exception e) {
ToastUtils.show(e.getMessage());
}
});
} else if (viewId == R.id.tv_ui_cancel) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
}
}
}

View File

@ -2,6 +2,8 @@ package com.hjq.demo.ui.dialog;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
@ -14,7 +16,7 @@ import com.hjq.base.BaseAdapter;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
import com.hjq.toast.ToastUtils;
import java.util.ArrayList;
@ -31,21 +33,24 @@ import java.util.List;
public final class SelectDialog {
public static final class Builder
extends UIDialog.Builder<Builder> {
extends CommonDialog.Builder<Builder>
implements View.OnLayoutChangeListener, Runnable {
@SuppressWarnings("rawtypes")
private OnListener mListener;
private final RecyclerView mRecyclerView;
private final SelectAdapter mAdapter;
public Builder(Context context) {
super(context);
setCustomView(R.layout.select_dialog);
RecyclerView recyclerView = findViewById(R.id.rv_select_list);
recyclerView.setItemAnimator(null);
mRecyclerView = findViewById(R.id.rv_select_list);
mRecyclerView.setItemAnimator(null);
mAdapter = new SelectAdapter(getContext());
recyclerView.setAdapter(mAdapter);
mRecyclerView.setAdapter(mAdapter);
}
public Builder setList(int... ids) {
@ -63,6 +68,7 @@ public final class SelectDialog {
@SuppressWarnings("all")
public Builder setList(List data) {
mAdapter.setData(data);
mRecyclerView.addOnLayoutChangeListener(this);
return this;
}
@ -98,6 +104,7 @@ public final class SelectDialog {
return this;
}
@SuppressWarnings("rawtypes")
public Builder setListener(OnListener listener) {
mListener = listener;
return this;
@ -106,32 +113,64 @@ public final class SelectDialog {
@SingleClick
@SuppressWarnings("all")
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_ui_confirm:
HashMap<Integer, Object> data = mAdapter.getSelectSet();
if (data.size() >= mAdapter.getMinSelect()) {
autoDismiss();
if (mListener != null) {
mListener.onSelected(getDialog(), data);
}
} else {
ToastUtils.show(String.format(getString(R.string.select_min_hint), mAdapter.getMinSelect()));
}
break;
case R.id.tv_ui_cancel:
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.tv_ui_confirm) {
HashMap<Integer, Object> data = mAdapter.getSelectSet();
if (data.size() >= mAdapter.getMinSelect()) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
mListener.onSelected(getDialog(), data);
}
break;
default:
break;
} else {
ToastUtils.show(String.format(getString(R.string.select_min_hint), mAdapter.getMinSelect()));
}
} else if (viewId == R.id.tv_ui_cancel) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
}
}
/**
* {@link View.OnLayoutChangeListener}
*/
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
mRecyclerView.removeOnLayoutChangeListener(this);
// 这里一定要加延迟如果不加在 Android 9.0 上面会导致 setLayoutParams 无效
post(this);
}
@Override
public void run() {
final ViewGroup.LayoutParams params = mRecyclerView.getLayoutParams();
final int maxHeight = getScreenHeight() / 4 * 3;
if (mRecyclerView.getHeight() > maxHeight) {
if (params.height != maxHeight) {
params.height = maxHeight;
mRecyclerView.setLayoutParams(params);
}
} else {
if (params.height != ViewGroup.LayoutParams.WRAP_CONTENT) {
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
mRecyclerView.setLayoutParams(params);
}
}
}
/**
* 获取屏幕的高度
*/
private int getScreenHeight() {
Resources resources = getResources();
DisplayMetrics outMetrics = resources.getDisplayMetrics();
return outMetrics.heightPixels;
}
}
private static final class SelectAdapter extends MyAdapter<Object>
private static final class SelectAdapter extends AppAdapter<Object>
implements BaseAdapter.OnItemClickListener {
/** 最小选择数量 */
@ -213,7 +252,7 @@ public final class SelectDialog {
}
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final TextView mTextView;
private final CheckBox mCheckBox;

View File

@ -17,7 +17,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.hjq.base.BaseAdapter;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.app.AppAdapter;
import com.hjq.toast.ToastUtils;
import com.hjq.umeng.Platform;
import com.hjq.umeng.UmengClient;
@ -114,7 +114,7 @@ public final class ShareDialog {
}
}
private static class ShareAdapter extends MyAdapter<ShareBean> {
private static class ShareAdapter extends AppAdapter<ShareBean> {
private ShareAdapter(Context context) {
super(context);
@ -126,7 +126,7 @@ public final class ShareDialog {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final ImageView mImageView;
private final TextView mTextView;

View File

@ -11,8 +11,8 @@ import androidx.recyclerview.widget.RecyclerView;
import com.hjq.base.BaseDialog;
import com.hjq.demo.R;
import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.common.MyAdapter;
import com.hjq.demo.other.PickerLayoutManager;
import com.hjq.demo.app.AppAdapter;
import com.hjq.demo.manager.PickerLayoutManager;
import java.util.ArrayList;
import java.util.Calendar;
@ -26,7 +26,7 @@ import java.util.Calendar;
public final class TimeDialog {
public static final class Builder
extends UIDialog.Builder<Builder> implements Runnable {
extends CommonDialog.Builder<Builder> implements Runnable {
private final RecyclerView mHourView;
private final RecyclerView mMinuteView;
@ -195,27 +195,23 @@ public final class TimeDialog {
@SingleClick
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_ui_confirm:
autoDismiss();
if (mListener != null) {
mListener.onSelected(getDialog(), mHourManager.getPickedPosition(), mMinuteManager.getPickedPosition(), mSecondManager.getPickedPosition());
}
break;
case R.id.tv_ui_cancel:
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
break;
default:
break;
public void onClick(View view) {
int viewId = view.getId();
if (viewId == R.id.tv_ui_confirm) {
autoDismiss();
if (mListener != null) {
mListener.onSelected(getDialog(), mHourManager.getPickedPosition(), mMinuteManager.getPickedPosition(), mSecondManager.getPickedPosition());
}
} else if (viewId == R.id.tv_ui_cancel) {
autoDismiss();
if (mListener != null) {
mListener.onCancel(getDialog());
}
}
}
}
private static final class PickerAdapter extends MyAdapter<String> {
private static final class PickerAdapter extends AppAdapter<String> {
private PickerAdapter(Context context) {
super(context);
@ -227,7 +223,7 @@ public final class TimeDialog {
return new ViewHolder();
}
private final class ViewHolder extends MyAdapter.ViewHolder {
private final class ViewHolder extends AppAdapter<?>.ViewHolder {
private final TextView mPickerView;

View File

@ -1,7 +1,11 @@
package com.hjq.demo.ui.dialog;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
@ -9,6 +13,7 @@ import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.core.app.NotificationCompat;
import androidx.core.content.FileProvider;
import com.hjq.base.BaseDialog;
@ -19,14 +24,11 @@ import com.hjq.demo.aop.SingleClick;
import com.hjq.demo.other.AppConfig;
import com.hjq.http.EasyHttp;
import com.hjq.http.listener.OnDownloadListener;
import com.hjq.http.model.DownloadInfo;
import com.hjq.http.model.HttpMethod;
import com.hjq.permissions.Permission;
import java.io.File;
import okhttp3.Call;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
@ -119,10 +121,10 @@ public final class UpdateDialog {
@SingleClick
@Override
public void onClick(View v) {
if (v == mCloseView) {
public void onClick(View view) {
if (view == mCloseView) {
dismiss();
} else if (v == mUpdateView) {
} else if (view == mUpdateView) {
// 判断下载状态
if (mDownloadComplete) {
if (mApkFile.isFile()) {
@ -143,14 +145,44 @@ public final class UpdateDialog {
* 下载 Apk
*/
@CheckNet
@Permissions({Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE})
@Permissions({Permission.MANAGE_EXTERNAL_STORAGE})
private void downloadApk() {
// 创建要下载的文件对象
mApkFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), getString(R.string.app_name) + "_v" + mNameView.getText().toString() + ".apk");
// 设置对话框不能被取消
setCancelable(false);
EasyHttp.download(this)
NotificationManager notificationManager = getSystemService(NotificationManager.class);
int notificationId = getContext().getApplicationInfo().uid;
String channelId = "";
// 适配 Android 8.0 通知渠道新特性
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(getString(R.string.update_notification_channel_id), getString(R.string.update_notification_channel_name), NotificationManager.IMPORTANCE_HIGH);
channel.enableLights(false);
channel.enableVibration(false);
channel.setVibrationPattern(new long[]{0});
channel.setSound(null, null);
notificationManager.createNotificationChannel(channel);
channelId = channel.getId();
}
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext(), channelId)
.setWhen(System.currentTimeMillis())
// 设置通知标题
.setContentTitle(getString(R.string.app_name))
// 设置通知小图标
.setSmallIcon(R.mipmap.launcher_ic)
// 设置通知大图标
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.launcher_ic))
// 设置通知静音
.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
.setVibrate(new long[]{0})
.setSound(null)
// 设置通知的优先级
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
// 创建要下载的文件对象
mApkFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
getString(R.string.app_name) + "_v" + mNameView.getText().toString() + ".apk");
EasyHttp.download(getDialog())
.method(HttpMethod.GET)
.file(mApkFile)
.url(mDownloadUrl)
@ -158,7 +190,7 @@ public final class UpdateDialog {
.listener(new OnDownloadListener() {
@Override
public void onStart(Call call) {
public void onStart(File file) {
// 标记为下载中
mDownloading = true;
// 标记成未下载完成
@ -171,13 +203,34 @@ public final class UpdateDialog {
}
@Override
public void onProgress(DownloadInfo info) {
mUpdateView.setText(String.format(getString(R.string.update_status_running), info.getDownloadProgress()));
mProgressView.setProgress(info.getDownloadProgress());
public void onProgress(File file, int progress) {
// 更新下载通知
notificationManager.notify(notificationId, notificationBuilder
// 设置通知的文本
.setContentText(String.format(getString(R.string.update_status_running), progress))
// 设置下载的进度
.setProgress(100, progress, false)
// 设置点击通知后是否自动消失
.setAutoCancel(false)
// 重新创建新的通知对象
.build());
mUpdateView.setText(String.format(getString(R.string.update_status_running), progress));
mProgressView.setProgress(progress);
}
@Override
public void onComplete(DownloadInfo info) {
public void onComplete(File file) {
// 显示下载成功通知
notificationManager.notify(notificationId, notificationBuilder
// 设置通知的文本
.setContentText(String.format(getString(R.string.update_status_successful), 100))
// 设置下载的进度
.setProgress(100, 100, false)
// 设置通知点击之后的意图
.setContentIntent(PendingIntent.getActivity(getContext(), 1, getInstallIntent(), Intent.FILL_IN_ACTION))
// 设置点击通知后是否自动消失
.setAutoCancel(true)
.build());
mUpdateView.setText(R.string.update_status_successful);
// 标记成下载完成
mDownloadComplete = true;
@ -187,14 +240,16 @@ public final class UpdateDialog {
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
public void onError(DownloadInfo info, Exception e) {
public void onError(File file, Exception e) {
// 清除通知
notificationManager.cancel(notificationId);
mUpdateView.setText(R.string.update_status_failed);
// 删除下载的文件
info.getFile().delete();
file.delete();
}
@Override
public void onEnd(Call call) {
public void onEnd(File file) {
// 更新进度条
mProgressView.setProgress(0);
mProgressView.setVisibility(View.GONE);
@ -205,6 +260,7 @@ public final class UpdateDialog {
setCancelable(true);
}
}
}).start();
}
@ -213,6 +269,13 @@ public final class UpdateDialog {
*/
@Permissions({Permission.REQUEST_INSTALL_PACKAGES})
private void installApk() {
getContext().startActivity(getInstallIntent());
}
/**
* 获取安装意图
*/
private Intent getInstallIntent() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
Uri uri;
@ -222,10 +285,9 @@ public final class UpdateDialog {
} else {
uri = Uri.fromFile(mApkFile);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
return intent;
}
}
}

Some files were not shown because too many files have changed in this diff Show More