diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..9466725e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..ec4bb386 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_bug.yml b/.github/ISSUE_TEMPLATE/issue_en_template_bug.yml new file mode 100644 index 00000000..f1ecd2ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_bug.yml @@ -0,0 +1,173 @@ +name: Submit Bug +description: Please let me know the issues with the framework, and I will assist you in resolving them! +title: "[Bug]:" +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) + - type: input + id: input_id_1 + attributes: + label: Framework Version [Required] + description: Please enter the version of the framework you are using. + validations: + required: true + - type: textarea + id: input_id_2 + attributes: + label: Issue Description [Required] + description: Please describe the issue you are facing. + validations: + required: true + - type: textarea + id: input_id_3 + attributes: + label: Steps to Reproduce [Required] + description: Please provide steps to reproduce the issue. + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: Is the Issue Reproducible? [Required] + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: input + id: input_id_5 + attributes: + label: Project targetSdkVersion [Required] + validations: + required: true + - type: input + id: input_id_6 + attributes: + label: Device Information [Required] + description: Please provide the brand and model of the device where the issue occurred. + validations: + required: true + - type: input + id: input_id_7 + attributes: + label: Android Version [Required] + description: Please provide the Android version where the issue occurred. + validations: + required: true + - type: dropdown + id: input_id_8 + attributes: + label: Issue Source Channel [Required] + multiple: true + options: + - Encountered by myself + - Identified in Bugly + - User feedback + - Other channels + - type: input + id: input_id_9 + attributes: + label: Is it specific to certain device models? [Required] + description: Specify whether the issue is specific to certain devices (e.g., specific brand or Android version). + validations: + required: true + - type: dropdown + id: input_id_10 + attributes: + label: Does the latest version of the framework have this issue? [Required] + description: If you are using an older version, it is recommended to upgrade and check if the issue still persists. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_11 + attributes: + label: Is the issue mentioned in the framework documentation? [Required] + description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_12 + attributes: + label: Did you consult the framework documentation but couldn't find a solution? [Required] + description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_13 + attributes: + label: Has a similar issue been reported in the issue list? [Required] + description: You can search the issue list for keywords related to your problem and refer to the solutions provided by others. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_14 + attributes: + label: Have you searched the issue list but couldn't find a solution? [Required] + description: If you have searched the issue list and couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_15 + attributes: + label: Can the issue be reproduced with a demo project? [Required] + description: Check if the issue can be reproduced in a minimal demo project to isolate potential issues in your own code. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: textarea + id: input_id_16 + attributes: + label: Provide Error Stack Trace + description: If there is an error, please provide the stack trace. Note, Do not include obfuscated code in the stack trace. + render: text + validations: + required: false + - type: textarea + id: input_id_17 + attributes: + label: Provide Screenshots or Videos + description: Provide screenshots or videos if necessary. This field is optional. + validations: + required: false + - type: textarea + id: input_id_18 + attributes: + label: Provide a Solution + description: If you have already found a solution, this field is optional. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_question.yml b/.github/ISSUE_TEMPLATE/issue_en_template_question.yml new file mode 100644 index 00000000..663218e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_question.yml @@ -0,0 +1,65 @@ +name: Ask a Question +description: Ask your questions, and I will provide you with answers. +title: "[Question]:" +labels: ["question"] + +body: + - type: markdown + attributes: + value: | + ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: Question Description [Required] + description: Please describe your question (Note, If it is a framework bug, please do not raise it here, as it will not be accepted). + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: Is the issue mentioned in the framework documentation? [Required] + description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: Did you consult the framework documentation but couldn't find a solution? [Required] + description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: Has a similar issue been reported in the issue list? [Required] + description: You can search the issue list for keywords related to your problem and refer to the solutions provided by others. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_5 + attributes: + label: Have you searched the issue list but couldn't find a solution? [Required] + description: If you have searched the issue list and couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_suggest.yml b/.github/ISSUE_TEMPLATE/issue_en_template_suggest.yml new file mode 100644 index 00000000..742e2b92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_suggest.yml @@ -0,0 +1,60 @@ +name: Submit Suggestion +description: Please let me know the shortcomings of the framework, so that I can improve it! +title: "[Suggestion]:" +labels: ["help wanted"] + +body: + - type: markdown + attributes: + value: | + ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: What are the shortcomings you have noticed in the framework? [Required] + description: You can describe any aspects of the framework that you are not satisfied with. + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: Has a similar suggestion been made in the issue list? [Required] + description: If a similar suggestion has already been made, I will not address it again. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: Is the suggestion mentioned in the framework documentation? [Required] + description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: Did you consult the framework documentation but couldn't find a solution? [Required] + description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: textarea + id: input_id_5 + attributes: + label: How do you suggest improving it? [Optional] + description: You can provide your ideas or approaches for the author's reference. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml new file mode 100644 index 00000000..af107d80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml @@ -0,0 +1,173 @@ +name: 提交 Bug +description: 请告诉我框架存在的问题,我会协助你解决此问题! +title: "[Bug]:" +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: input + id: input_id_1 + attributes: + label: 框架版本【必填】 + description: 请输入你使用的框架版本 + validations: + required: true + - type: textarea + id: input_id_2 + attributes: + label: 问题描述【必填】 + description: 请输入你对这个问题的描述 + validations: + required: true + - type: textarea + id: input_id_3 + attributes: + label: 复现步骤【必填】 + description: 请输入问题的复现步骤 + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: 是否必现【必填】 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: input + id: input_id_5 + attributes: + label: 项目 targetSdkVersion【必填】 + validations: + required: true + - type: input + id: input_id_6 + attributes: + label: 出现问题的手机信息【必填】 + description: 请填写出现问题的品牌和机型 + validations: + required: true + - type: input + id: input_id_7 + attributes: + label: 出现问题的安卓版本【必填】 + description: 请填写出现问题的 Android 版本 + validations: + required: true + - type: dropdown + id: input_id_8 + attributes: + label: 问题信息的来源渠道【必填】 + multiple: true + options: + - 自己遇到的 + - Bugly 看到的 + - 用户反馈 + - 其他渠道 + - type: input + id: input_id_9 + attributes: + label: 是部分机型还是所有机型都会出现【必答】 + description: 部分/全部(例如:某为,某 Android 版本会出现) + validations: + required: true + - type: dropdown + id: input_id_10 + attributes: + label: 框架最新的版本是否存在这个问题【必答】 + description: 如果用的是旧版本的话,建议升级看问题是否还存在 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_11 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_12 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_13 + attributes: + label: issue 列表中是否有人曾提过类似的问题【必答】 + description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_14 + attributes: + label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 + description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_15 + attributes: + label: 是否可以通过 Demo 来复现该问题【必答】 + description: 排查一下是不是自己的项目代码写得有问题导致的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: textarea + id: input_id_16 + attributes: + label: 提供报错堆栈 + description: 如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来 + render: text + validations: + required: false + - type: textarea + id: input_id_17 + attributes: + label: 提供截图或视频 + description: 根据需要提供,此项不强制 + validations: + required: false + - type: textarea + id: input_id_18 + attributes: + label: 提供解决方案 + description: 如果已经解决了的话,此项不强制 + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml new file mode 100644 index 00000000..ba56beb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml @@ -0,0 +1,65 @@ +name: 提出疑问 +description: 提出你的困惑,我会给你解答 +title: "[疑惑]:" +labels: ["question"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: 问题描述【必填】 + description: 请描述一下你的问题(注意:如果确定是框架 bug 请不要在这里提,否则一概不受理) + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: issue 列表中是否有人曾提过类似的问题【必答】 + description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_5 + attributes: + label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 + description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml new file mode 100644 index 00000000..e7ef5198 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml @@ -0,0 +1,60 @@ +name: 提交建议 +description: 请告诉我框架的不足之处,让我做得更好! +title: "[建议]:" +labels: ["help wanted"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: 你觉得框架有什么不足之处?【必答】 + description: 你可以描述框架有什么令你不满意的地方 + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: issue 是否有人曾提过类似的建议?【必答】 + description: 一旦出现重复提问我将不会再次解答 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: textarea + id: input_id_5 + attributes: + label: 你觉得该怎么去完善会比较好?【非必答】 + description: 你可以提供一下自己的想法或者做法供作者参考 + validations: + required: false \ No newline at end of file diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..635d6950 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,15 @@ +name: Android CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 diff --git a/.gitignore b/.gitignore index d8e49a90..371c80dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ -*.iml -*/build -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -/.idea +.gradle +.idea +.cxx +.kotlin +.externalNativeBuild +build +captures + +._* +*.iml +.DS_Store +local.properties \ No newline at end of file diff --git a/API.md b/API.md deleted file mode 100644 index f73cf8c1..00000000 --- a/API.md +++ /dev/null @@ -1,197 +0,0 @@ -#### MyActivity - -> 所有的 Activity 必须继承至 MyActivity,如果使用 MVP 请继承至 MvpActivity,已经处理了 Activity 多重启动的问题,[详情可点击此处查看](https://www.jianshu.com/p/579f1f118161) - -> 获取 Context 或者 Activity - - public Context getContext() - - public A getActivity() - -> startActivity 方法优化 - - public void startActivity(Class cls) - - public void startActivityFinish(Class cls) - - public void startActivityFinish(Intent intent) - -> startActivityForResult 方法优化 - - public void startActivityForResult(Intent intent, ActivityCallback callback) - - public void startActivityForResult(Intent intent, @Nullable Bundle options, ActivityCallback callback) - -> setResult 方法优化 - - public void finishResult(int resultCode) - - public void finishResult(int resultCode, Intent data) - -> Activity 标题 - - public void setTitle(int titleId) - - public void setTitle(CharSequence title) - - public CharSequence getTitle() - -> Toast 方法 - - public void toast(CharSequence s) - - public void toast(int id) - - public void toast(Object object) - -> Handler 方法 - - public static Handler getHandler() - -> TitleBar 方法 - - public TitleBar getTitleBar() - -> TitleBar 监听方法(需要被重写) - - // 标题栏左项被点击了,默认返回 - public void onLeftClick(View v) - - // 标题栏中间项被点击了 - public void onTitleClick(View v) - - // 标题栏右项被点击了 - public void onRightClick(View v) - -#### MyFragment - -> 获取Activity,防止出现 getActivity() 为空 - - public FragmentActivity getFragmentActivity() - -> 是否进行了懒加载 - - protected boolean isLazyLoad() - -> 当前 Fragment 是否可见 - - public boolean isFragmentVisible() - -> 跟 Activity 的同名方法效果一样 - - protected void onRestart() - -> 根据资源 id 获取一个 View 对象 - - protected T findViewById(@IdRes int id) - - protected T findActivityViewById(@IdRes int id) - -> 跳转到其他Activity - - public void startActivity(Class cls) - -> 销毁当前 Fragment 所在的 Activity - - public void finish() - -> 获取系统服务 - - public Object getSystemService(@NonNull String name) - -> Fragment返回键被按下时回调(只做预留方法,没有效果) - - public boolean onKeyDown(int keyCode, KeyEvent event) - -> Toast 方法 - - public void toast(CharSequence s) - - public void toast(int id) - - public void toast(Object object) - -##### MyRecyclerViewAdapter - -> 获取 RecyclerView 或者 Context - - public RecyclerView getRecyclerView() - - public Context getContext() - -> 布局摆放器(可以被重载,由于 RecyclerView 不能没有设置 LayoutManager,这里设置了默认的) - - protected RecyclerView.LayoutManager getDefaultLayoutManager(Context context) { - return new LinearLayoutManager(context); - } - -> 分页逻辑预留方法 - - public int getPageNumber() - - public void setPageNumber(int pageNumber) - - public boolean isLastPage() - - public void setLastPage(boolean lastPage) - -> 标记方法 - - public Object getTag() - - public void setTag(Object tag) - -> 操作数据集合 - - public void setData(List data) - - public List getData() - - public void addData(List data) - - public void clearData() - -> 操作单个数据 - - public T getItem(int position) - - public void setItem(int position, T item) - - public void addItem(T item) - - public void addItem(int position, T item) - - public void removeItem(T item) - - public void removeItem(int position) - -> MyRecyclerViewAdapter.ViewHolder 方法 - - public final View getItemView() - - public final V findViewById(@IdRes int id) - - public final ViewHolder setText(@IdRes int id, @StringRes int id) - - public final ViewHolder setText(@IdRes int id, String text) - - public final ViewHolder setVisibility(@IdRes int id, int visibility) - - public final ViewHolder setColor(@IdRes int id, @ColorInt int color) - - public final ViewHolder setImage(@IdRes int id, @DrawableRes int drawableId) - -> 监听方法(必须在 RecyclerView.setAdapter 之前调用) - - public void setOnItemClickListener(OnItemClickListener listener) - - public void setOnChildClickListener(@IdRes int id, OnChildClickListener listener) - - public void setOnItemLongClickListener(OnItemLongClickListener listener) - - public void setOnChildLongClickListener(@IdRes int id, OnChildLongClickListener listener) - - public void setOnScrollingListener(OnScrollingListener listener) - -> MyListViewAdapter 和 MyRecyclerViewAdapter 差不多,只不过没有上面这些监听方法,因为 ListView 本身已经自带这些了 - diff --git a/AndroidBug.md b/AndroidBug.md deleted file mode 100644 index 4a647e83..00000000 --- a/AndroidBug.md +++ /dev/null @@ -1,89 +0,0 @@ -# Android普遍存在的问题 - -> 这些问题已经在模板项目中已经被修复好了,具体修复过程如下 - -#### 修复 Button 在Android 5.1 之后英文字符串自动大写的问题 - -> 给 Button 添加如下属性即可,模板工程已经把该属性封装到Style中,直接引用style="@style/ButtonStyle"即可 - - android:textAllCaps="false" - -#### 修复 Button 在设置状态选择器后仍然残留按压阴影的问题 - -> 给 Button 设置样式如下即可,模板工程已经把该属性封装到Style中,直接引用style="@style/ButtonStyle"即可 - - style="Widget.AppCompat.Button.Borderless" - -#### 修复某些低配置机型启动页停留在白屏的时间比较长的问题 - -> 某些低配置机型上出现该问题比较明显,如果配置好的机型则看不出来,添加一个透明的Activity主题样式 - - - - -> 在清单文件中给启动页的Activity设置主题样式 - - - - - - - - - - - - - - -> 还有一点需要特别注意,Android 8.0 及之后不允许透明主题的Activity设置屏幕方向,所以请不要给Activity设置该属性,否则会导致崩溃 - -#### 修复某些机型在 WebView 长按时布局被顶下来的问题 - -> 这个问题在只要界面有 WebView 的情况才会发生,在 Android 5.1 经过测试,在 WebView 中长按选择复制文字时,会显示一个类似于 Actionbar 的控件,但是这个东西叫做ActionMode,会将当前 Activity 所在布局顶下去,这时会和我们项目中的标题栏出现冲突,类似于一个界面同时出现了两个标题栏的效果,解决的方法很简单,就是让出现的 ActionMode 悬浮在 Activity上,这样就把项目中的标题栏遮挡住了,不会出现那种类似一个界面出现两种标题栏的效果,当 WebView 取消长按复制文字后,ActionMode也会随之消失 - -> 如何让 ActionMode 悬浮在 Activity 上呢?其实很简单,在 Application 主题中加入以下属性 - - - true - @null - - @color/colorPrimary - -#### 修复 任务栈中 首页Activity 被重复启动的问题 - -> 这个问题导致是因为LauncherActivity作为APP的第一个界面,销毁后没有保存任务栈的状态,导致我们在桌面上启动的时候系统误认为当前启动LauncherActivity的任务栈已经被销毁,所以重新创建了新的任务栈并且跳转到LauncherActivity,最终导致用户从桌面点击APP图标时,总是跳转到LauncherActivity而不是HomeActivity - - - - -#### 修复 Android 9.0 限制 Http 明文请求的问题 - -> Android P 限制了明文流量的网络请求,非加密的流量请求都会被系统禁止掉。 -如果当前应用的请求是 htttp 请求,而非 https ,这样就会导系统禁止当前应用进行该请求,如果 WebView 的 url 用 http 协议,同样会出现加载失败,https 不受影响 - -> 在 res 下新建一个 xml 目录,然后创建一个名为:network_security_config.xml 文件 ,该文件内容如下 - - - - - - -> 然后在 AndroidManifest.xml application 标签内应用上面的xml配置 - - - diff --git a/AndroidProject.apk b/AndroidProject.apk deleted file mode 100644 index 546a4aea..00000000 Binary files a/AndroidProject.apk and /dev/null differ diff --git a/HelpDoc.md b/HelpDoc.md new file mode 100644 index 00000000..5c7c432e --- /dev/null +++ b/HelpDoc.md @@ -0,0 +1,478 @@ +#### 目录 + +* [为什么没有用 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.0](#为什么最低兼容到-android-50) + +* [为什么不加入 XXX 功能](#为什么不加入-xxx-功能) + +* [为什么不加入 EventBus](#为什么不加入-eventbus) + +* [为什么没有用 Retrofit 和 RxJava](#为什么没有用-retrofit-和-rxjava) + +* [为什么没有用 Jetpack 全家桶](#为什么没有用-jetpack-全家桶) + +* [为什么不对图片加载框架进行再次封装](#为什么不对图片加载框架进行再次封装) + +* [模板 架构 技术中台有什么区别](#模板-架构-技术中台有什么区别) + +* [为什么不按业务来划分包名](#为什么不按业务来划分包名) + +* [为什么没有关于列表多 type 的封装](#为什么没有关于列表多-type-的封装) + +* [这不就是一个模板工程换成我也能写一个](#这不就是一个模板工程换成我也能写一个) + +* [假设 AndroidProject 更新了该怎么升级它](#假设-androidproject-更新了该怎么升级它) + +* [为什么不用谷歌 ActivityResultContracts](#为什么不用谷歌-activityresultcontracts) + +* [为什么新版移除了权限申请的 AOP 注解](#为什么新版移除了权限申请的-aop-注解) + +* [轮子哥你怎么看待层出不穷的新技术](#轮子哥你怎么看待层出不穷的新技术) + +#### 为什么没有用 MVP + +![](picture/help/mvp_issue_1.jpg) + +![](picture/help/mvp_issue_2.jpg) + +![](picture/help/mvvm_issue.jpg) + +* AndroidProject 舍弃 MVP 的最大一个原因,需要写各种类,各种回调,如果这个页面比较简单的话,使用 MVP 会让原本简单的代码变复杂,导致后续开发和维护成本是非常高,前期付出的代价和后期的维护不成正比关系,当然这种说法只适用于各种中小型项目,大型的项目我还没有经历过,不过我觉得,无论是 MVC、MVP、MVVM,它们出现的目的是为了解决代码多并且乱的问题,作用就是给代码做分类,但是可以跟大家分享我的心得,我并不看好 MVP,因为它让我开发和维护都很痛苦,所以我就直接将它从 AndroidProject 移除,目的也很简单,不推荐大家使用,因为 MVP 不适合大多数项目的开发和维护。我更推荐大家直接将代码写在 Activity,但是有一个前提条件需要大家遵守,大家要做好代码封装和重复代码的抽取,尽量让 Activity 成为只有业务代码的类,这样一个项目里面的大多数 Activity 代码量都能很好控制在 1000 行代码以内。但是这种看似简单的操作,但是实际要做到是一件不容易的事情,这里面不仅要解决代码带来的问题,还要解决带来的各种人性矛盾,困难重重,这种想法经过很长一段时间的思考,虽然写法在开发和维护中效率是非常高的,但是不被大多数人认可,大家更愿意相信 MVC、MVP、MVVM,而很少有人理解这三种模式的本质是什么,就是为了给代码做分类,但这三种模式都不够灵活,很生硬,像是一套套规则,而这样的代码分类,只会让大多数人的开发越来越头疼。 + +#### 为什么没有用 ButterKnife + +* 随着 AndroidProject 的不断优化,ButterKnife 功能很强大,但是实际开发中,大多数人只用到了 BindView 和 OnClick 注解,在 OnClick 注解在我的项目中发现一个 Bug,就是有时候不会响应点击事件,这个问题并不是必现的。还有 BindView 注解只是在视觉上面将 View 和 ID 的关系更明显了,它其实不能为我们简化代码,因为使用 BindView 和 findViewById 的代码量是一样的。 + +* ButterKnife 最大的缺点是还会自动生成 ViewBinding 类,就算在类中只使用了一个 BindView,它也会生成这个类,其实这样是不太好的。 + +* 另外一个点,将 Android Studio 升级到 4.1 之后,会出现以下提示,这个是因为 Gradle 在 5.0 之后的版本,View ID 将不会以常量的形式存在,所以不能将其定义在 `BindView` 注解或者在 `switch case` 块中。 + +```text +Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them as annotation +Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them in switch case statements +``` + +* 考虑到这些情况,我在新版的 AndroidProject 上面移除了 ButterKnife 框架,其实 findViewById 一直挺好,只是我们没有认真思考过而已。 + +* 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) + +#### 为什么没有用 ViewBinding + +* 首先 ViewBinding 和 ButterKnife 有一个相同的毛病,就是自动生成一个类,然后在这个类里面进行 findViewById,但是有一个致命的缺点,每个 `Activity / Fragment / Dialog / Adapter` 都需要先初始化 ViewBinding 对象,因为每次生成的类名都是不固定的,所以无法在基类中封装处理,并且每次都要写 `binding.xxx` 才能操作控件。 + +```java +ActivityXxxxBinding.inflate binding = ActivityXxxxBinding.inflate(getLayoutInflater()); +binding.tv_data_name.setText("字符串"); +``` + +* 另外一个它会根据控件 id 作为属性的名称,这样会导致一个代码不规范的问题,如果在 xml 中控件的 id 命名符合规范了,会导致在 Java 代码中的命名不规范,如果在 Java 代码中的命名规范了,又会导致 xml 的 id 不符合规范了。而代码规范关系到后续的代码维护,是一个很重要的问题,不容忽视。 + +* 虽然 ViewBinding 是谷歌官方推荐的,但是我觉得并不完美,解决了 findViewById 的同时又带来了其他的问题,在关键问题上有问题和矛盾,直白点说这些问题都是硬伤。 + +* 正如我上面所说的,findViewById 一直挺好,只是我们没有认真思考过而已。 + +* 另外大家如果不想写 findViewById,我可以推荐一款自动生成 findViewById 的插件给大家:[FindViewByMe](https://plugins.jetbrains.com/plugin/8261-findviewbyme) + +#### 为什么没有用 DataBinding + +* DataBinding 最大的优势在于,因为它可以在 xml 直接给 View 赋值,但它的优点正是它最致命的缺点,当业务逻辑简单时,会显得格外美好,但是一旦判断条件复杂起来,由于 xml 属性不能换行的特性,会导致无法在 xml 直接赋值又或者很长的一段代码堆在布局中,间接导致 CodeReview 时异常艰难,更别说在原有的基础上继续更新迭代,这对每一个开发者来讲无疑是一个巨大的灾难。 + +* 还有一个是 DataBinding 的学习成本比较高,其次使用成本也挺高,使用前需要做很多封装,另外每次使用时都需要添加 `layout` 和 `data` 节点,然后在 Java 代码中初始化 DataBinding 对象,无法在基类中封装处理,每次都要写 `binding.xxx` 才能操作控件,和 ViewBinding 的问题差不多。 + +```java +ActivityXxxxBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_xxxx); +``` + +#### 为什么没有用组件化 + +* 先来说说组件化的优点,能够把不同的业务代码进行隔离,达到完全解耦的效果,同时也能提升编译和打包速度。但是这两个优点只有项目业务变得大并且复杂的情况下才能产生价值,否则价值并不大,在我看来,代码解耦其实是把双刃剑,解耦的过程相对比较麻烦,这会直接加大前期开发的工作量,并且一些解耦的方式可能会导致代码重复,例如 AndroidManifest 清单文件,需要同时配置两份文件,后期改动也需要改两遍,另外一个是路由跳转,现在大多数路由框架都是通过 APT 生成一张映射表,这个需要我们每写一个 Activity 都要写一个路径的注解,这个不仅写起来麻烦,管理起来也会很麻烦,另外对每个业务模块的 SDK 初始化操作和数据存储交互上又该如何处理和解耦?这些都是组件化所存在的问题,矛和盾又该如何抉择? + +* 组件化其实是一个很好的思想,但是它并不适用于中小型项目,因为这些项目并没有那么复杂,大部分业务模块都很小,大的业务模块其实也不多,当然我个人建议可以将大点的模业务进行模块化,但是没有必要做组件化,因为一旦涉及,从组件化中得到弊会大于利。而在一些大型的项目中,大大小小的模块非常多,一次打包编译可能要半个小时甚至更久(请注意大厂的电脑基本都是高配或者顶配),相比较这种情况之下,组件化的优点就已经大于了它的缺点,同时他们也有充足的人力和过硬的技术,并且能长期投入巨大的时间和精力来做这件事。 + +* AndroidProject 面对的是大众开发者,所以更倾向中小型的项目代码的设计,虽然我没有做过大型的项目,但是在我看来是差不多的,最大的不同可能是代码分类方式的不同,该做的事情不会少,该写的代码也不会少,就是业务和代码的体量上比我们大,所以他们要处理体量大所带来的的问题。 + +#### 为什么没有集成界面侧滑功能 + +* AndroidProject 其实有加入过这个功能,但是在 [v9.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/9.0) 就移除了,原因是第三方侧滑框架 [BGASwipeBackLayout](https://github.com/bingoogolapple/BGASwipeBackLayout-Android) 在 Android 9.0 上面会[闪屏](https://github.com/bingoogolapple/BGASwipeBackLayout-Android/issues/173),并且还是 **100% 必现**,**用户体验极差**,我也跟作者反馈过这个问题,但结果不了了之,所以不得不移除。但是到了 [v10.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/10.0),我又加上界面侧滑功能了,不过这次我换成了 [SmartSwipe](https://github.com/luckybilly/SmartSwipe) 来做,但是我又再一次失望了,这个框架在 Android 11 上面,如果 Activity 上有 WindowManager 正在显示,然后使用界面侧滑,那么会出现闪屏的情况,具体效果如下图: + +![](picture/help/swipe_issue.jpg) + +* 就这个情况我也联系过作者,并详细阐述了产生的原因和具体的复现步骤,但是我等了三天连个回复都没有,实属有些让我心寒,在等待的期间我看到 Github 的 issue 已经基本没有回复了,并且最后一次提交是在 13 个月前了,种种迹象都已经表明,所以经过慎重考虑,最终决定在 [v12.1 版本](https://github.com/getActivity/AndroidProject/releases/tag/12.1) 移除界面侧滑功能。 + +#### 为什么没有用今日头条的适配方案 + +* 关于屏幕适配方案,其实不能说头条的方案就是最好的,其实谷歌已经针对屏幕适配做了处理,就是 dp 和 sp ,而 dp 的计算转换是由屏幕的像素决定,系统只认 px 单位, dp 需要进行转换,比如 1dp 等于几个 px ,这个时候就需要基数进行转换,比如 1dp = 2px,这个基数就是 2。 + +```text +ldpi:1dp=0.75px + +mdpi:1dp=1px + +hdpi:1dp=1.5px + +xhdpi:1dp=2px + +xxhdpi:1dp=3px + +xxxhdpi:1dp=4px +``` + +* 这个是谷歌对屏幕适配的一种默认方式,厂商也可以根据需要去修改默认的基数,从而达到最优的显示效果。 + +* 谷歌的屏幕适配方案也不是百分之一百完美的,其实会存在一些需求不能满足的问题。谷歌的设计理念是屏幕越大显示的东西越多,这种想法并没有错,但有些 App 可能对这块会有要求,希望根据屏幕大小对控件进行百分比压缩。这个时候谷歌那套适配方案的设计已经和需求完全不一致了。 + +* 那什么样的 App 才会有那样的需求呢?现在手机的屏幕大多在 5 - 6寸,而平板大多在 8 - 10 寸,也就是说我们只适配手机的话,只需要针对 5 - 6 寸的,并且它们的分辨率都差不多,其实用谷歌那种方案是最优的,如果我们需要适配平板的话,一般都会要求对控件进行百分比压缩,这个时候谷歌那套方案会把原先在手机显示的控件在平板上面变大一点,这样就会导致屏幕剩余的空间过大,导致控件显示出来的效果比较小,而如果采用百分比对控件压缩的方式,能比较好地控制 App 在不同屏幕下显示的效果。 + +* 另外谈谈我的经历,我自己之前的公司主要是做平板上面的应用,所以也用过 [AutoSize 框架](https://github.com/JessYanCoding/AndroidAutoSize),一年多的使用体验下来,发现这个框架 Bug 还算是比较多的,例如框架会偶尔出现机型适配失效,重写了 **getResources** 方法情况之后出现的情况少了一些,但是仍然还有一些奇奇怪怪的问题,这里就不一一举例了,最后总结下来就是框架还不够成熟,但是框架的思想还是很不错的。我后面换了一家公司,也是做平板应用,项目用的是用[通配符的适配方案](https://github.com/wildma/ScreenAdaptation),跟 AutoSize 相对比,没有了那些奇奇怪怪的问题,但是代码的侵入性比较高。这两种方案各有优缺点,大家看着选择。 + +![](picture/help/vote_2.jpg) + +* 在这块我也发起过群投票,相比谷歌的适配方案,大多数人更认同那种百分比适配方案,秉承着少数服从多数的理念,我在 AndroidProject [v13.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/13.0) 加入了通配符的适配方案。虽然有一部分人不认同,但是我想跟这些人说的是:我的每一个决定都是十分谨慎的,因为这其中涉及到许多人的利益,AndroidProject 虽然是我创造的,但是它早就不是我一个人的了,而是大家的,每个重要的决定我都会考虑再三才会去做,在做决定的时候我会把大众的利益放在第一位,把自己的利益放在最后一位,所以大家唯一能做的是,相信我的选择。或许你可能觉得这样不太对,也随时欢迎你提出不同的意见给到我,我不认为自己做的决定一定都是对的,但是我会一直朝着对的方向前进。 + +#### 字体大小为什么不用 dp 而用 sp + +* 首先我们先回顾一下谷歌原生的写法,将控件大小的单位定成了 dp,而字体大小的单位定成了 sp,而无论是 dp 还是 sp 作为单位,最终还是会转成 px 的单位。 + +* 谷歌这样做也有一定目的,dp 是根据屏幕的密度来计算的,而 sp 是根据手机设置的字体大小来计算的,如果用 dp 来代替 sp 会有一个问题,那么就是无论用户在手机里面怎么设置字体大小,应用的字体大小不会产生任何变化。这种场景对年轻人来讲没有太大的影响,而对一些老龄用户,例如我们的爸妈,他们一般会把手机的字体调大,这样才能看清楚里面的字,如果我们采用 dp 来代替 sp 的方案,会对这类用户造成不便,换位思考,我们终有一天也会变老,变得老眼昏花,我们会如何看待这个事情? + +* 显然这种方式是不合理的,也非常地不人性化。网上这种方案可能主要就是为了解决把控件宽高写死之后,在某些字体上显示比较大的机型会出现字显示不全的问题,而这种把控件宽高写死的方式本身也是不合理的,应该在不得已的情况下才把控件的宽高写死,一般情况下我们应当使用自适应的方式,让控件自己测量自己的宽高,特别是在有显示字体的控件下,就更不应该把宽高写死。 + +#### 为什么没有用 DialogFragment 来防止内存泄漏 + +* DialogFragment 的出现就是为了解决 Dialog 和 Activity 生命周期不同步导致的内存泄漏问题,在 AndroidProject 曾经引入过,也经过了很多个版本的更新迭代,不过在 [10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 版本后就被移除了,原因是 Dialog 虽然有坑,但是 DialogFragment 也有坑,可以说解决了一个问题又引发了各种问题。先来细数我在 DialogFragment 上踩过的各种坑: + + 1. DialogFragment 会占用 Dialog 的 Cancel 和 Dismiss 监听,为了就是在 Dialog 消失之后将自己(Fragment)从 Activity 上移除,这样的操作看起很合理,但是会引发一个问题,那么就是会导致我们原先给 Dialog 设置的 Cancel 和 Dismiss 监听被覆盖掉,间接导致我们无法使用这个监听,因为 Dialog 的监听器只能有一个观察者,而 AndroidProject 前期解决这个问题的方式是:将 Dialog 的监听器使用的观察者模式,从一对一改造成一对多,也就是一个被观察者可以有很多个观察者,由此来解决这个问题。 + + 2. DialogFragment 的显示和隐藏操作都不能在后台中进行,否则会出现一个报错 `java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState`,这个是因为 DialogFragment 的 show 和 dismiss 方法使用了 FragmentTransaction.commit 方法,这个 commit 方法会触发对 Activity 状态的检查,如果 Activity 的状态已经保存了(即已经调用了 onSaveInstanceState 方法),这个时候把 Fragment commit 到 Activity 上会抛出异常,这种场景在执行异步操作(例如请求网络)未结束前,用户手动将 App 返回到桌面,然后异步操作执行完毕,下一步就是回调异步监听器,这个时候我们的 App 已经处于后台状态,那么我们如果在监听回调中 show 或 dismiss DialogFragment,那么就会触发这个异常。AndroidProject 前期对于这个问题的解决方案是重写 DialogFragment.show 方法,加一个对 Activity 的状态判断,如果 Activity 处于后台状态,那么不去调用 super.show(),但是这样会导致一个问题,虽然解决了崩溃的问题,但是又会导致 Dialog 没显示出来,而重写 DialogFragment.dismiss 方法,直接调用 dismissAllowingStateLoss 方法,因为这个方法不会去检查 Activity 的状态。虽然这种解决方式不够完美,但却是我那个时候能想到的最好方法。 + + 3. 最后一个问题是关于 DialogFragment 屏幕旋转的问题,首先 DialogFragment 是通过自身 onCreateDialog 方法来获取 Dialog 对象的,但是如果我们直接通过外层给 DialogFragment 传入 Dialog 的对象时,这样的代码逻辑貌似没有问题,但是在用户进行屏幕旋转,而刚好我们的应用没有固定屏幕方向时,DialogFragment 对象会跟随 Activity 销毁重建,因为它本身就是一个 Fragment,但是会导致之前的外层传入 Dialog 对象被回收并置空,然后再调用到 onCreateDialog 方法时,返回的是一个空对象的 Dialog,那么就会直接 DialogFragment 内部引发空指针异常,而 AndroidProject 前期解决这个问题的方案是,重写 onActivityCreated,赶在 onCreateDialog 方法调用之前,先判断 DialogFragment 对象内部持有的 Dialog 是否为空,如果是一个空对象,那么就将自己 dismissAllowingStateLoss 掉。 + +* 看过这些问题,你是不是和我一样,感觉这 DialogFragment 不是一般的坑,不过最终我放弃了使用 DialogFragment,并不是因为 DialogFragment 又出现了新问题,而是我想到了更好的方案来代替 DialogFragment,方案就是 Application.registerActivityLifecycleCallbacks,想必大家现在已经猜到我想干啥,和 DialogFragment 的作用一样,通过监听 Activity 的方式来管控 Dialog 的生命周期,但唯一不同的是,它不会出现刚刚说过 DialogFragment 的那些问题,这种方式在 AndroidProject 上迭代了几个版本过后,这期间没有发现新的问题,也没有收到别人反馈过这块的问题,证明这种方式是可行的。 + +#### 为什么没有用腾讯 X5 WebView + +* 首先我问大家一个问题,腾讯出品的 X5 WebView 就一定比原生 WebView 好吗?我觉得未必,我依稀记得 Android 9.0 还是 Android 10 刚出来的时候,我点了升级按钮,然后就发现微信和 QQ 的网页浏览卡得让我怀疑人生,不过后面突然某一天就变好了,从这件事可以得出两点结论: + + 1. 第一个 SDK 有自我更新功能,意味着 WebView 掌控权握在腾讯公司手中 + + 2. 第二个是 SDK 需要腾讯来持续维护,意味着这个项目的生命周期会跟随腾讯公司的发展和决策 + +* 基于以上两点,我的个人建议是优先使用原生 WebView,如果不满足需求了,可以自行替换成 X5 WebView,当然不是说 X5 WebView 一定不好,用原生 WebView 一定就好,而是 AndroidProject 的目标是稳中求胜,另外一个是 AndroidProject 中有针对 WebView 做统一封装,后续替换成 X5 WebView 的成本还算是相对较低的。 + +#### 为什么没有用单 Activity 多 Fragment + +* 这个问题在前几年是一个比较火热的话题,我表示很能理解,因为新鲜的事物总是能勾起人的好奇,让人忍不住试一试,但是我先问大家一个问题,单 Activity 多 Fragment 和写多个 Activity 有什么优点?大家第一个反应应该是每写一个页面都不需要在清单文件中注册了,但是这个真的是优点吗?我可以很明确地告诉大家,我已经写了那么多句代码,不差那句在清单文件注册的代码。那么究竟什么才是对我们有价值的?我觉得就两点,一是减少前期开发的工作量,二是降低后续维护的难度。所以省那一两句有前途吗?我们是差那一两句代码的人吗?如果这种模式能够帮助我们写好代码,这个当然是有价值的,非常值得一试的,否则就是纯属瞎扯淡。不仅如此,我个人觉得这种模式有很大的弊端,会引发很多问题,例如: + + 1. 有的页面是全屏有的页面是非全屏,有的页面是固定竖屏有的页面是横屏,进入时怎么切换?返回时怎么切换回来?然后又该怎么去做统一的封装? + + 2. 不同 Fragment 之间应该怎样通讯?Activity 有 onActivityResult 方法可以用,但是 Fragment 有什么方法可以用?还是全用 EventBus 来处理?如果是这样做会不会太低效了?每次都要写一个 Event 类,并且在代码中找起来是不是也不太好找? + + 3. 如何保证这个 Activity 被系统回收之后,然后引发重建操作,又该如何保证这个 Activity 中的多个 Fragment 之间的回退栈是否正常?假设这个 Activity 里面有 10 个Fragment,一下子引发 10 个 Fragment 创建是否会对内存和性能造成影响呢? + +* 如果单 Activity 多 Fragment 不能为我们创造太大的价值时,这种模式根本就不值得我们去做,因为我们最终得到的,永远抵不上付出的。 + +#### 为什么没有用 ConstraintLayout 来写布局 + +* 大家如果有仔细观察的话,会发现 AndroidProject 其实没有用到 ConstraintLayout 布局,在这里谈谈我对这个布局的看法,约束布局有一个优点,没有布局嵌套,所以能减少测量次数,从而提升布局绘制的速度,但是优点也是它的缺点,正是因为没有布局嵌套,View 也就没有层级概念,所以它需要定义很多 ViewID 来约束相邻的 View 的位置,就算这个 View 我们在 Java 代码中没有用到,但是在约束布局中还是要定义。这样带来的弊端有几个: + + 1. 我们每次都要想好这个 ViewID 的名称叫什么,这个就有点烧脑筋了,既要符合代码规范,也要明确和突出其作用。 + + 2. 要考虑好每个 View 上下左右之间的约束关系,否则就会容易出现越界的情况,例如一个 TextView 设计图上有 5 个字,但是后台返回了 10 个字,这个时候 TextView 的控件宽度会被拉长,如果没有设置好右边的约束,极有可能出现遮盖右边 View 的情况。 + + 3. View 之间的关系会变得复杂起来,具体表现为布局一旦发生变更,例如删除或增加某一个 View,都会影响整个 ConstraintLayout 布局,因为很多约束关系会因此发生改变,并且在布局预览中就会变得错乱起来,简单通俗点来讲就是,你拆了一块瓦,很可能会导致房倒屋塌。 + + 4. 是我们无法直接在布局中无法直接预判这个 View 在 Java 代码中是否有使用到,因为每个 View 几乎都有定义 ID,要想知道这个 View 有没有用到,还是得在 Java 代码中找一找。 + +* 我的想法是:项目里面大多数页面还是比较简单的,可以结合 LinearLayout 和 FrameLayout 布局来写,并且不需要嵌套得太深,我觉得合理的嵌套是 2~3 层,如果超过 5 层可以考虑用 ConstraintLayout 布局来写,当然这种情况在实际项目中还是比较少的。 + +* 另外一个问题,就是我发现有些人写布局喜欢嵌套很多层,但是真正的情况并不是真的就需要嵌套那么多,而是这个人对这个布局的特性和属性不太熟悉而导致,正确的方式是深入学习,这样才能用好每一个布局。 + +#### 为什么不拆成多个框架来做这件事 + +* AndroidProject 其实一直有这样做,把很多组件都拆成了独立的框架,例如:权限请求框架 [XXPermissions](https://github.com/getActivity/XXPermissions),网络请求框架 [EasyHttp](https://github.com/getActivity/EasyHttp)、吐司框架 [Toaster](https://github.com/getActivity/Toaster) 等等,我都是将它抽离在 AndroidProject 之外,作为一个单独的开源项目进行开发和维护,至于说为什么还有一些代码没有抽取出来,主要原因有几点: + + 1. 和业务的耦合性高,例如 Dialog 组件引用了很多项目的基类,例如 **BaseDialog**、**BaseAdapter** 等 + + 2. 业务有定制化需求,因为 Dialog 的 UI 风格要跟随项目的设计走,所以代码如果在项目中,修改起来会非常方便,如果抽取到框架中,要怎么修改和统一 UI 风格呢?我个人认为框架不适合做 UI 定制化,因为每个产品的设计风格都不一样,就算开放再多的 API 给外部调用的人设置 UI 风格,也无法满足所有人的需求。 + +* 基于以上几点,我并不认为所有的东西都适合抽取成框架给大家用,有些东西还是跟随 **AndroidProject** 一起更新比较好。当然像权限请求这种东西,我个人觉得抽成框架是比较合适的,因为它和业务的关联性不大,更重要的是,如果某一天你觉得 **XXPermissions** 做得不够好,你随时可以在 **AndroidProject** 替换掉它,并且整个过程不需要太大的改动。 + +#### 为什么最低兼容到 Android 5.0 + +* 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 有些问题是**真硬伤**,这是一个非常**令人头疼**的问题。 + +#### 为什么不加入 XXX 功能 + +* 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/vote_1.jpg) + +* 无疑 Glide 已成大家最喜爱的图片加载框架,当然也有人使用 Fresco,但是占比极少。 + +* 那既然萝卜白菜各有所爱,那么为什么不对图片加载框架抽取成接口呢?这样不就把所有的问题都解决了? + +* 其实 [AndroidProject 10.0](https://github.com/getActivity/AndroidProject/releases/tag/10.0) 之前的版本有做过这块的内容,但是移除的原因是,抽取接口其实不难,难的是后续的扩展,例如 Glide 给我们开放了很多 API,我们最常用的是加载网络图片、加载圆角图片、加载圆形图片,但是如果是其他形状的图片呢?那就要涉及到 Glide 图形变换的 API 了,还有一个就是加载监听的事件,也需要涉及到 Glide 的 API,缓存策略,不止如此,还有很多 API 都涉及到 Glide 的 API,如果直接用 Glide 来做,我们可以轻松实现,但是如果经过一层的代码封装,那么会把框架本身的灵活性给扼杀掉。但并不是不可以实现,而是没有这个必要,就算做了付出和收益也会远远不成正比,同时也会给大家带来一定的学习成本。 + +#### 模板 架构 技术中台有什么区别 + +* AndroidProject 正式从 **安卓架构** 更名为 **安卓技术中台**,因为它符合技术中台的特性,既能够做到快速开发,同时又能保证后续维护也能快速迭代。大家可以也将技术中台理解为:模板+架构,一般写模板代码的人做不了架构设计,而做架构设计的人又不想写模板代码,那么技术中台的概念便出现了,并且结合了这两种的优点,开发和维护都兼顾到位。 + +#### 为什么不按业务来划分包名 + +* 有一些业务职责不明确,无法限定属于哪一个业务模块,并且大多数模块的类都是比较少,只有少部分的模块拥有一定数量的类,所以在一般的中小项目开发中,我更推荐以类的作用来划分包名。 + +#### 为什么没有关于列表多 type 的封装 + +* 原生的 RecyclerView.Adapter 本身就支持多 type,只需要重写适配器的 getItemType 方法即可,具体用法不做过多介绍。 + +#### 这不就是一个模板工程换成我也能写一个 + +* 想把 AndroidProject 做出来并不难,我当时只花了一两个星期,而做好它需要无限的时间和精力,我花了两年多的时间仍然还在半路之上,尽管有很多人认为它很好用,没有任何 Bug,但是在我看来还不够,因为每个人衡量标准的程度不同,我的标准是随着时间的推移和技术的提升而不断提高。具体付出了多少努力,[可以先让我们看一组数据](https://github.com/getActivity/AndroidProject/graphs/contributors): + +![](picture/help/contributors.jpg) + +* 与其说 AndroidProject 做的是模板工程,但实际我在架构设计上花费的时间和精力会更多,其实这两者我都有在做,因为我的目的只有一个,能够帮助大家更好地开发和维护项目。具体 AndroidProject 在代码设计上有什么亮点,这里我建议你看一下里面的代码,我相信你看完后会有收获的,后面我可能也会出一篇文章具体讲述 AndroidProject 的亮点。 + +#### 假设 AndroidProject 更新了该怎么升级它 + +* 原因和解释:首先纠正一点,AndroidProject 严格意义上来说,不是框架一种,而属于架构一种,架构升级本身就是一件大事,并且存在很多未知的风险点,我不推荐已使用 AndroidProject 开发的项目去做升级,因为开发和测试的成本极其高,间接能为业务带来价值其实很低,很多时候我知道大家很喜欢 AndroidProject 的代码,想用最新的代码到公司项目中去,但是我仍然不推荐你那么做,假设这是你的个人项目可以那么做,但是公司项目最好不要,因为公司和你都是要靠这个项目赚钱,谁也不希望项目出现问题,如果是公司要开发人员重构公司项目,也可以考虑那么做,毕竟这个时候的风险公司已经承担了大部分了,接下来的话只需要服从公司安排即可。 + +* 更新的方式:由于 AndroidProject 不是一个单独的框架那么简单,无法通过更新远程依赖的方式进行升级,所以只能通过替换代码的形式进行更新,需要注意的是,代码覆盖完需要经过严格的自测及测试,测试是做这件事情的关键流程,需要重视起来,对每一处功能进行详细测试,一定要详细,特别涉及到主流程的功能。 + +#### 为什么不用谷歌 ActivityResultContracts + +* ActivityResultContract是 Activity 1.2.0-alpha02 和 Fragment 1.3.0-alpha02 中新追加的新 API,但是在此之前 AndroidProject 早已经对 onActivityResult 回调进行了封装,详情请见 BaseActivity + +```java +public abstract class BaseActivity extends AppCompatActivity { + + /** Activity 回调集合 */ + private SparseArray mActivityCallbacks; + + /** + * startActivityForResult 方法优化 + */ + + public void startActivityForResult(Class clazz, OnActivityCallback callback) { + startActivityForResult(new Intent(this, clazz), null, callback); + } + + public void startActivityForResult(Intent intent, OnActivityCallback callback) { + startActivityForResult(intent, null, callback); + } + + public void startActivityForResult(Intent intent, @Nullable Bundle options, OnActivityCallback callback) { + if (mActivityCallbacks == null) { + mActivityCallbacks = new SparseArray<>(1); + } + // 请求码必须在 2 的 16 次方以内 + int requestCode = new Random().nextInt((int) Math.pow(2, 16)); + mActivityCallbacks.put(requestCode, callback); + startActivityForResult(intent, requestCode, options); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + OnActivityCallback callback; + if (mActivityCallbacks != null && (callback = mActivityCallbacks.get(requestCode)) != null) { + callback.onActivityResult(resultCode, data); + mActivityCallbacks.remove(requestCode); + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + public interface OnActivityCallback { + + /** + * 结果回调 + * + * @param resultCode 结果码 + * @param data 数据 + */ + void onActivityResult(int resultCode, @Nullable Intent data); + } +} +``` + +* 至于要不要换成谷歌出的那种呢?我们先来对比这两种的方式的用法 + +```java +// Google 的用法 +registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback() { + @Override + public void onActivityResult(ActivityResult result) { + Intent data = result.getData(); + int resultCode = result.getResultCode(); + } +}).launch(new Intent(this, HomeActivity.class)); +``` + +--- + +```java +// AndroidProject 的用法 +startActivityForResult(HomeActivity.class, new OnActivityCallback() { + @Override + public void onActivityResult(int resultCode, @Nullable Intent data) { + + } +}); +``` + +* 对这两种经过对比,得出结论如下: + + 1. 谷歌原生的没有 AndroidProject 封装得那么人性化,谷歌那种方式调用稍微麻烦一点 + + 2. 谷歌那种方式直接集成进 AndroidX 包的,要比直接在 BaseActivity 中封装要好 + + 3. AndroidProject 封装 onActivityResult 回调至少要比谷歌要早一两年,并非谷歌之后的产物 + + 4. 之前使用 AndroidProject 的人群已经习惯和记忆了那种方式,所以 API 不能删也不能改 + +* 所以并不是我不想用,而是谷歌封装得还不够好,至少在我看来还不够好,抛去 AndroidProject 封装的时间早不说,谷歌封装出来的效果也是强差人意,我感觉谷歌工程师的封装得越来越敷衍了,看起来像是在完成任务,而不是在做好一件事。 + +#### 为什么新版移除了权限申请的 AOP 注解 + +* 具体原因可以看这个 [`wurensen/gradle_plugin_android_aspectjx/issues/60`](https://github.com/wurensen/gradle_plugin_android_aspectjx/issues/60),这里就不展开讲了,我的解决方案是移除权限申请的 AOP 注解,避免后面的人踩同样的坑,如果你已经知晓问题的原因,但是就是想用怎么办?我可以把删除的代码贴出来,到底要不要加进去大家自行斟酌。 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Permissions { + + /** + * 需要申请权限的集合 + */ + String[] value(); +} +``` + +```java +@SuppressWarnings("unused") +@Aspect +public class PermissionsAspect { + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.Permissions * *(..))") + public void method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("method() && @annotation(permissions)") + public void aroundJoinPoint(ProceedingJoinPoint joinPoint, Permissions permissions) { + Activity activity = null; + + // 方法参数值集合 + Object[] parameterValues = joinPoint.getArgs(); + for (Object arg : parameterValues) { + if (!(arg instanceof Activity)) { + continue; + } + activity = (Activity) arg; + break; + } + + if (activity == null || activity.isFinishing() || activity.isDestroyed()) { + activity = ActivityManager.getInstance().getTopActivity(); + } + + if (activity == null || activity.isFinishing() || activity.isDestroyed()) { + Timber.e("The activity has been destroyed and permission requests cannot be made"); + return; + } + + requestPermissions(joinPoint, activity, permissions.value()); + } + + private void requestPermissions(ProceedingJoinPoint joinPoint, Activity activity, String[] requestPermissions) { + XXPermissions.with(activity) + .permission(requestPermissions) + .interceptor(new PermissionInterceptor()) + .description(new PermissionDescription()) + .request((grantedList, deniedList) -> { + boolean allGranted = deniedList.isEmpty(); + if (!allGranted) { + return; + } + try { + // 获得权限,执行原方法 + joinPoint.proceed(); + } catch (Throwable e) { + e.printStackTrace(); + CrashReport.postCatchedException(e); + } + }); + } +} +``` + +#### 轮子哥你怎么看待层出不穷的新技术 + +* 新东西的出现总能引起别人的好奇和尝试,但是我建议有兴趣的人可以学一下,但是如果要应用到项目中,我个人建议还是要慎重,因为纵观历史,我们不难发现,技术创新虽然很受欢迎,但是大多数都经不住时间的考验,最终一个个气尽倒下,这是因为很多新技术,表面看起来很美好,但实际上一入坑深似海。当然也有一些优秀的技术创新活了下来,但是毕竟占的是少数。 + +* 谈谈我对新技术的看法,首先我会思考这种新技术能解决什么痛点,这点非常重要,再好的技术创新,也必须得创造价值,否则就是在扯淡。有人肯定会问,什么样的技术才算有价值?对于我们 Android 程序员来讲,无非就围绕两点,开发和维护。要么在前期开发上,能发挥很大的作用,要么在后续维护上面,能体现它的优势。 + +* 还有谷歌的新技术不一定都是好的,也有一些是 **KPI 产物**,别忘了,他们也是打工的,他们也有 **KPI 考核**,为了年终奖和晋升,他们不得不卖力宣传,纵使他们知道这个东西有硬伤,但是他们也会推出来看看市场反应。所以我们看待一种新技术,不要太看重是否是大公司出品的,也不要太看重是哪个行业名人写的,我们应该要重点关注的是,产品的质量以及能带给我们带来哪些帮助,还有会带来哪些不好的影响,这个才是正确的技术价值观。 diff --git a/LICENSE b/LICENSE index d6456956..4b7bce0a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License - Version 2.0, January 2004 + Version 2.0, October 2018 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -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. diff --git a/ProjectDetails.md b/ProjectDetails.md deleted file mode 100644 index aa048aba..00000000 --- a/ProjectDetails.md +++ /dev/null @@ -1,78 +0,0 @@ -# 项目介绍 - -## Module 介绍 - -> 这里的项目 Module 关系十分简单: - -* app:关于 APP 功能实现的业务逻辑代码,集成了一些常用的框架 - -* base:定义一些常用的基类,不集成任何框架 - -* widget:自定义一些精品的 View,不集成任何框架 - -> 本项目基于最新的 Android SDK 28 编译,[点击此处查看配置](build.gradle),最低安装要求为 Android 4.0 - -> 其中 Android Studio 的版本为3.2,Gradle的版本为 4.4 - - targetSdkVersion = 28 - compileSdkVersion = 28 - buildToolsVersion = '28.0.0' - -## Activity关系 - -> 模板项目中的Activity有三层继承关系,Fragment 和 Application 继承关系也是雷同,这里不再赘述 - -* [BaseActivity](baselibrary/src/main/java/com/hjq/baselibrary/base/BaseActivity.java):继承至AppCompatActivity,主要做一些简单的代码封装 - -* [UIActivity](app/src/main/java/com/hjq/demo/common/UIActivity.java):继承至BaseActivity,主要加入了界面侧滑和状态栏沉浸式 - -* [CommonActivity](app/src/main/java/com/hjq/demo/common/CommonActivity.java):继承至UIActivity,主要实现项目中的业务逻辑代码 - -## 复制模板 - -> 考虑到 Studio 创建一个 Activity 还是比较费劲的,所以在项目中预留了可供复制的副本 - -* [CopyActivity.java](app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.java) 对应 [activity_copy.xml](app/src/main/res/layout/activity_copy.xml) - -* [CopyFragment.java](app/src/main/java/com/hjq/demo/ui/fragment/CopyFragment.java) 对应 [fragment_copy.xml](app/src/main/res/layout/fragment_copy.xml) - -## View 样式 - -* 普通的圆角 Button 样式:style="@style/ButtonStyle" - -* 普通不带圆角 Button 样式:style="@style/RectButtonStyle" - -* 普通 EditText 样式:style="@style/EditTextStyle" - -* 普通的水平分割线 View 样式:style="@style/HorizontalLineStyle" - -* 普通的垂直分割线 View 样式:style="@style/VerticalLineStyle" - -## View 使用 - -> 具体用法可以[点击此处查看示例](app/src/main/res/layout/fragment_test_b.xml) - -![](picture/2.png) - -## 框架使用 - -> 具体用法可以[点击此处查看示例](app/src/main/java/com/hjq/demo/ui/fragment/TestFragmentC.java) - -![](picture/3.png) - -## 友盟多渠道打包 - -> 具体配置可以[点击此处查看](app/build.gradle) - -![](picture/flavors_1.jpg) - -![](picture/flavors_2.jpg) - -![](picture/flavors_3.jpg) - -> 使用友盟多渠道统计时需要[更换清单文件中的key](app/src/main/AndroidManifest.xml) - - - \ No newline at end of file diff --git a/README.md b/README.md index a92ede28..5da3fd1c 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,183 @@ -# 安卓架构 - -> 博客地址:[但愿人长久,搬砖不再有](https://www.jianshu.com/p/77dd326f21dc) - -> 当我们日复一日年复一年的搬砖的时候,你是否曾想过提升一下开发效率,如果一个模板的项目摆在你的面前,你还会选择自己搭架构么 - -> 但是搭建出一个好的架构并非易事,有多少人愿意选择去做,还有多少人选择努力去做好,可能寥寥无几,但是你今天看到的,正是你所想要的,一个真正能解决你开发新项目时最大痛点的架构工程,你不需要再麻木 Copy 原有旧项目的代码,只需改动少量代码就能得到想要的效果,你会发现开发新项目其实是一件很快乐的事 - -> 已经正式投入多个公司项目实践,暂时没有发现任何问题,[点击此处下载Demo](https://raw.githubusercontent.com/getActivity/AndroidProject/master/AndroidProject.apk) - -![](picture/demo_code.png) - -#### 常用界面 - -![](picture/activity/1.png) ![](picture/activity/2.png) ![](picture/activity/3.png) - -![](picture/activity/4.png) ![](picture/activity/5.png) ![](picture/activity/6.png) - -![](picture/activity/7.png) ![](picture/activity/8.png) ![](picture/activity/9.png) - -![](picture/activity/11.png) ![](picture/activity/12.png) ![](picture/activity/13.png) - -![](picture/activity/14.png) ![](picture/activity/15.png) ![](picture/activity/16.png) - -![](picture/activity/17.png) ![](picture/activity/18.png) ![](picture/activity/19.png) - -![](picture/activity/20.png) ![](picture/activity/21.png) ![](picture/activity/22.png) - -#### 常用对话框 - -![](picture/dialog/1.png) ![](picture/dialog/2.png) ![](picture/dialog/3.png) - -![](picture/dialog/4.png) ![](picture/dialog/5.png) ![](picture/dialog/6.png) - -![](picture/dialog/7.png) ![](picture/dialog/8.png) ![](picture/dialog/9.png) - -![](picture/dialog/10.png) ![](picture/dialog/11.png) ![](picture/dialog/12.png) - -![](picture/dialog/13.png) ![](picture/dialog/14.png) ![](picture/dialog/15.png) - -#### 动图欣赏 - -![](picture/gif/1.gif) ![](picture/gif/2.gif) ![](picture/gif/3.gif) - -![](picture/gif/4.gif) ![](picture/gif/5.gif) ![](picture/gif/6.gif) - -#### 集成框架 - -* 权限请求框架:[https://github.com/getActivity/XXPermissions](https://github.com/getActivity/XXPermissions) - -* 标题栏:[https://github.com/getActivity/TitleBar](https://github.com/getActivity/TitleBar) - -* 吐司工具类:[https://github.com/getActivity/ToastUtils](https://github.com/getActivity/ToastUtils) - -* 状态栏沉浸:[https://github.com/gyf-dev/ImmersionBar](https://github.com/gyf-dev/ImmersionBar) - -* 缩放 ImageView:[https://github.com/chrisbanes/PhotoView](https://github.com/chrisbanes/PhotoView) - -* ViewPager 指示器:[https://github.com/romandanylyk/PageIndicatorView](https://github.com/romandanylyk/PageIndicatorView) - -* ButterKnife 注解:[https://github.com/JakeWharton/butterknife](https://github.com/JakeWharton/butterknife) - -* EventBus 事件:[https://github.com/greenrobot/EventBus](https://github.com/greenrobot/EventBus) - -* 内存泄漏捕捉:[https://github.com/square/leakcanary](https://github.com/square/leakcanary) - -* 本地异常捕捉:[https://github.com/Ereza/CustomActivityOnCrash](https://github.com/Ereza/CustomActivityOnCrash) - -#### 模板项目亮点,[查看详细](ProjectDetails.md) - -* 必备优秀框架:危险权限处理,标题栏控件,吐司工具类,圆形ImageView - -* 常用页面模板:启动界面,主页界面,登录界面,注册界面,关于界面,浏览器界面 - -* 集成友盟统计:集成友盟统计,并且加入了友盟多渠道打包,在发布release包时可选择渠道包 - -* 界面样式规范:项目的严格按照 Material Design 设计进行配色,统一和规范Button和EditText控件样式 - -* 常用自定义View:圆形ImageView,验证码点击倒计时View,带清除按钮的EditText,正方形的FrameLayout、LinearLayout、RelativeLayout、ImageView - -* 代码注释规范:代码严格按照谷歌级规范来做,如需寻找友盟相关的代码,全局搜索 "友盟" 即可,任何一处不关于原生的 API 都有非常完善的注释 - -#### 修复Android普遍存在的问题,[查看详细](AndroidBug.md) - -* 修复 Button 在Android 5.1 之后英文字符串自动大写的问题 - -* 修复 Button 在设置状态选择器后仍然残留按压阴影的问题 - -* 修复某些低配置机型启动页停留在白屏的时间比较长的问题 - -* 修复某些机型在 WebView 长按时布局被顶下来的问题 - -* 修复 任务栈中 首页Activity 被重复启动的问题 - -* 修正 Android 9.0 限制 Http 明文请求的问题 - -#### 作者的其他开源项目 - -* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) - -* 吐司框架:[ToastUtils](https://github.com/getActivity/ToastUtils) - -* 国际化框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) - -* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) - -* 悬浮窗框架:[XToast](https://github.com/getActivity/XToast) - -#### Android技术讨论Q群:78797078 - -#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat: - -![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) - -#### [点击查看捐赠列表](https://github.com/getActivity/Donate) - -## License - -```text -Copyright 2018 Huang JinQun - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -``` +# 安卓技术中台 + +* 项目地址:[Github](https://github.com/getActivity/AndroidProject) + +* Kotlin 版本:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) + +* 博客地址:[但愿人长久,搬砖不再有](https://www.jianshu.com/p/77dd326f21dc) + +* 当我们日复一日年复一年的搬砖的时候,你是否曾想过提升一下开发效率,如果一个通用的架构摆在你的面前,你还会选择自己搭架构么,但是搭建出一个好的架构并非易事,有多少人愿意选择去做,还有多少人选择努力去做好,可能寥寥无几,但是你今天看到的,正是你所想要的,一个真正能解决你开发新项目时最大痛点的架构工程,你不需要再麻木 Copy 原有旧项目的代码,只需改动少量代码就能得到想要的效果,你会发现开发新项目其实是一件很快乐的事。 + +* AndroidProject 已维护七年多的时间,几乎耗尽我所有的业余时间,里面的代码改了再改,改了又改,不断 Review、不断创新、不断改进、不断测试、不断优化,每天都在重复这些枯燥的步骤,但是只有这样才能把这件事做好,因为我相信把同样一件事重复做,迟早有一天可以做好。 + +* 已经正式投入到多个公司项目实践中,暂时没有发现任何问题或者 Bug,[点击下载 Apk 体验](https://github.com/getActivity/AndroidProject/releases/download/13.1/AndroidProject.apk),又或者扫码下载 + +![](picture/demo_code.png) + +#### 常用界面 + +![](picture/activity/1.jpg) ![](picture/activity/2.jpg) ![](picture/activity/3.jpg) + +![](picture/activity/4.jpg) ![](picture/activity/5.jpg) ![](picture/activity/6.jpg) + +![](picture/activity/7.jpg) ![](picture/activity/8.jpg) ![](picture/activity/9.jpg) + +![](picture/activity/10.jpg) ![](picture/activity/11.jpg) ![](picture/activity/12.jpg) + +![](picture/activity/13.jpg) ![](picture/activity/14.jpg) ![](picture/activity/15.jpg) + +![](picture/activity/16.jpg) ![](picture/activity/17.jpg) ![](picture/activity/18.jpg) + +![](picture/activity/19.jpg) ![](picture/activity/20.jpg) ![](picture/activity/21.jpg) + +![](picture/activity/22.jpg) ![](picture/activity/23.jpg) ![](picture/activity/24.jpg) + +![](picture/activity/25.jpg) ![](picture/activity/26.jpg) ![](picture/activity/27.jpg) + +------ + +![](picture/activity/28.jpg) + +![](picture/activity/29.jpg) + +![](picture/activity/30.jpg) + +![](picture/activity/31.jpg) + +![](picture/activity/32.jpg) + +![](picture/activity/33.jpg) + +![](picture/activity/34.jpg) + +![](picture/activity/35.jpg) + +![](picture/activity/36.jpg) + +#### 常用对话框 + +![](picture/dialog/1.jpg) ![](picture/dialog/2.jpg) ![](picture/dialog/3.jpg) + +![](picture/dialog/4.jpg) ![](picture/dialog/5.jpg) ![](picture/dialog/6.jpg) + +![](picture/dialog/7.jpg) ![](picture/dialog/8.jpg) ![](picture/dialog/9.jpg) + +![](picture/dialog/10.jpg) ![](picture/dialog/11.jpg) ![](picture/dialog/12.jpg) + +![](picture/dialog/13.jpg) ![](picture/dialog/14.jpg) ![](picture/dialog/15.jpg) + +![](picture/dialog/16.jpg) ![](picture/dialog/17.jpg) ![](picture/dialog/18.jpg) + +#### 动图欣赏 + +![](picture/gif/1.gif) ![](picture/gif/2.gif) ![](picture/gif/3.gif) + +![](picture/gif/4.gif) ![](picture/gif/5.gif) ![](picture/gif/6.gif) + +![](picture/gif/7.gif) ![](picture/gif/8.gif) ![](picture/gif/9.gif) + +![](picture/gif/10.gif) ![](picture/gif/11.gif) ![](picture/gif/12.gif) + +#### 项目亮点 + +* App 优化:已经进行了全面的内存优化、布局优化、代码优化、瘦身优化,并且对结果进行了严格的长久测试。 + +* 代码规范:参照 Android SDK 、Support 源码和参考阿里巴巴的代码规范文档对代码进行命名,并对难点代码进行了注释,对重点代码进行了说明。 + +* 代码统一:对项目中常见的代码进行了封装,或是封装到基类中、或是封装到工具类中、或者封装到框架中,不追求过度封装,根据实际场景和代码维护性考虑,尽量保证同一个功能的代码在项目中不重复。 + +* 敏捷开发:一个 App 大概率会出现的功能已经写好,对项目的敏捷开发起到了至关重要的作用,可用于新项目开发或者旧项目重构,可将开发周期缩短近一半的时间,并且后续不会因为前期的快速开发而留下成堆的技术遗留问题,万丈高楼平地起,AndroidProject 属于基建工程,而在软件行业我们称之为技术中台。 + +* 无任何瑕疵:对小屏手机、全面屏手机、带虚拟按键手机进行了适配和优化,确保每一个界面细节都能处理到位、每一个功能细节都能符合大众的需求、乃至每一行代码都能贴合 Android 程序员的审美观。 + +* 兼容性优良:在此感谢开源道路上给予我支持和帮助的小伙伴,一个人一台机在兼容性面前无能为力,而在几百人几百台机面前却不是问题。如果没有这些的测试,有些问题我一个人可能这辈子都发现不了,纵使代码写得再好,逻辑再严谨,没有经过大众的验证,无异于纸上谈兵。 + +* 优秀的代码设计:AndroidProject 对 startActivityForResult 的设计进行了改良,使得可以直接在方法上传入监听对象,这样我们就不需要重写 onActivityResult 方法来拿到回调,另外原生的 startActivityForResult 还需要传 requestCode 参数,而 AndroidProject 会自动帮你生成这个 requestCode 码,并在 onActivityResult 进行判断,如果满足条件,那么就会回调外层传入的监听对象。然而这只是冰山一角,更多优秀的代码设计还需要你通过阅读 AndroidProject 源码的形式来发掘,在这里不再细说。 + +#### [代码规范文档请点击这里查看](https://github.com/getActivity/AndroidCodeStandard) + +#### [版本适配文档请点击这里查看](https://github.com/getActivity/AndroidVersionAdapter) + +#### [常见问题解答请点击这里查看](HelpDoc.md) + +#### 使用案例 + +![](picture/douyin/douyin_logo.png) + +[![](picture/douyin/douyin_open_source_agreement.jpg)](https://aweme.snssdk.com/draft/douyin_agreement/open_source.html?hide_nav_bar=1&disable_auto_expose=1&font_scale=1.0&is_init_login=1&contact_permission=0&push_permission=1&bounce_disable=1&is_tab=0&launch_method=enter_launch&show_loading=1) + +#### 作者的其他开源项目 + +* 安卓技术中台 Kt 版:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) ![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) + +* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) + +* 吐司框架:[Toaster](https://github.com/getActivity/Toaster) ![](https://img.shields.io/github/stars/getActivity/Toaster.svg) ![](https://img.shields.io/github/forks/getActivity/Toaster.svg) + +* 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) ![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg) ![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) + +* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) ![](https://img.shields.io/github/stars/getActivity/TitleBar.svg) ![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) + +* 悬浮窗框架:[EasyWindow](https://github.com/getActivity/EasyWindow) ![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg) ![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) + +* 设备兼容框架:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) + +* ShapeView 框架:[ShapeView](https://github.com/getActivity/ShapeView) ![](https://img.shields.io/github/stars/getActivity/ShapeView.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) + +* ShapeDrawable 框架:[ShapeDrawable](https://github.com/getActivity/ShapeDrawable) ![](https://img.shields.io/github/stars/getActivity/ShapeDrawable.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeDrawable.svg) + +* 语种切换框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) ![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg) ![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) + +* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) ![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg) ![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) + +* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) ![](https://img.shields.io/github/stars/getActivity/Logcat.svg) ![](https://img.shields.io/github/forks/getActivity/Logcat.svg) + +* 嵌套滚动布局框架:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout) ![](https://img.shields.io/github/stars/getActivity/NestedScrollLayout.svg) ![](https://img.shields.io/github/forks/getActivity/NestedScrollLayout.svg) + +* Android 命令行工具集:[AndroidCmdTools](https://github.com/getActivity/AndroidCmdTools) ![](https://img.shields.io/github/stars/getActivity/AndroidCmdTools.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCmdTools.svg) + +* Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) ![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) + +* Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) + +* Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) + +* Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) + +* Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) + +* 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) + +* AI 资源大汇总:[AiIndex](https://github.com/getActivity/AiIndex) ![](https://img.shields.io/github/stars/getActivity/AiIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AiIndex.svg) + +* 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson) ![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg) ![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) + +* Markdown 语法文档:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) + +#### 微信公众号:Android轮子哥 + +![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png) + +#### Android 技术 Q 群:10047167 + +#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:([点击查看捐赠列表](https://github.com/getActivity/Donate)) + +![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) + +## License + +```text +Copyright 2018 Huang JinQun + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 82253098..4e5d5fdf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,213 +1,238 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion rootProject.ext.compileVersion - - // 使用 JDK 1.8 - compileOptions { - targetCompatibility JavaVersion.VERSION_1_8 - sourceCompatibility JavaVersion.VERSION_1_8 - } - - defaultConfig { - // 无痛修改包名:https://www.jianshu.com/p/17327e191d2e - applicationId "com.hjq.demo" - minSdkVersion 14 - targetSdkVersion rootProject.ext.targetVersion - versionCode 10 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - // 仅保留中文语种的资源 - resConfig 'zh' - - // 仅保留 xxhdpi 图片资源(目前主流分辨率 1920 * 1080) - resConfig 'xxhdpi' - - // 仅保留两种架构的 so 库 - ndk { - // armeabi:已经淘汰(0%) - // armeabi-v7a:曾经主流的架构平台(20%) - // arm64-v8a:目前主流架构平台(80%) - abiFilters "armeabi-v7a", "arm64-v8a" - } - - // 开启 Dex 分包 - //multiDexEnabled true - - // 混淆配置 - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-app.pro' - - javaCompileOptions { - annotationProcessorOptions { - // EventBus Apt 索引类生成位置 - arguments = [ eventBusIndex : applicationId + '.MyEventBusIndex' ] - } - } - } - - // APK 签名的那些事:https://www.jianshu.com/p/a1f8e5896aa2 - signingConfigs { - debug { - storeFile file(StoreFile) - storePassword StorePassword - keyAlias KeyAlias - keyPassword KeyPassword - } - release { - storeFile file(StoreFile) - storePassword StorePassword - keyAlias KeyAlias - keyPassword KeyPassword - } - } - - buildTypes { - release { - // 移除无用的资源文件 - shrinkResources true - // ZipAlign 优化 - zipAlignEnabled true - // 设置混淆 - minifyEnabled true - // 正式环境签名 - signingConfig signingConfigs.release - // 正式环境下的 BuglyId - buildConfigField "String", "BUGLY_ID", "\"请自行替换 Bugly 上面的 AppID\"" - } - - debug { - // 移除无用的资源文件 - shrinkResources false - // ZipAlign 优化 - zipAlignEnabled false - // 设置混淆 - minifyEnabled false - // 开发环境签名 - signingConfig signingConfigs.debug - // 开发环境下的 BuglyId - buildConfigField "String", "BUGLY_ID", "\"请自行替换 Bugly 上面的 AppID\"" - } - } - - // 默认渠道名 - flavorDimensions "default" - // 友盟多渠道打包 - productFlavors { - tencent {} // 应用宝 - baidu {} // 百度 - xiaomi {} // 小米 - huawei {} // 华为 - - productFlavors.all { flavor -> - flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] - } - } - - // JNI 目录 - sourceSets { - main { - jniLibs.srcDirs = ['libs'] - } - } - - // 执行配置 - applicationVariants.all { variant -> - - // Apk 输出配置 - variant.outputs.all { output -> - def appName = "AndroidProject" - if (variant.buildType.name == 'debug') { - outputFileName = appName + '_v' + versionName + '_' + variant.buildType.name + '.apk' - } else { - outputFileName = appName + '_v' + versionName + '_' + new Date().format("yyyyMMdd") + '_' + variant.productFlavors[0].name + '_' + variant.buildType.name + '.apk' - } - } - - // AndroidManifest 输出配置 - variant.outputs[0].processManifest.doLast { - def manifestFile = "${manifestOutputDirectory}/AndroidManifest.xml" - def updatedContent = new File(manifestFile).getText('UTF-8') - .replaceAll("UMENG_APPKEY_VALUE", "5cb16d93570df399fd0014e2") // 友盟 AppKey - .replaceAll("QQ_APPID_VALUE", "100424468") // QQ AppId - .replaceAll("QQ_APPKEY_VALUE", "c7394704798a158208a74ab60104f0ba") // QQ Key - .replaceAll("WX_APPID_VALUE", "wxdc1e388c3822c80b") // 微信 AppId - .replaceAll("WX_APPKEY_VALUE", "3baf1193c85774b3fd9d18447d76cab0") // 微信 Key - new File(manifestFile).write(updatedContent, 'UTF-8') - } - } -} - -// api 与 implementation 的区别:https://www.jianshu.com/p/8962d6ba936e -dependencies { - // 依赖 libs 目录下所有 jar 包 - implementation fileTree(include: ['*.jar'], dir: 'libs') - // 依赖 libs 目录下所有 aar 包 - implementation fileTree(include: ['*.aar'], dir: 'libs') - - // 基础库(不包任何第三方框架) - implementation project(':base') - // 自定义 View - implementation project(':widget') - // Glide 隔离 - implementation project(':image') - // 友盟隔离 - implementation project(':umeng') - - // 谷歌 Support 包 - implementation "androidx.appcompat:appcompat:$rootProject.ext.appcompatVersion" - implementation "com.google.android.material:material:$rootProject.ext.materialVersion" - - // Dex 分包,解决 64k 方法问题 - //implementation 'androidx.multidex:multidex:2.0.1' - - // ButterKnife 注解库:https://github.com/JakeWharton/butterknife - implementation 'com.jakewharton:butterknife:10.1.0' - annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0' - - // EventBus 事件总线:https://github.com/greenrobot/EventBus - implementation "org.greenrobot:eventbus:3.1.1" - annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1' - - // 状态栏沉浸:https://github.com/gyf-dev/ImmersionBar - implementation 'com.gyf.immersionbar:immersionbar:3.0.0' - - // 权限请求框架:https://github.com/getActivity/XXPermissions - implementation 'com.hjq:xxpermissions:6.0' - - // 标题栏:https://github.com/getActivity/TitleBar - implementation 'com.hjq:titlebar:6.0' - - // 吐司工具类:https://github.com/getActivity/ToastUtils - implementation 'com.hjq:toast:8.0' - - // 支持放大缩放的 ImageView:https://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.0.1' - implementation 'com.tencent.bugly:nativecrashreport:3.7.1' - - // 本地异常捕捉框架:https://github.com/Ereza/CustomActivityOnCrash - implementation 'cat.ereza:customactivityoncrash:2.2.0' - - // 内存泄漏捕捉:https://github.com/square/leakcanary - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' - - // 网络请求(待发布):https://github.com/getActivity/EasyHttp - - // 国际化:https://github.com/getActivity/MultiLanguages - // 悬浮窗:https://github.com/getActivity/XToast - // 上拉刷新下拉加载:https://github.com/scwang90/SmartRefreshLayout - // 工具类:https://github.com/Blankj/AndroidUtilCode - // 轮播图:https://github.com/bingoogolapple/BGABanner-Android - // 二维码:https://github.com/bingoogolapple/BGAQRCode-Android - // 第三方支付:https://github.com/getActivity/RxPay - // Log 打印:https://github.com/elvishew/XLog - // 图片压缩:https://github.com/Curzibn/Luban - // 对象存储:https://github.com/leavesC/DoKV - // 数据注入:https://github.com/JumeiRdGroup/Parceler +plugins { + alias(libs.plugins.application) + alias(libs.plugins.kotlin) + alias(libs.plugins.aop) + alias(libs.plugins.easyLauncher) +} + +apply from : '../common.gradle' + +// Android 代码规范文档:https://github.com/getActivity/AndroidCodeStandard +android { + namespace 'com.hjq.demo' + + buildFeatures { + // 是否生成 BuildConfig 类 + buildConfig true + } + + // 资源目录存放指引:https://developer.android.google.cn/guide/topics/resources/providing-resources + defaultConfig { + + // 无痛修改包名:https://www.jianshu.com/p/17327e191d2e + applicationId 'com.hjq.demo' + + // 仅保留中文语种的资源 + resConfigs 'zh' + + // 仅保留 xxhdpi 图片资源(目前主流分辨率 1920 * 1080) + resConfigs 'xxhdpi' + + // 混淆配置 + proguardFiles 'proguard-sdk.pro', 'proguard-app.pro' + + // 日志开关 + buildConfigField('boolean', 'LOG_ENABLE', '' + LOG_ENABLE + '') + // 主机地址 + buildConfigField('String', 'HOST_URL', '"' + HOST_URL + '"') + // BuglyId + buildConfigField('String', 'BUGLY_ID', '"' + BUGLY_ID + '"') + // BuglyKey + buildConfigField('String', 'BUGLY_KEY', '"' + BUGLY_KEY + '"') + + // 仅保留 arm64-v8a 架构(需要注意的是 mmkv 库在 2.0 及之后的版本已经不支持在 32 位的机器上面运行) + ndk { + abiFilters 'arm64-v8a' + } + } + + sourceSets { + main { + // res 资源目录配置 + res.srcDirs( + 'src/main/res', + 'src/main/res-common' + ) + } + } + + // Apk 签名的那些事:https://www.jianshu.com/p/a1f8e5896aa2 + signingConfigs { + config { + storeFile file(STORE_FILE) + storePassword STORE_PASSWORD + keyAlias KEY_ALIAS + keyPassword KEY_PASSWORD + } + } + + // 构建配置:https://developer.android.google.cn/studio/build/build-variants + buildTypes { + + debug { + // 给包名添加后缀 + applicationIdSuffix '.debug' + // 调试模式开关 + debuggable true + jniDebuggable true + // 移除无用的资源 + shrinkResources false + // 代码混淆开关 + minifyEnabled false + // 签名信息配置 + signingConfig signingConfigs.config + // 添加清单占位符 + addManifestPlaceholders([ + 'app_name' : '@string/app_name_debug' + ]) + } + + preview.initWith(debug) + preview { + applicationIdSuffix '' + // 添加清单占位符 + addManifestPlaceholders([ + 'app_name' : '@string/app_name_preview' + ]) + } + + release { + // 调试模式开关 + debuggable false + jniDebuggable false + // 移除无用的资源 + shrinkResources true + // 代码混淆开关 + minifyEnabled true + // 签名信息配置 + signingConfig signingConfigs.config + // 添加清单占位符 + addManifestPlaceholders([ + 'app_name' : '@string/app_name' + ]) + } + } + + packagingOptions { + // 剔除这个包下的所有文件(不会移除签名信息) + exclude 'META-INF/*******' + } + + // AOP 配置(exclude 和 include 二选一) + androidAopConfig { + // 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突) + // exclude 'androidx', 'com.google', 'com.squareup', 'org.apache', 'com.alipay', 'com.taobao', 'versions.9' + // 只对以下包名做 AOP 处理 + include android.defaultConfig.applicationId + } + + applicationVariants.configureEach { variant -> + // apk 输出文件名配置 + variant.outputs.configureEach { output -> + outputFileName = rootProject.getName() + '_v' + variant.versionName + '_' + variant.buildType.name + if (variant.buildType.name == buildTypes.release.getName()) { + outputFileName += '_' + new Date().format('MMdd') + } + outputFileName += '.apk' + } + } +} + +// 应用启动图标配置 +easylauncher { + buildTypes { + preview { + filters = [ + customRibbon(label: "preview", position: "topRight", + ribbonColor: "#b65656", labelColor: "#FFFFFF") + ] + } + debug { + filters = [ + customRibbon(label: "debug", position: "topRight", + ribbonColor: "#b65656", labelColor: "#FFFFFF") + ] + } + release { + enable false + } + } +} + +// 添加构建依赖项:https://developer.android.google.cn/studio/build/dependencies +// api 与 implementation 的区别:https://www.jianshu.com/p/8962d6ba936e +dependencies { + implementation project(':library:core') + implementation project(':library:base') + implementation project(':library:smallestWidth') + implementation project(':library:customWidget') + implementation project(':library:umengSdk') + + implementation libs.deviceCompat + implementation libs.xxPermissions + + implementation libs.titleBar + + implementation libs.toaster + + implementation libs.easyHttp + implementation libs.okHttp + + implementation(libs.gsonFactory) { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-reflect' + } + implementation(libs.gson) + implementation libs.kotlinReflect + + implementation libs.shapeView + implementation libs.shapeDrawable + + implementation libs.nestedScrollLayout + + implementation libs.glide + annotationProcessor libs.glideCompiler + + implementation libs.immersionBar + + implementation libs.photoView + + implementation libs.bugly + + implementation libs.lottie + + implementation libs.refreshLayoutKernel + implementation libs.refreshHeaderMaterial + + implementation libs.timber + + implementation libs.circleIndicator + + implementation(libs.mmkvStatic) { + exclude group: 'androidx.annotation', module: 'annotation' + } + + implementation libs.softInputEvent + implementation libs.androidAopCore + annotationProcessor libs.androidAopApt + + debugImplementation libs.leakCanary + previewImplementation libs.leakCanary + + debugImplementation libs.chucker + previewImplementation libs.chucker + releaseImplementation libs.chuckerNoOp + + // 多语种:https://github.com/getActivity/MultiLanguages + // 悬浮窗:https://github.com/getActivity/EasyWindow + // 日志输出:https://github.com/getActivity/Logcat + // 工具类:https://github.com/Blankj/AndroidUtilCode + // 轮播图:https://github.com/bingoogolapple/BGABanner-Android + // 二维码:https://github.com/bingoogolapple/BGAQRCode-Android + // 跑马灯:https://github.com/sunfusheng/MarqueeView + // 对象注解:https://www.jianshu.com/p/f1f888e4a35f + // 对象存储:https://github.com/leavesC/DoKV + // 多渠道打包:https://github.com/Meituan-Dianping/walle + // 设备唯一标识:http://msa-alliance.cn/col.jsp?id=120 + // 嵌套滚动容器:https://github.com/donkingliang/ConsecutiveScroller + // 隐私调用监控:https://github.com/allenymt/PrivacySentry } \ No newline at end of file diff --git a/app/gradle.properties b/app/gradle.properties index 0caf1444..db1dd0a7 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1,5 +1,4 @@ -# ǩļԿϢ -StoreFile = AppSignature.jks -StorePassword = AndroidProject -KeyAlias = AndroidProject -KeyPassword = AndroidProject \ No newline at end of file +STORE_FILE = AppSignature.jks +STORE_PASSWORD = AndroidProject +KEY_ALIAS = AndroidProject +KEY_PASSWORD = AndroidProject \ No newline at end of file diff --git a/app/proguard-app.pro b/app/proguard-app.pro index ae479810..58761e46 100644 --- a/app/proguard-app.pro +++ b/app/proguard-app.pro @@ -1,39 +1,21 @@ # 忽略警告 --ignorewarning +#-ignorewarning # 混淆保护自己项目的部分代码以及引用的第三方jar包 -#-libraryjars libs/umeng-analytics-v5.2.4.jar +#-libraryjars libs/xxxxxxxxx.jar -# 标题栏框架 -#-keep class com.hjq.bar.** {*;} - -# 吐司框架 -#-keep class com.hjq.toast.** {*;} - -# 权限请求框架 -#-keep class com.hjq.permissions.** {*;} - -# 不混淆 WebView 的 JS 接口 --keepattributes *JavascriptInterface* -# 不混淆 WebView 的类的所有的内部类 --keepclassmembers class com.veidy.activity.WebViewActivity$*{ - *; +# 不混淆这个包下的类 +-keep class com.hjq.demo.http.api.** { + ; } -# 不混淆 WebChromeClient 中的 openFileChooser 方法 --keepclassmembers class * extends android.webkit.WebChromeClient{ - public void openFileChooser(...); -} - -# EventBus3 --keepattributes *Annotation* --keepclassmembers class ** { - @org.greenrobot.eventbus.Subscribe ; +-keep class com.hjq.demo.http.response.** { + ; } --keep enum org.greenrobot.eventbus.ThreadMode { *; } --keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent { - (java.lang.Throwable); +-keep class com.hjq.demo.http.model.** { + ; } -# Bugly --dontwarn com.tencent.bugly.** --keep public class com.tencent.bugly.**{*;} \ No newline at end of file +# 不混淆被 Log 注解的方法信息 +-keepclassmembernames class ** { + @com.hjq.demo.aop.Log ; +} \ No newline at end of file diff --git a/app/proguard-sdk.pro b/app/proguard-sdk.pro new file mode 100644 index 00000000..03bec5c4 --- /dev/null +++ b/app/proguard-sdk.pro @@ -0,0 +1,8 @@ +# EasyHttp +# 不混淆实现 OnHttpListener 接口的类,必须要加上此规则,否则会导致泛型解析失败 +-keep class * implements com.hjq.http.listener.OnHttpListener { + *; +} +-keep class * extends com.hjq.http.model.ResponseClass { + *; +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b9ed121f..4202a811 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,170 +1,278 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/ImmersionAction.java b/app/src/main/java/com/hjq/demo/action/ImmersionAction.java new file mode 100644 index 00000000..dcfb0ff7 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/ImmersionAction.java @@ -0,0 +1,30 @@ +package com.hjq.demo.action; + +import android.view.View; +import androidx.annotation.Nullable; +import com.hjq.bar.OnTitleBarListener; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2025/12/13 + * desc : 沉浸式意图 + */ +public interface ImmersionAction extends OnTitleBarListener { + + /** + * 获取需要沉浸的顶部 View 对象 + */ + @Nullable + default View getImmersionTopView() { + return null; + } + + /** + * 获取需要沉浸的底部 View 对象 + */ + @Nullable + default View getImmersionBottomView() { + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/StatusAction.java b/app/src/main/java/com/hjq/demo/action/StatusAction.java new file mode 100644 index 00000000..eefdd2d1 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/StatusAction.java @@ -0,0 +1,106 @@ +package com.hjq.demo.action; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import androidx.annotation.DrawableRes; +import androidx.annotation.RawRes; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; +import com.hjq.demo.R; +import com.hjq.demo.widget.StatusLayout; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/08 + * desc : 状态布局意图 + */ +public interface StatusAction { + + /** + * 获取状态布局 + */ + StatusLayout acquireStatusLayout(); + + /** + * 显示加载中 + */ + default void showLoading() { + showLoading(R.raw.loading); + } + + default void showLoading(@RawRes int id) { + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } + layout.show(); + layout.setAnimResource(id); + layout.setHint(""); + layout.setOnRetryListener(null); + } + + /** + * 显示加载完成 + */ + default void showComplete() { + StatusLayout layout = acquireStatusLayout(); + if (layout == null || !layout.isShow()) { + return; + } + layout.hide(); + } + + /** + * 显示空提示 + */ + default void showEmpty() { + showLayout(R.drawable.status_empty_ic, R.string.status_layout_no_data, null); + } + + /** + * 显示错误提示 + */ + default void showError(StatusLayout.OnRetryListener listener) { + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } + 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.status_network_ic, R.string.status_layout_error_network, listener); + return; + } + } + showLayout(R.drawable.status_error_ic, R.string.status_layout_error_request, listener); + } + + /** + * 显示自定义提示 + */ + default void showLayout(@DrawableRes int drawableId, @StringRes int stringId, StatusLayout.OnRetryListener listener) { + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } + Context context = layout.getContext(); + showLayout(ContextCompat.getDrawable(context, drawableId), context.getString(stringId), listener); + } + + default void showLayout(Drawable drawable, CharSequence hint, StatusLayout.OnRetryListener listener) { + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } + layout.show(); + layout.setIcon(drawable); + layout.setHint(hint); + layout.setOnRetryListener(listener); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/TitleBarAction.java b/app/src/main/java/com/hjq/demo/action/TitleBarAction.java new file mode 100644 index 00000000..8c64d8fa --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/TitleBarAction.java @@ -0,0 +1,185 @@ +package com.hjq.demo.action; + +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import com.hjq.bar.OnTitleBarListener; +import com.hjq.bar.TitleBar; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/08 + * desc : 标题栏意图 + */ +public interface TitleBarAction extends OnTitleBarListener { + + /** + * 获取标题栏对象 + */ + @Nullable + TitleBar acquireTitleBar(); + + /** + * 设置标题栏的标题 + */ + default void setTitle(@StringRes int id) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setTitle(id); + } + + /** + * 设置标题栏的标题 + */ + default void setTitle(CharSequence title) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setTitle(title); + } + + /** + * 设置标题栏的左标题 + */ + default void setLeftTitle(int id) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setLeftTitle(id); + } + + default void setLeftTitle(CharSequence text) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setLeftTitle(text); + } + + default CharSequence getLeftTitle() { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return ""; + } + return titleBar.getLeftTitle(); + } + + /** + * 设置标题栏的右标题 + */ + default void setRightTitle(int id) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setRightTitle(id); + } + + default void setRightTitle(CharSequence text) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setRightTitle(text); + } + + default CharSequence getRightTitle() { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return ""; + } + return titleBar.getRightTitle(); + } + + /** + * 设置标题栏的左图标 + */ + default void setLeftIcon(int id) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setLeftIcon(id); + } + + default void setLeftIcon(Drawable drawable) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setLeftIcon(drawable); + } + + @Nullable + default Drawable getLeftIcon() { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return null; + } + return titleBar.getLeftIcon(); + } + + /** + * 设置标题栏的右图标 + */ + default void setRightIcon(int id) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setRightIcon(id); + } + + default void setRightIcon(Drawable drawable) { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; + } + titleBar.setRightIcon(drawable); + } + + @Nullable + default Drawable getRightIcon() { + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return null; + } + return titleBar.getRightIcon(); + } + + /** + * 递归获取 ViewGroup 中的 TitleBar 对象 + */ + default TitleBar findTitleBar(@Nullable View contentView) { + if (contentView == null) { + return null; + } + if (contentView instanceof TitleBar) { + return (TitleBar) contentView; + } + if (contentView instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) contentView; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View view = viewGroup.getChildAt(i); + if ((view instanceof TitleBar)) { + return (TitleBar) view; + } + + if (view instanceof ViewGroup) { + TitleBar titleBar = findTitleBar(view); + if (titleBar != null) { + return titleBar; + } + } + } + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/ToastAction.java b/app/src/main/java/com/hjq/demo/action/ToastAction.java new file mode 100644 index 00000000..49cd2887 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/ToastAction.java @@ -0,0 +1,25 @@ +package com.hjq.demo.action; + +import androidx.annotation.StringRes; +import com.hjq.toast.Toaster; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/08 + * desc : 吐司意图 + */ +public interface ToastAction { + + default void toast(CharSequence text) { + Toaster.show(text); + } + + default void toast(@StringRes int id) { + Toaster.show(id); + } + + default void toast(Object object) { + Toaster.show(object); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/CheckNet.java b/app/src/main/java/com/hjq/demo/aop/CheckNet.java new file mode 100644 index 00000000..77b9fd80 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/CheckNet.java @@ -0,0 +1,18 @@ +package com.hjq.demo.aop; + +import com.flyjingfish.android_aop_annotation.anno.AndroidAopPointCut; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/01/11 + * desc : 网络检测注解 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@AndroidAopPointCut(CheckNetCut.class) +public @interface CheckNet {} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/CheckNetCut.java b/app/src/main/java/com/hjq/demo/aop/CheckNetCut.java new file mode 100644 index 00000000..3dabd48f --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/CheckNetCut.java @@ -0,0 +1,36 @@ +package com.hjq.demo.aop; + +import android.app.Application; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import com.flyjingfish.android_aop_annotation.ProceedJoinPoint; +import com.flyjingfish.android_aop_annotation.base.BasePointCut; +import com.hjq.core.manager.ActivityManager; +import com.hjq.demo.R; +import com.hjq.toast.Toaster; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/01/11 + * desc : 网络检测切面 + */ +public class CheckNetCut implements BasePointCut { + + @SuppressWarnings("deprecation") + @Override + public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull CheckNet anno) throws Throwable { + Application application = ActivityManager.getInstance().getApplication(); + ConnectivityManager manager = ContextCompat.getSystemService(application, ConnectivityManager.class); + if (manager != null) { + NetworkInfo info = manager.getActiveNetworkInfo(); + if (info == null || !info.isConnected()) { + Toaster.show(R.string.common_network_hint); + return null; + } + } + return joinPoint.proceed(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/Log.java b/app/src/main/java/com/hjq/demo/aop/Log.java new file mode 100644 index 00000000..269da8e5 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/Log.java @@ -0,0 +1,21 @@ +package com.hjq.demo.aop; + +import com.flyjingfish.android_aop_annotation.anno.AndroidAopPointCut; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : Debug 日志注解 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@AndroidAopPointCut(LogCut.class) +public @interface Log { + + String value() default "AOPLog"; +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/LogCut.java b/app/src/main/java/com/hjq/demo/aop/LogCut.java new file mode 100644 index 00000000..34a5d46c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/LogCut.java @@ -0,0 +1,84 @@ +package com.hjq.demo.aop; + +import android.annotation.SuppressLint; +import android.os.Looper; +import android.os.Trace; +import androidx.annotation.NonNull; +import com.flyjingfish.android_aop_annotation.ProceedJoinPoint; +import com.flyjingfish.android_aop_annotation.base.BasePointCut; +import java.util.concurrent.TimeUnit; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : 日志切面 + */ +public class LogCut implements BasePointCut { + + @Override + public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull Log anno) throws Throwable { + enterMethod(joinPoint, anno); + long startNanos = System.nanoTime(); + Object result = joinPoint.proceed(); + long stopNanos = System.nanoTime(); + exitMethod(joinPoint, anno, result, TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos)); + return result; + } + + @SuppressLint("UnclosedTrace") + private void enterMethod(ProceedJoinPoint joinPoint, Log log) { + String className = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass().getName() : ""; + String methodName = joinPoint.getTargetMethod().getName(); + String[] parameterNames = null; + Object[] parameterValues = joinPoint.getArgs(); + + StringBuilder builder = getMethodLogInfo(className, methodName, parameterNames, parameterValues); + log(log.value(), builder.toString()); + final String section = builder.substring(2); + Trace.beginSection(section); + } + + @NonNull + private StringBuilder getMethodLogInfo(String className, String methodName, String[] parameterNames, Object[] parameterValues) { + StringBuilder builder = new StringBuilder("\u21E2 "); + builder.append(className).append(".").append(methodName).append('('); + if (parameterValues != null && parameterNames != null) { + for (int i = 0; i < parameterValues.length; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(parameterNames[i]).append('='); + builder.append(parameterValues[i]); + } + } + builder.append(')'); + if (Looper.myLooper() != Looper.getMainLooper()) { + builder.append(" [Thread:\"").append(Thread.currentThread().getName()).append("\"]"); + } + return builder; + } + + private void exitMethod(ProceedJoinPoint joinPoint, Log log, Object result, long lengthMillis) { + Trace.endSection(); + String className = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass().getName() : ""; + String methodName = joinPoint.getTargetMethod().getName(); + StringBuilder builder = new StringBuilder("\u21E0 ") + .append(className) + .append('.') + .append(methodName) + .append(" [") + .append(lengthMillis) + .append("ms]"); + if (result != null) { + builder.append(" = ").append(result); + } + log(log.value(), builder.toString()); + } + + private void log(String tag, String msg) { + Timber.tag(tag); + Timber.d(msg); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/SingleClick.java b/app/src/main/java/com/hjq/demo/aop/SingleClick.java new file mode 100644 index 00000000..6bc902c2 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/SingleClick.java @@ -0,0 +1,24 @@ +package com.hjq.demo.aop; + +import com.flyjingfish.android_aop_annotation.anno.AndroidAopPointCut; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : 防重复点击注解 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@AndroidAopPointCut(SingleClickCut.class) +public @interface SingleClick { + + /** + * 快速点击的间隔 + */ + long value() default 1000; +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/SingleClickCut.java b/app/src/main/java/com/hjq/demo/aop/SingleClickCut.java new file mode 100644 index 00000000..1ff65982 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/SingleClickCut.java @@ -0,0 +1,49 @@ +package com.hjq.demo.aop; + +import androidx.annotation.NonNull; +import com.flyjingfish.android_aop_annotation.ProceedJoinPoint; +import com.flyjingfish.android_aop_annotation.base.BasePointCut; +import org.jetbrains.annotations.Nullable; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : 防重复点击切面 + */ +public class SingleClickCut implements BasePointCut { + + private static long lastTime; + + @Nullable + private static String lastTag; + + @Override + public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull SingleClick anno) throws Throwable { + String className = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass().getName() : ""; + String methodName = joinPoint.getTargetMethod().getName(); + Object[] parameterValues = joinPoint.getArgs(); + + StringBuilder builder = new StringBuilder(className).append(".").append(methodName).append("("); + for (int i = 0; i < (parameterValues != null ? parameterValues.length : 0); i++) { + if (i == 0) { + builder.append(parameterValues[i]); + } else { + builder.append(", ").append(parameterValues[i]); + } + } + builder.append(")"); + String tag = builder.toString(); + + long now = System.currentTimeMillis(); + if (now - lastTime < anno.value() && tag.equals(lastTag)) { + Timber.tag("SingleClick"); + Timber.i("%d 毫秒内发生快速点击:%s", anno.value(), tag); + return null; + } + lastTime = now; + lastTag = tag; + return joinPoint.proceed(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppActivity.java b/app/src/main/java/com/hjq/demo/app/AppActivity.java new file mode 100644 index 00000000..53b7db67 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppActivity.java @@ -0,0 +1,270 @@ +package com.hjq.demo.app; + +import android.content.Intent; +import android.graphics.Insets; +import android.view.View; +import android.view.View.OnApplyWindowInsetsListener; +import android.view.WindowInsets; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import com.gyf.immersionbar.ImmersionBar; +import com.hjq.bar.TitleBar; +import com.hjq.base.BaseActivity; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.demo.action.ImmersionAction; +import com.hjq.demo.action.TitleBarAction; +import com.hjq.demo.action.ToastAction; +import com.hjq.demo.http.model.HttpData; +import com.hjq.demo.ui.dialog.common.WaitDialog; +import com.hjq.http.config.IRequestApi; +import com.hjq.http.listener.OnHttpListener; +import com.hjq.umeng.sdk.UmengClient; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : Activity 业务基类 + */ +public abstract class AppActivity extends BaseActivity + implements ToastAction, TitleBarAction, ImmersionAction, OnHttpListener { + + /** 标题栏对象 */ + private TitleBar mTitleBar; + /** 状态栏沉浸 */ + private ImmersionBar mImmersionBar; + + /** 加载对话框 */ + private WaitDialog.Builder mDialog; + /** 对话框数量 */ + private int mDialogCount; + + /** + * 当前加载对话框是否在显示中 + */ + public boolean isShowDialog() { + return mDialog != null && mDialog.isShowing(); + } + + /** + * 显示加载对话框 + */ + public void showLoadingDialog() { + showLoadingDialog(getString(R.string.common_loading)); + } + + public void showLoadingDialog(String message) { + if (isFinishing() || isDestroyed()) { + return; + } + + mDialogCount++; + postDelayed(() -> { + if (mDialogCount <= 0 || isFinishing() || isDestroyed()) { + return; + } + + if (mDialog == null) { + mDialog = new WaitDialog.Builder(this) + .setCancelable(false); + } + mDialog.setMessage(message); + if (!mDialog.isShowing()) { + mDialog.show(); + } + }, 300); + } + + /** + * 隐藏加载对话框 + */ + public void hideLoadingDialog() { + if (isFinishing() || isDestroyed()) { + return; + } + + if (mDialogCount > 0) { + mDialogCount--; + } + + if (mDialogCount != 0 || mDialog == null || !mDialog.isShowing()) { + return; + } + + mDialog.dismiss(); + } + + @Override + protected void initLayout() { + super.initLayout(); + + TitleBar titleBar = acquireTitleBar(); + if (titleBar != null) { + titleBar.setOnTitleBarListener(this); + } + + // 初始化沉浸式状态栏 + if (isStatusBarEnabled()) { + getStatusBarConfig().init(); + } + + // 适配 Android 15 EdgeToEdge 特性 + if (AndroidVersion.isAndroid15()) { + getWindow().getDecorView().setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() { + + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets insets) { + Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars()); + View immersionTopView = getImmersionTopView(); + View immersionBottomView = getImmersionBottomView(); + if (immersionTopView != null && immersionTopView == immersionBottomView) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), systemBars.bottom); + return insets; + } + if (immersionTopView != null) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), immersionTopView.getPaddingBottom()); + } + if (immersionBottomView != null) { + immersionBottomView.setPadding(immersionBottomView.getPaddingLeft(), immersionBottomView.getPaddingTop(), + immersionBottomView.getPaddingRight(), systemBars.bottom); + } + return insets; + } + }); + } else { + View immersionTopView = getImmersionTopView(); + if (immersionTopView != null) { + ImmersionBar.setTitleBar(this, immersionTopView); + } + } + } + + /** + * 是否使用沉浸式状态栏 + */ + protected boolean isStatusBarEnabled() { + return true; + } + + /** + * 状态栏字体深色模式 + */ + protected boolean isStatusBarDarkFont() { + return true; + } + + /** + * 获取状态栏沉浸的配置对象 + */ + @NonNull + public ImmersionBar getStatusBarConfig() { + if (mImmersionBar == null) { + mImmersionBar = createStatusBarConfig(); + } + return mImmersionBar; + } + + /** + * 初始化沉浸式状态栏 + */ + @NonNull + protected ImmersionBar createStatusBarConfig() { + return ImmersionBar.with(this) + // 默认状态栏字体颜色为黑色 + .statusBarDarkFont(isStatusBarDarkFont()) + // 指定导航栏背景颜色 + .navigationBarColor(R.color.white) + // 状态栏字体和导航栏内容自动变色,必须指定状态栏颜色和导航栏颜色才可以自动变色 + .autoDarkModeEnable(true, 0.2f); + } + + /** + * 设置标题栏的标题 + */ + @Override + public void setTitle(@StringRes int id) { + setTitle(getString(id)); + } + + /** + * 设置标题栏的标题 + */ + @Override + public void setTitle(CharSequence title) { + super.setTitle(title); + TitleBar titleBar = acquireTitleBar(); + if (titleBar != null) { + titleBar.setTitle(title); + } + } + + @Nullable + @Override + public TitleBar acquireTitleBar() { + if (mTitleBar == null) { + mTitleBar = findTitleBar(getContentView()); + } + return mTitleBar; + } + + /** + * 获取需要沉浸的顶部 View 对象 + */ + @Nullable + @Override + public View getImmersionTopView() { + return acquireTitleBar(); + } + + @Override + public void onLeftClick(TitleBar titleBar) { + onBackPressed(); + } + + /** + * {@link OnHttpListener} + */ + + @Override + public void onHttpStart(@NonNull IRequestApi api) { + showLoadingDialog(); + } + + @Override + public void onHttpSuccess(@NonNull Object result) { + if (result instanceof HttpData) { + toast(((HttpData) result).getMessage()); + } + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + toast(throwable.getMessage()); + } + + @Override + public void onHttpEnd(@NonNull IRequestApi api) { + hideLoadingDialog(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (isShowDialog()) { + hideLoadingDialog(); + } + mDialog = null; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + // 友盟回调 + UmengClient.onActivityResult(this, requestCode, resultCode, data); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppAdapter.java b/app/src/main/java/com/hjq/demo/app/AppAdapter.java new file mode 100644 index 00000000..6d370f09 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppAdapter.java @@ -0,0 +1,240 @@ +package com.hjq.demo.app; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.View; +import androidx.annotation.IntRange; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import com.hjq.base.BaseAdapter; +import com.hjq.custom.widget.layout.WrapRecyclerView; +import java.util.ArrayList; +import java.util.List; + + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/12/19 + * desc : RecyclerView 适配器业务基类 + */ +public abstract class AppAdapter extends BaseAdapter.AppViewHolder> { + + /** 列表数据 */ + @NonNull + private List mDataSet = new ArrayList<>(); + + /** 当前列表的页码,默认为第一页,用于分页加载功能 */ + private int mPageNumber = 1; + + /** 是否是最后一页,默认为false,用于分页加载功能 */ + private boolean mLastPage; + + /** 标记对象 */ + private Object mTag; + + public AppAdapter(@NonNull Context context) { + super(context); + } + + @Override + public int getItemCount() { + return getCount(); + } + + /** + * 获取数据总数 + */ + public int getCount() { + return mDataSet.size(); + } + + /** + * 设置新的数据 + */ + @SuppressLint("NotifyDataSetChanged") + public void setData(@Nullable List data) { + if (data == null) { + mDataSet.clear(); + } else { + mDataSet = data; + } + notifyDataSetChanged(); + } + + /** + * 获取当前数据 + */ + @NonNull + public List getData() { + return mDataSet; + } + + /** + * 追加一些数据 + */ + public void addData(List data) { + if (data == null || data.isEmpty()) { + return; + } + + mDataSet.addAll(data); + notifyItemRangeInserted(mDataSet.size() - data.size(), data.size()); + } + + /** + * 清空当前数据 + */ + @SuppressLint("NotifyDataSetChanged") + public void clearData() { + mDataSet.clear(); + notifyDataSetChanged(); + } + + /** + * 是否包含了某个位置上的条目数据 + */ + public boolean containsItem(@IntRange(from = 0) int position) { + return containsItem(getItem(position)); + } + + /** + * 是否包含某个条目数据 + */ + public boolean containsItem(T item) { + if (item == null) { + return false; + } + return mDataSet.contains(item); + } + + /** + * 获取某个位置上的数据 + */ + public T getItem(@IntRange(from = 0) int position) { + return mDataSet.get(position); + } + + /** + * 更新某个位置上的数据 + */ + public void setItem(@IntRange(from = 0) int position, @NonNull T item) { + mDataSet.set(position, item); + notifyItemChanged(position); + } + + /** + * 添加单条数据 + */ + public void addItem(@NonNull T item) { + addItem(mDataSet.size(), item); + } + + public void addItem(@IntRange(from = 0) int position, @NonNull T item) { + if (position < mDataSet.size()) { + mDataSet.add(position, item); + } else { + mDataSet.add(item); + position = mDataSet.size() - 1; + } + notifyItemInserted(position); + } + + /** + * 删除单条数据 + */ + public void removeItem(@NonNull T item) { + int index = mDataSet.indexOf(item); + if (index != -1) { + removeItem(index); + } + } + + public void removeItem(@IntRange(from = 0) int position) { + mDataSet.remove(position); + notifyItemRemoved(position); + } + + /** + * 获取当前的页码 + */ + public int getPageNumber() { + return mPageNumber; + } + + /** + * 设置当前的页码 + */ + public void setPageNumber(@IntRange(from = 0) int number) { + mPageNumber = number; + } + + /** + * 当前是否为最后一页 + */ + public boolean isLastPage() { + return mLastPage; + } + + /** + * 设置是否为最后一页 + */ + public void setLastPage(boolean lastPage) { + mLastPage = lastPage; + } + + /** + * 获取标记 + */ + @Nullable + public Object getTag() { + return mTag; + } + + /** + * 设置标记 + */ + public void setTag(@NonNull Object tag) { + mTag = tag; + } + + public abstract class AppViewHolder extends BaseAdapter.BaseViewHolder { + + public AppViewHolder(@LayoutRes int id) { + super(id); + } + + public AppViewHolder(View itemView) { + super(itemView); + } + + @Override + protected int getViewHolderPosition() { + int position = super.getViewHolderPosition(); + RecyclerView recyclerView = getRecyclerView(); + if (recyclerView instanceof WrapRecyclerView) { + // 这里要减去头部的数量 + position -= ((WrapRecyclerView) recyclerView).getHeaderViewsCount(); + } + return position; + } + } + + public final class SimpleViewHolder extends AppViewHolder { + + public SimpleViewHolder(@LayoutRes int id) { + super(id); + } + + public SimpleViewHolder(View itemView) { + super(itemView); + } + + @Override + public void onBindView(int position) { + // default implementation ignored + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppApplication.java b/app/src/main/java/com/hjq/demo/app/AppApplication.java new file mode 100644 index 00000000..929d62cd --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppApplication.java @@ -0,0 +1,46 @@ +package com.hjq.demo.app; + +import android.app.Application; +import com.hjq.core.manager.ActivityManager; +import com.hjq.demo.aop.Log; +import com.hjq.demo.http.glide.GlideApp; +import com.hjq.demo.manager.InitManager; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 应用入口 + */ +public final class AppApplication extends Application { + + @Log("启动耗时") + @Override + public void onCreate() { + super.onCreate(); + + // 如果当前的进程不是主进程的话,则不进行第三方框架的初始化 + if (!ActivityManager.isMainProcess(this)) { + return; + } + + InitManager.preInitSdk(this); + if (InitManager.isAgreePrivacy(this)) { + InitManager.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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppFragment.java b/app/src/main/java/com/hjq/demo/app/AppFragment.java new file mode 100644 index 00000000..2157d89e --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/AppFragment.java @@ -0,0 +1,78 @@ +package com.hjq.demo.app; + +import androidx.annotation.NonNull; +import com.hjq.base.BaseFragment; +import com.hjq.demo.action.ToastAction; +import com.hjq.demo.http.model.HttpData; +import com.hjq.http.config.IRequestApi; +import com.hjq.http.listener.OnHttpListener; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : Fragment 业务基类 + */ +public abstract class AppFragment extends BaseFragment + implements ToastAction, OnHttpListener { + + /** + * 当前加载对话框是否在显示中 + */ + public boolean isShowDialog() { + A activity = getAttachActivity(); + if (activity == null) { + return false; + } + return activity.isShowDialog(); + } + + /** + * 显示加载对话框 + */ + public void showLoadingDialog() { + A activity = getAttachActivity(); + if (activity == null) { + return; + } + activity.showLoadingDialog(); + } + + /** + * 隐藏加载对话框 + */ + public void hideLoadingDialog() { + A activity = getAttachActivity(); + if (activity == null) { + return; + } + activity.hideLoadingDialog(); + } + + /** + * {@link OnHttpListener} + */ + + @Override + public void onHttpStart(@NonNull IRequestApi api) { + showLoadingDialog(); + } + + @Override + public void onHttpSuccess(@NonNull Object result) { + if (!(result instanceof HttpData)) { + return; + } + toast(((HttpData) result).getMessage()); + } + + @Override + public void onHttpFail(@NonNull Throwable throwable) { + toast(throwable.getMessage()); + } + + @Override + public void onHttpEnd(@NonNull IRequestApi api) { + hideLoadingDialog(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/TitleBarFragment.java b/app/src/main/java/com/hjq/demo/app/TitleBarFragment.java new file mode 100644 index 00000000..7f6212ff --- /dev/null +++ b/app/src/main/java/com/hjq/demo/app/TitleBarFragment.java @@ -0,0 +1,147 @@ +package com.hjq.demo.app; + +import android.graphics.Insets; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnApplyWindowInsetsListener; +import android.view.WindowInsets; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.gyf.immersionbar.ImmersionBar; +import com.hjq.bar.TitleBar; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.demo.action.ImmersionAction; +import com.hjq.demo.action.TitleBarAction; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/10/31 + * desc : 带标题栏的 Fragment 业务基类 + */ +public abstract class TitleBarFragment + extends AppFragment implements TitleBarAction, ImmersionAction { + + /** 标题栏对象 */ + private TitleBar mTitleBar; + /** 状态栏沉浸 */ + private ImmersionBar mImmersionBar; + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // 设置标题栏点击监听 + TitleBar titleBar = acquireTitleBar(); + if (titleBar != null) { + titleBar.setOnTitleBarListener(this); + } + + if (isStatusBarEnabled()) { + // 初始化沉浸式状态栏 + getStatusBarConfig().init(); + } + + // 适配 Android 15 EdgeToEdge 特性 + if (AndroidVersion.isAndroid15()) { + view.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() { + + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets insets) { + Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars()); + View immersionTopView = getImmersionTopView(); + View immersionBottomView = getImmersionBottomView(); + if (immersionTopView != null && immersionTopView == immersionBottomView) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), systemBars.bottom); + return insets; + } + if (immersionTopView != null) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), immersionTopView.getPaddingBottom()); + } + if (immersionBottomView != null) { + immersionBottomView.setPadding(immersionBottomView.getPaddingLeft(), immersionBottomView.getPaddingTop(), + immersionBottomView.getPaddingRight(), systemBars.bottom); + } + return insets; + } + }); + } else { + View immersionTopView = getImmersionTopView(); + if (immersionTopView != null) { + ImmersionBar.setTitleBar(this, immersionTopView); + } + } + } + + @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(R.color.white) + // 状态栏字体和导航栏内容自动变色,必须指定状态栏颜色和导航栏颜色才可以自动变色 + .autoDarkModeEnable(true, 0.2f); + } + + /** + * 获取状态栏字体颜色 + */ + protected boolean isStatusBarDarkFont() { + A activity = getAttachActivity(); + if (activity == null) { + return false; + } + // 返回真表示黑色字体 + return activity.isStatusBarDarkFont(); + } + + @Override + @Nullable + public TitleBar acquireTitleBar() { + if (mTitleBar == null || !isLoading()) { + mTitleBar = findTitleBar(getView()); + } + return mTitleBar; + } + + @Nullable + @Override + public View getImmersionTopView() { + return acquireTitleBar(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/common/MyActivity.java b/app/src/main/java/com/hjq/demo/common/MyActivity.java deleted file mode 100644 index 7a07c316..00000000 --- a/app/src/main/java/com/hjq/demo/common/MyActivity.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.hjq.demo.common; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.DrawableRes; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.gyf.immersionbar.ImmersionBar; -import com.hjq.bar.OnTitleBarListener; -import com.hjq.bar.TitleBar; -import com.hjq.base.BaseActivity; -import com.hjq.base.BuildConfig; -import com.hjq.demo.R; -import com.hjq.demo.helper.ActivityStackManager; -import com.hjq.demo.other.EventBusManager; -import com.hjq.demo.other.StatusManager; -import com.hjq.toast.ToastUtils; -import com.hjq.umeng.UmengClient; - -import butterknife.ButterKnife; -import butterknife.Unbinder; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/10/18 - * desc : 项目中的 Activity 基类 - */ -public abstract class MyActivity extends BaseActivity - implements OnTitleBarListener { - - /** 标题栏对象 */ - private TitleBar mTitleBar; - /** 状态栏沉浸 */ - private ImmersionBar mImmersionBar; - /** ButterKnife 注解 */ - private Unbinder mButterKnife; - - /** - * 获取标题栏 id - */ - protected int getTitleId() { - return 0; - } - - @Override - protected void initActivity() { - super.initActivity(); - ActivityStackManager.getInstance().onCreated(this); - } - - @Override - protected void initLayout() { - super.initLayout(); - - // 初始化标题栏的监听 - if (getTitleId() > 0) { - // 勤快模式 - View view = findViewById(getTitleId()); - if (view instanceof TitleBar) { - mTitleBar = (TitleBar) view; - } - } else if (getTitleId() == 0) { - // 懒人模式 - mTitleBar = findTitleBar(getContentView()); - } - if (mTitleBar != null) { - mTitleBar.setOnTitleBarListener(this); - } - - mButterKnife = ButterKnife.bind(this); - EventBusManager.register(this); - initImmersion(); - } - - /** - * 递归获取 ViewGroup 中的 TitleBar 对象 - */ - static TitleBar findTitleBar(ViewGroup group) { - for (int i = 0; i < group.getChildCount(); i++) { - View view = group.getChildAt(i); - if ((view instanceof TitleBar)) { - return (TitleBar) view; - } else if (view instanceof ViewGroup) { - TitleBar titleBar = findTitleBar((ViewGroup) view); - if (titleBar != null) { - return titleBar; - } - } - } - return null; - } - - /** - * 初始化沉浸式 - */ - protected void initImmersion() { - // 初始化沉浸式状态栏 - if (isStatusBarEnabled()) { - statusBarConfig().init(); - - // 设置标题栏沉浸 - if (getTitleId() > 0) { - ImmersionBar.setTitleBar(this, findViewById(getTitleId())); - } else if (mTitleBar != null) { - ImmersionBar.setTitleBar(this, mTitleBar); - } - } - } - - /** - * 是否使用沉浸式状态栏 - */ - public boolean isStatusBarEnabled() { - return true; - } - - /** - * 获取状态栏沉浸的配置对象 - */ - public ImmersionBar getStatusBarConfig() { - return mImmersionBar; - } - - /** - * 获取状态栏字体颜色 - */ - public boolean statusBarDarkFont() { - // 返回真表示黑色字体 - return true; - } - - /** - * 初始化沉浸式状态栏 - */ - protected ImmersionBar statusBarConfig() { - // 在BaseActivity里初始化 - mImmersionBar = ImmersionBar.with(this) - // 默认状态栏字体颜色为黑色 - .statusBarDarkFont(statusBarDarkFont()); - return mImmersionBar; - } - - /** - * 设置标题栏的标题 - */ - @Override - public void setTitle(@StringRes int id) { - setTitle(getString(id)); - } - - /** - * 设置标题栏的标题 - */ - @Override - public void setTitle(CharSequence title) { - super.setTitle(title); - if (mTitleBar != null) { - mTitleBar.setTitle(title); - } - } - - /** - * 设置标题栏的左标题 - */ - public void setLeftTitle(int id) { - if (mTitleBar != null) { - mTitleBar.setLeftTitle(id); - } - } - - public void setLeftTitle(CharSequence text) { - if (mTitleBar != null) { - mTitleBar.setLeftTitle(text); - } - } - - public CharSequence getLeftTitle() { - if (mTitleBar != null) { - return mTitleBar.getLeftTitle(); - } - return ""; - } - - /** - * 设置标题栏的右标题 - */ - public void setRightTitle(int id) { - if (mTitleBar != null) { - mTitleBar.setRightTitle(id); - } - } - - public void setRightTitle(CharSequence text) { - if (mTitleBar != null) { - mTitleBar.setRightTitle(text); - } - } - - public CharSequence getRightTitle() { - if (mTitleBar != null) { - return mTitleBar.getRightTitle(); - } - return ""; - } - - /** - * 设置标题栏的左图标 - */ - public void setLeftIcon(int id) { - if (mTitleBar != null) { - mTitleBar.setLeftIcon(id); - } - } - - public void setLeftIcon(Drawable drawable) { - if (mTitleBar != null) { - mTitleBar.setLeftIcon(drawable); - } - } - - @Nullable - public Drawable getLeftIcon() { - if (mTitleBar != null) { - return mTitleBar.getLeftIcon(); - } - return null; - } - - /** - * 设置标题栏的右图标 - */ - public void setRightIcon(int id) { - if (mTitleBar != null) { - mTitleBar.setRightIcon(id); - } - } - - public void setRightIcon(Drawable drawable) { - if (mTitleBar != null) { - mTitleBar.setRightIcon(drawable); - } - } - - @Nullable - public Drawable getRightIcon() { - if (mTitleBar != null) { - return mTitleBar.getRightIcon(); - } - return null; - } - - @Nullable - public TitleBar getTitleBar() { - return mTitleBar; - } - - /** - * {@link OnTitleBarListener} - */ - - /** - * TitleBar 左边的View被点击了 - */ - @Override - public void onLeftClick(View v) { - onBackPressed(); - } - - /** - * TitleBar 中间的View被点击了 - */ - @Override - public void onTitleClick(View v) {} - - /** - * TitleBar 右边的View被点击了 - */ - @Override - public void onRightClick(View v) {} - - @Override - protected void onResume() { - super.onResume(); - UmengClient.onResume(this); - } - - @Override - protected void onPause() { - UmengClient.onPause(this); - super.onPause(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (mButterKnife != null) { - mButterKnife.unbind(); - } - EventBusManager.unregister(this); - ActivityStackManager.getInstance().onDestroyed(this); - } - - @Override - public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { - super.startActivityForResult(intent, requestCode, options); - overridePendingTransition(R.anim.activity_right_in, R.anim.activity_right_out); - } - - @Override - public void finish() { - super.finish(); - overridePendingTransition(R.anim.activity_left_in, R.anim.activity_left_out); - } - - /** - * 显示吐司 - */ - public void toast(CharSequence text) { - ToastUtils.show(text); - } - - public void toast(@StringRes int id) { - ToastUtils.show(id); - } - - public void toast(Object object) { - ToastUtils.show(object); - } - - /** - * 打印日志 - */ - public void log(Object object) { - if (BuildConfig.DEBUG) { - Log.v(getClass().getSimpleName(), object != null ? object.toString() : "null"); - } - } - - private final StatusManager mStatusManager = new StatusManager(); - - /** - * 显示加载中 - */ - public void showLoading() { - mStatusManager.showLoading(this); - } - - public void showLoading(@StringRes int id) { - mStatusManager.showLoading(this, getString(id)); - } - - public void showLoading(CharSequence text) { - mStatusManager.showLoading(this, text); - } - - /** - * 显示加载完成 - */ - public void showComplete() { - mStatusManager.showComplete(); - } - - /** - * 显示空提示 - */ - public void showEmpty() { - mStatusManager.showEmpty(getContentView()); - } - - /** - * 显示错误提示 - */ - public void showError() { - mStatusManager.showError(getContentView()); - } - - /** - * 显示自定义提示 - */ - public void showLayout(@DrawableRes int drawableId, @StringRes int stringId) { - mStatusManager.showLayout(getContentView(), drawableId, stringId); - } - - public void showLayout(Drawable drawable, CharSequence hint) { - mStatusManager.showLayout(getContentView(), drawable, hint); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/common/MyApplication.java b/app/src/main/java/com/hjq/demo/common/MyApplication.java deleted file mode 100644 index b039eb56..00000000 --- a/app/src/main/java/com/hjq/demo/common/MyApplication.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.hjq.demo.common; - -import android.app.Application; -import android.content.Context; -import android.util.Log; -import android.widget.Toast; - -import com.hjq.demo.BuildConfig; -import com.hjq.demo.other.EventBusManager; -import com.hjq.demo.ui.activity.CrashActivity; -import com.hjq.demo.ui.activity.HomeActivity; -import com.hjq.image.ImageLoader; -import com.hjq.toast.ToastInterceptor; -import com.hjq.toast.ToastUtils; -import com.hjq.umeng.UmengClient; -import com.squareup.leakcanary.LeakCanary; -import com.tencent.bugly.crashreport.CrashReport; - -import cat.ereza.customactivityoncrash.config.CaocConfig; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/10/18 - * desc : 项目中的 Application 基类 - */ -public final class MyApplication extends Application { - - @Override - public void onCreate() { - super.onCreate(); - initSDK(this); - } - - /** - * 初始化一些第三方框架 - */ - public static void initSDK(Application application) { - // 这个过程专门用于堆分析的 leak 金丝雀 - // 你不应该在这个过程中初始化你的应用程序 - if (LeakCanary.isInAnalyzerProcess(application)) { - return; - } - - // 内存泄漏检测 - LeakCanary.install(application); - - // 友盟统计、登录、分享 SDK - UmengClient.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; - } - }); - // 吐司工具类 - ToastUtils.init(application); - - // 图片加载器 - ImageLoader.init(application); - - // EventBus 事件总线 - EventBusManager.init(); - - // Bugly 异常捕捉 - CrashReport.initCrashReport(application, BuildConfig.BUGLY_ID, false); - - // Crash 捕捉界面 - CaocConfig.Builder.create() - .backgroundMode(CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM) - .enabled(true) - .trackActivities(true) - .minTimeBetweenCrashesMs(2000) - // 重启的 Activity - .restartActivity(HomeActivity.class) - // 错误的 Activity - .errorActivity(CrashActivity.class) - // 设置监听器 - //.eventListener(new YourCustomEventListener()) - .apply(); - } - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - // 使用 Dex分包 - //MultiDex.install(this); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/common/MyDialogFragment.java b/app/src/main/java/com/hjq/demo/common/MyDialogFragment.java deleted file mode 100644 index f2298970..00000000 --- a/app/src/main/java/com/hjq/demo/common/MyDialogFragment.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.hjq.demo.common; - -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.FragmentActivity; - -import com.hjq.base.BaseDialogFragment; -import com.hjq.toast.ToastUtils; - -import butterknife.ButterKnife; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/05/18 - * desc : 项目中的 Dialog 基类 - */ -public final class MyDialogFragment { - - public static class Builder - extends BaseDialogFragment.Builder { - - public Builder(FragmentActivity activity) { - super(activity); - } - - @Override - public B setContentView(@NonNull View view) { - // 使用 ButterKnife 注解 - ButterKnife.bind(this, view); - return super.setContentView(view); - } - - /** - * 显示吐司 - */ - public void toast(CharSequence text) { - ToastUtils.show(text); - } - - public void toast(@StringRes int id) { - ToastUtils.show(id); - } - - public void toast(Object object) { - ToastUtils.show(object); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/common/MyLazyFragment.java b/app/src/main/java/com/hjq/demo/common/MyLazyFragment.java deleted file mode 100644 index ca2b3a16..00000000 --- a/app/src/main/java/com/hjq/demo/common/MyLazyFragment.java +++ /dev/null @@ -1,285 +0,0 @@ -package com.hjq.demo.common; - -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.gyf.immersionbar.ImmersionBar; -import com.hjq.bar.OnTitleBarListener; -import com.hjq.bar.TitleBar; -import com.hjq.base.BaseLazyFragment; -import com.hjq.base.BuildConfig; -import com.hjq.demo.other.EventBusManager; -import com.hjq.demo.other.StatusManager; -import com.hjq.toast.ToastUtils; -import com.hjq.umeng.UmengClient; - -import butterknife.ButterKnife; -import butterknife.Unbinder; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/10/18 - * desc : 项目中 Fragment 懒加载基类 - */ -public abstract class MyLazyFragment - extends BaseLazyFragment implements OnTitleBarListener { - - /** 标题栏对象 */ - private TitleBar mTitleBar; - /** 状态栏沉浸 */ - private ImmersionBar mImmersionBar; - /** ButterKnife 注解 */ - private Unbinder mButterKnife; - - /** - * 获取标题栏 id - */ - protected int getTitleId() { - return 0; - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - if (view != null) { - mButterKnife = ButterKnife.bind(this, view); - } - return view; - } - - @Override - protected void initFragment() { - if (getTitleId() > 0) { - // 勤快模式 - View view = findViewById(getTitleId()); - if (view instanceof TitleBar) { - mTitleBar = (TitleBar) view; - } - } else if (getTitleId() == 0 && getView() instanceof ViewGroup) { - // 懒人模式 - mTitleBar = MyActivity.findTitleBar((ViewGroup) getView()); - } - if (mTitleBar != null) { - mTitleBar.setOnTitleBarListener(this); - } - - initImmersion(); - super.initFragment(); - EventBusManager.register(this); - } - - /** - * 初始化沉浸式 - */ - protected void initImmersion() { - - // 初始化沉浸式状态栏 - if (isStatusBarEnabled()) { - statusBarConfig().init(); - - // 设置标题栏沉浸 - if (getTitleId() > 0) { - ImmersionBar.setTitleBar(this, findViewById(getTitleId())); - } else if (mTitleBar != null) { - ImmersionBar.setTitleBar(this, mTitleBar); - } - } - } - - /** - * 是否在Fragment使用沉浸式 - */ - public boolean isStatusBarEnabled() { - return false; - } - - /** - * 获取状态栏沉浸的配置对象 - */ - protected ImmersionBar getStatusBarConfig() { - return mImmersionBar; - } - - /** - * 初始化沉浸式 - */ - private ImmersionBar statusBarConfig() { - //在BaseActivity里初始化 - mImmersionBar = ImmersionBar.with(this) - // 默认状态栏字体颜色为黑色 - .statusBarDarkFont(statusBarDarkFont()) - // 解决软键盘与底部输入框冲突问题,默认为false,还有一个重载方法,可以指定软键盘mode - .keyboardEnable(true); - return mImmersionBar; - } - - /** - * 获取状态栏字体颜色 - */ - protected boolean statusBarDarkFont() { - // 返回真表示黑色字体 - return true; - } - - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (isVisibleToUser && isStatusBarEnabled() && isLazyLoad()) { - // 重新初始化状态栏 - statusBarConfig().init(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mButterKnife != null) { - mButterKnife.unbind(); - } - EventBusManager.unregister(this); - } - - /** - * 设置标题栏的标题 - */ - public void setTitle(@StringRes int id) { - setTitle(getString(id)); - } - - /** - * 设置标题栏的标题 - */ - public void setTitle(CharSequence title) { - if (mTitleBar != null) { - mTitleBar.setTitle(title); - } else { - // 如果没有标题栏对象就直接设置给绑定的 Activity - getAttachActivity().setTitle(title); - } - } - - @Nullable - public TitleBar getTitleBar() { - if (getTitleId() > 0 && findViewById(getTitleId()) instanceof TitleBar) { - return findViewById(getTitleId()); - } - return null; - } - - /** - * 显示吐司 - */ - public void toast(CharSequence text) { - ToastUtils.show(text); - } - - public void toast(@StringRes int id) { - ToastUtils.show(id); - } - - public void toast(Object object) { - ToastUtils.show(object); - } - - /** - * 打印日志 - */ - public void log(Object object) { - if (BuildConfig.DEBUG) { - Log.v(getClass().getSimpleName(), object != null ? object.toString() : "null"); - } - } - - @Override - public void onResume() { - super.onResume(); - UmengClient.onResume(this); - } - - @Override - public void onPause() { - UmengClient.onPause(this); - super.onPause(); - } - - /** - * {@link OnTitleBarListener} - */ - - /** - * TitleBar 左边的View被点击了 - */ - @Override - public void onLeftClick(View v) {} - - /** - * TitleBar 中间的View被点击了 - */ - @Override - public void onTitleClick(View v) {} - - /** - * TitleBar 右边的View被点击了 - */ - @Override - public void onRightClick(View v) {} - - private final StatusManager mStatusManager = new StatusManager(); - - /** - * 显示加载中 - */ - public void showLoading() { - mStatusManager.showLoading(getAttachActivity()); - } - - public void showLoading(@StringRes int id) { - mStatusManager.showLoading(getAttachActivity(), getString(id)); - } - - public void showLoading(CharSequence text) { - mStatusManager.showLoading(getAttachActivity(), text); - } - - /** - * 显示加载完成 - */ - public void showComplete() { - mStatusManager.showComplete(); - } - - /** - * 显示空提示 - */ - public void showEmpty() { - mStatusManager.showEmpty(getView()); - } - - /** - * 显示错误提示 - */ - public void showError() { - mStatusManager.showError(getView()); - } - - /** - * 显示自定义提示 - */ - public void showLayout(@DrawableRes int drawableId, @StringRes int stringId) { - mStatusManager.showLayout(getView(), drawableId, stringId); - } - - public void showLayout(Drawable drawable, CharSequence hint) { - mStatusManager.showLayout(getView(), drawable, hint); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/common/MyListViewAdapter.java b/app/src/main/java/com/hjq/demo/common/MyListViewAdapter.java deleted file mode 100644 index fc882b2c..00000000 --- a/app/src/main/java/com/hjq/demo/common/MyListViewAdapter.java +++ /dev/null @@ -1,232 +0,0 @@ -package com.hjq.demo.common; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.LayoutRes; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.hjq.base.BaseListViewAdapter; -import com.hjq.toast.ToastUtils; - -import java.util.ArrayList; -import java.util.List; - -import butterknife.ButterKnife; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/10/18 - * desc : 项目中 ListView 适配器基类 - */ -public abstract class MyListViewAdapter extends BaseListViewAdapter { - - /** 列表数据 */ - private List mDataSet; - /** 当前列表的页码,默认为第一页,用于分页加载功能 */ - private int mPageNumber = 1; - /** 是否是最后一页,默认为false,用于分页加载功能 */ - private boolean mLastPage; - /** 标记对象 */ - private Object mTag; - - public MyListViewAdapter(Context context) { - super(context); - } - - @Override - public int getItemCount() { - return mDataSet == null ? 0 : mDataSet.size(); - } - - /** - * 设置新的数据 - */ - public void setData(List data) { - mDataSet = data; - notifyDataSetChanged(); - } - - /** - * 获取当前数据 - */ - @Nullable - public List getData() { - return mDataSet; - } - - /** - * 追加一些数据 - */ - public void addData(List data) { - if (mDataSet != null) { - mDataSet.addAll(data); - } else { - mDataSet = data; - } - notifyDataSetChanged(); - } - - /** - * 清空当前数据 - */ - public void clearData() { - //当前的数据不能为空 - if (mDataSet == null || mDataSet.size() == 0) { - return; - } - - mDataSet.clear(); - notifyDataSetChanged(); - } - - /** - * 获取某个位置上的数据 - */ - @Override - public T getItem(int position) { - return mDataSet.get(position); - } - - /** - * 更新某个位置上的数据 - */ - public void setItem(int position, T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } - mDataSet.set(position, item); - notifyDataSetChanged(); - } - - /** - * 添加单条数据 - */ - public void addItem(T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } - - addItem(mDataSet.size(), item); - } - - /** - * 添加单条数据 - */ - public void addItem(int position, T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } - - //如果是在for循环添加后要记得position++ - if (position < mDataSet.size()) { - mDataSet.add(position, item); - } else { - mDataSet.add(item); - } - notifyDataSetChanged(); - } - - /** - * 删除单条数据 - */ - public void removeItem(T item) { - int index = mDataSet.indexOf(item); - if (index != -1) { - removeItem(index); - } - } - - public void removeItem(int position) { - //如果是在for循环删除后要记得i-- - mDataSet.remove(position); - notifyDataSetChanged(); - } - - /** - * 获取当前的页码 - */ - public int getPageNumber() { - return mPageNumber; - } - - /** - * 设置当前的页码 - */ - public void setPageNumber(int pageNumber) { - mPageNumber = pageNumber; - } - - /** - * 当前是否为最后一页 - */ - public boolean isLastPage() { - return mLastPage; - } - - /** - * 设置是否为最后一页 - */ - public void setLastPage(boolean lastPage) { - mLastPage = lastPage; - } - - /** - * 获取标记 - */ - public Object getTag() { - return mTag; - } - - /** - * 设置标记 - */ - public void setTag(Object tag) { - mTag = tag; - } - - /** - * 显示吐司 - */ - public void toast(CharSequence text) { - ToastUtils.show(text); - } - - public void toast(@StringRes int id) { - ToastUtils.show(id); - } - - public void toast(Object object) { - ToastUtils.show(object); - } - - public abstract class ViewHolder extends BaseListViewAdapter.ViewHolder { - - public ViewHolder(ViewGroup parent, @LayoutRes int id) { - super(parent, id); - ButterKnife.bind(getItemView()); - } - - public ViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(itemView); - } - } - - public class SimpleHolder extends ViewHolder { - - public SimpleHolder(ViewGroup parent, @LayoutRes int id) { - super(parent, id); - } - - public SimpleHolder(View itemView) { - super(itemView); - } - - @Override - public void onBindView(int position) {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/common/MyRecyclerViewAdapter.java b/app/src/main/java/com/hjq/demo/common/MyRecyclerViewAdapter.java deleted file mode 100644 index 8f1868b5..00000000 --- a/app/src/main/java/com/hjq/demo/common/MyRecyclerViewAdapter.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.hjq.demo.common; - -import android.content.Context; -import android.view.View; - -import androidx.annotation.LayoutRes; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.hjq.base.BaseRecyclerViewAdapter; -import com.hjq.toast.ToastUtils; - -import java.util.ArrayList; -import java.util.List; - -import butterknife.ButterKnife; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/12/19 - * desc : 项目中 RecyclerView 适配器基类 - */ -public abstract class MyRecyclerViewAdapter extends BaseRecyclerViewAdapter { - - /** 列表数据 */ - private List mDataSet; - /** 当前列表的页码,默认为第一页,用于分页加载功能 */ - private int mPageNumber = 1; - /** 是否是最后一页,默认为false,用于分页加载功能 */ - private boolean mLastPage; - /** 标记对象 */ - private Object mTag; - - public MyRecyclerViewAdapter(Context context) { - super(context); - } - - @Override - public int getItemCount() { - return mDataSet == null ? 0 : mDataSet.size(); - } - - /** - * 设置新的数据 - */ - public void setData(List data) { - mDataSet = data; - notifyDataSetChanged(); - } - - /** - * 获取当前数据 - */ - @Nullable - public List getData() { - return mDataSet; - } - - /** - * 追加一些数据 - */ - public void addData(List data) { - if (data == null || data.size() == 0) { - return; - } - - if (mDataSet == null || mDataSet.size() == 0) { - setData(data); - } else { - mDataSet.addAll(data); - notifyItemRangeInserted(mDataSet.size() - data.size(), data.size()); - } - } - - /** - * 清空当前数据 - */ - public void clearData() { - if (mDataSet == null || mDataSet.size() == 0) { - return; - } - - mDataSet.clear(); - notifyDataSetChanged(); - } - - /** - * 获取某个位置上的数据 - */ - public T getItem(int position) { - return mDataSet.get(position); - } - - /** - * 更新某个位置上的数据 - */ - public void setItem(int position, T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } - mDataSet.set(position, item); - notifyItemChanged(position); - } - - /** - * 添加单条数据 - */ - public void addItem(T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } - - addItem(mDataSet.size(), item); - } - - public void addItem(int position, T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } - - if (position < mDataSet.size()) { - mDataSet.add(position, item); - } else { - mDataSet.add(item); - position = mDataSet.size() - 1; - } - notifyItemInserted(position); - } - - /** - * 删除单条数据 - */ - public void removeItem(T item) { - int index = mDataSet.indexOf(item); - if (index != -1) { - removeItem(index); - } - } - - public void removeItem(int position) { - //如果是在for循环删除后要记得i-- - mDataSet.remove(position); - //告诉适配器删除数据的位置,会有动画效果 - notifyItemRemoved(position); - } - - /** - * 获取当前的页码 - */ - public int getPageNumber() { - return mPageNumber; - } - - /** - * 设置当前的页码 - */ - public void setPageNumber(int pageNumber) { - mPageNumber = pageNumber; - } - - /** - * 当前是否为最后一页 - */ - public boolean isLastPage() { - return mLastPage; - } - - /** - * 设置是否为最后一页 - */ - public void setLastPage(boolean lastPage) { - mLastPage = lastPage; - } - - /** - * 获取标记 - */ - public Object getTag() { - return mTag; - } - - /** - * 设置标记 - */ - public void setTag(Object tag) { - mTag = tag; - } - - /** - * 显示吐司 - */ - public void toast(CharSequence text) { - ToastUtils.show(text); - } - - public void toast(@StringRes int id) { - ToastUtils.show(id); - } - - public void toast(Object object) { - ToastUtils.show(object); - } - - public abstract class ViewHolder extends BaseRecyclerViewAdapter.ViewHolder { - - public ViewHolder(@LayoutRes int id) { - super(id); - ButterKnife.bind(this, itemView); - } - - public ViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - } - } - - public class SimpleHolder extends ViewHolder { - - public SimpleHolder(@LayoutRes int id) { - super(id); - } - - public SimpleHolder(View itemView) { - super(itemView); - } - - @Override - public void onBindView(int position) {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/helper/ActivityStackManager.java b/app/src/main/java/com/hjq/demo/helper/ActivityStackManager.java deleted file mode 100644 index 8380f185..00000000 --- a/app/src/main/java/com/hjq/demo/helper/ActivityStackManager.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.hjq.demo.helper; - -import android.app.Activity; -import android.app.Application; - -import androidx.collection.ArrayMap; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/18 - * desc : Activity 栈管理 - */ -public final class ActivityStackManager { - - private static volatile ActivityStackManager sInstance; - - private final ArrayMap mActivitySet = new ArrayMap<>(); - - /** 当前 Activity 对象标记 */ - private String mCurrentTag; - - private ActivityStackManager() {} - - public static ActivityStackManager getInstance() { - // 加入双重校验锁 - if(sInstance == null) { - synchronized (ActivityStackManager.class) { - if(sInstance == null){ - sInstance = new ActivityStackManager(); - } - } - } - return sInstance; - } - - /** - * 获取 Application 对象 - */ - public Application getApplication() { - return getTopActivity().getApplication(); - } - - /** - * 获取栈顶的 Activity - */ - public Activity getTopActivity() { - return mActivitySet.get(mCurrentTag); - } - - /** - * 销毁所有的 Activity - */ - public void finishAllActivities() { - finishAllActivities((Class) null); - } - - /** - * 销毁所有的 Activity,除这些 Class 之外的 Activity - */ - @SafeVarargs - public final void finishAllActivities(Class... classArray) { - String[] keys = mActivitySet.keySet().toArray(new String[]{}); - for (String key : keys) { - Activity activity = mActivitySet.get(key); - if (activity != null && !activity.isFinishing()) { - boolean whiteClazz = false; - if (classArray != null) { - for (Class clazz : classArray) { - if (activity.getClass() == clazz) { - whiteClazz = true; - } - } - } - // 如果不是白名单上面的 Activity 就销毁掉 - if (!whiteClazz) { - activity.finish(); - mActivitySet.remove(key); - } - } - } - } - - /** - * Activity 同名方法回调 - */ - public void onCreated(Activity activity) { - mCurrentTag = getObjectTag(activity); - mActivitySet.put(getObjectTag(activity), activity); - } - - /** - * Activity 同名方法回调 - */ - public void onDestroyed(Activity activity) { - mActivitySet.remove(getObjectTag(activity)); - // 如果当前的 Activity 是最后一个的话 - if (getObjectTag(activity).equals(mCurrentTag)) { - // 清除当前标记 - mCurrentTag = null; - } - } - - /** - * 获取一个对象的独立无二的标记 - */ - private static String getObjectTag(Object object) { - // 对象所在的包名 + 对象的内存地址 - return object.getClass().getName() + Integer.toHexString(object.hashCode()); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/helper/CacheDataManager.java b/app/src/main/java/com/hjq/demo/helper/CacheDataManager.java deleted file mode 100644 index 68fee3e9..00000000 --- a/app/src/main/java/com/hjq/demo/helper/CacheDataManager.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.hjq.demo.helper; - -import android.content.Context; -import android.os.Environment; - -import java.io.File; -import java.math.BigDecimal; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/03/01 - * desc : 应用缓存管理 - */ -public final class CacheDataManager { - - /** - * 获取缓存大小 - */ - public static String getTotalCacheSize(Context context) { - long cacheSize = getFolderSize(context.getCacheDir()); - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - cacheSize += getFolderSize(context.getExternalCacheDir()); - } - return getFormatSize(cacheSize); - } - - /** - * 清除缓存 - */ - public static void clearAllCache(Context context) { - deleteDir(context.getCacheDir()); - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - deleteDir(context.getExternalCacheDir()); - } - } - - /** - * 删除文件夹 - */ - private static boolean deleteDir(File dir) { - if (dir != null) { - if (dir.isDirectory()) { - String[] children = dir.list(); - for (String child : children) { - boolean success = deleteDir(new File(dir, child)); - if (!success) { - return false; - } - } - } else { - return dir.delete(); - } - } - return false; - } - - // 获取文件大小 - // Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/ 目录,一般放一些长时间保存的数据 - // Context.getExternalCacheDir() --> SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据 - private static long getFolderSize(File file) { - long size = 0; - try { - File[] list = file.listFiles(); - for (File temp : list) { - // 如果下面还有文件 - if (temp.isDirectory()) { - size = size + getFolderSize(temp); - } else { - size = size + temp.length(); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - return size; - } - - /** - * 格式化单位 - */ - private static String getFormatSize(double size) { - double kiloByte = size / 1024; - if (kiloByte < 1) { - // return size + "Byte"; - return "0K"; - } - - double megaByte = kiloByte / 1024; - if (megaByte < 1) { - BigDecimal result1 = new BigDecimal(Double.toString(kiloByte)); - return result1.setScale(2, BigDecimal.ROUND_HALF_UP) - .toPlainString() + "K"; - } - - double gigaByte = megaByte / 1024; - if (gigaByte < 1) { - BigDecimal result2 = new BigDecimal(Double.toString(megaByte)); - return result2.setScale(2, BigDecimal.ROUND_HALF_UP) - .toPlainString() + "M"; - } - - double teraBytes = gigaByte / 1024; - if (teraBytes < 1) { - BigDecimal result3 = new BigDecimal(Double.toString(gigaByte)); - return result3.setScale(2, BigDecimal.ROUND_HALF_UP) - .toPlainString() + "GB"; - } - BigDecimal result4 = new BigDecimal(teraBytes); - return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() - + "TB"; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/helper/InputTextHelper.java b/app/src/main/java/com/hjq/demo/helper/InputTextHelper.java deleted file mode 100644 index e12d2ce8..00000000 --- a/app/src/main/java/com/hjq/demo/helper/InputTextHelper.java +++ /dev/null @@ -1,305 +0,0 @@ -package com.hjq.demo.helper; - -import android.app.Activity; -import android.app.Application; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/10/18 - * desc : 文本输入辅助类,通过管理多个 TextView 输入是否为空来启用或者禁用按钮的点击事件 - * blog : https://www.jianshu.com/p/fd3795e8a6b3 - */ -public final class InputTextHelper implements TextWatcher { - - /** 操作按钮的View */ - private View mView; - /** 是否禁用后设置半透明度 */ - private boolean isAlpha; - - /** TextView集合 */ - private List mViewSet; - - /** 输入监听器 */ - private OnInputTextListener mListener; - - /** - * 构造函数 - * - * @param view 跟随 TextView 输入为空来判断启动或者禁用这个 View - * @param alpha 是否需要设置透明度 - */ - private InputTextHelper(View view, boolean alpha) { - if (view == null) { - throw new IllegalArgumentException("The view is empty"); - } - mView = view; - isAlpha = alpha; - } - - /** - * 创建 Builder - */ - public static Builder with(Activity activity) { - return new Builder(activity); - } - - /** - * 添加 TextView - * - * @param views 传入单个或者多个 TextView - */ - public void addViews(List views) { - if (views == null) { - return; - } - - if (mViewSet == null) { - mViewSet = views; - } else { - mViewSet.addAll(views); - } - - for (TextView view : views) { - view.addTextChangedListener(this); - } - - // 触发一次监听 - notifyChanged(); - } - - /** - * 添加 TextView - * - * @param views 传入单个或者多个 TextView - */ - public void addViews(TextView... views) { - if (views == null) { - return; - } - - if (mViewSet == null) { - mViewSet = new ArrayList<>(views.length); - } - - for (TextView view : views) { - // 避免重复添加 - if (!mViewSet.contains(view)) { - view.addTextChangedListener(this); - mViewSet.add(view); - } - } - // 触发一次监听 - notifyChanged(); - } - - /** - * 移除 TextView 监听,避免内存泄露 - */ - public void removeViews(TextView... views) { - if (mViewSet != null && mViewSet.size() > 0) { - for (TextView view : views) { - view.removeTextChangedListener(this); - mViewSet.remove(view); - } - // 触发一次监听 - notifyChanged(); - } - } - - /** - * 移除所有 TextView 监听,避免内存泄露 - */ - public void removeAllViews() { - if (mViewSet == null) { - return; - } - - for (TextView view : mViewSet) { - view.removeTextChangedListener(this); - } - mViewSet.clear(); - mViewSet = null; - } - - /** - * 设置输入监听 - */ - public void setListener(OnInputTextListener listener) { - mListener = listener; - } - - /** - * {@link TextWatcher} - */ - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - notifyChanged(); - } - - /** - * 通知更新 - */ - public void notifyChanged() { - if (mViewSet == null) { - return; - } - - // 重新遍历所有的输入 - for (TextView view : mViewSet) { - if ("".equals(view.getText().toString())) { - setEnabled(false); - return; - } - } - - if (mListener != null) { - setEnabled(mListener.onInputChange(this)); - } else { - setEnabled(true); - } - } - - /** - * 设置 View 的事件 - * - * @param enabled 启用或者禁用 View 的事件 - */ - public void setEnabled(boolean enabled) { - if (enabled == mView.isEnabled()) { - return; - } - - if (enabled) { - //启用View的事件 - mView.setEnabled(true); - if (isAlpha) { - //设置不透明 - mView.setAlpha(1f); - } - } else { - //禁用View的事件 - mView.setEnabled(false); - if (isAlpha) { - //设置半透明 - mView.setAlpha(0.5f); - } - } - } - - public static final class Builder { - - /** 当前的 Activity */ - private final Activity mActivity; - /** 操作按钮的 View */ - private View mView; - /** 是否禁用后设置半透明度 */ - private boolean isAlpha; - /** TextView集合 */ - private final List mViewSet = new ArrayList<>(); - /** 文本输入辅助类 */ - private InputTextHelper mTextHelper; - /** 文本 */ - private OnInputTextListener mListener; - - private Builder(Activity activity) { - mActivity = activity; - } - - public Builder addView(TextView view) { - mViewSet.add(view); - return this; - } - - public Builder setMain(View view) { - mView = view; - return this; - } - - public Builder setAlpha(boolean alpha) { - isAlpha = alpha; - return this; - } - - public Builder setListener(OnInputTextListener listener) { - mListener = listener; - return this; - } - - public InputTextHelper build(){ - mTextHelper = new InputTextHelper(mView, isAlpha); - mTextHelper.addViews(mViewSet); - mTextHelper.setListener(mListener); - mActivity.getApplication().registerActivityLifecycleCallbacks(new TextInputLifecycle(mActivity, mTextHelper)); - return mTextHelper; - } - } - - private static class TextInputLifecycle implements Application.ActivityLifecycleCallbacks { - - private Activity mActivity; - private InputTextHelper mTextHelper; - - private TextInputLifecycle(Activity activity, InputTextHelper helper) { - this.mActivity = activity; - this.mTextHelper = helper; - } - - @Override - public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {} - - @Override - public void onActivityStarted(@NonNull Activity activity) {} - - @Override - public void onActivityResumed(@NonNull Activity 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) { - if (mActivity != null && mActivity == activity) { - mTextHelper.removeAllViews(); - mActivity.getApplication().registerActivityLifecycleCallbacks(this); - mTextHelper = null; - mActivity = null; - } - } - } - - /** - * 文本变化监听器 - */ - public interface OnInputTextListener { - - /** - * 输入发生了变化 - * @return 返回按钮的 Enabled 状态 - */ - boolean onInputChange(InputTextHelper helper); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/helper/KeyboardUtils.java b/app/src/main/java/com/hjq/demo/helper/KeyboardUtils.java deleted file mode 100644 index 0a013139..00000000 --- a/app/src/main/java/com/hjq/demo/helper/KeyboardUtils.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/com/hjq/demo/helper/ManifestHelper.java b/app/src/main/java/com/hjq/demo/helper/ManifestHelper.java deleted file mode 100644 index 59215aa5..00000000 --- a/app/src/main/java/com/hjq/demo/helper/ManifestHelper.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.hjq.demo.helper; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Bundle; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/04/02 - * desc : 获取清单文件中的值 - */ -public final class ManifestHelper { - - /** - * 获取 meta-data 的值 - */ - private static Bundle getMetaData(Context context) { - try { - return context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).metaData; - } catch (PackageManager.NameNotFoundException ignored) { - return new Bundle(); - } - } - - /** - * 检查 key - * @param context 上下文 - * @return meta-data - */ - public static Bundle checkMetaData(Context context, String key) { - Bundle metaData = getMetaData(context); - if (!metaData.containsKey(key)) { - // 清单文件没有设置这个 key - throw new IllegalArgumentException("are you ok?"); - } - return metaData; - } - - /** - * 是否有设置这个值 - * - * @param context 上下文 - * @param key key - * @return value - */ - public static boolean contains(Context context, String key) { - return checkMetaData(context, key).containsKey(key); - } - - /** - * 获取 Object - * - * @param context 上下文 - * @param key key - * @return value - */ - public static Object get(Context context, String key) { - return checkMetaData(context, key).get(key); - } - - /** - * 获取 String - * - * @param context 上下文 - * @param key key - * @return value - */ - public static String getString(Context context, String key) { - return checkMetaData(context, key).getString(key); - } - - /** - * 获取 boolean - * - * @param context 上下文 - * @param key key - * @return value - */ - public static boolean getBoolean(Context context, String key) { - return checkMetaData(context, key).getBoolean(key); - } - - /** - * 获取 int - * - * @param context 上下文 - * @param key key - * @return value - */ - public static int getInt(Context context, String key) { - return checkMetaData(context, key).getInt(key); - } - - /** - * 获取 long - * - * @param context 上下文 - * @param key key - * @return value - */ - public static long getLong(Context context, String key) { - return checkMetaData(context, key).getLong(key); - } - - /** - * 获取 float - * - * @param context 上下文 - * @param key key - * @return value - */ - public static float getFloat(Context context, String key) { - return checkMetaData(context, key).getFloat(key); - } - - /** - * 获取 double - * - * @param context 上下文 - * @param key key - * @return value - */ - public static double getDouble(Context context, String key) { - return checkMetaData(context, key).getDouble(key); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/helper/PopupWindowHelper.java b/app/src/main/java/com/hjq/demo/helper/PopupWindowHelper.java deleted file mode 100644 index 25931a6b..00000000 --- a/app/src/main/java/com/hjq/demo/helper/PopupWindowHelper.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.hjq.demo.helper; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.ScaleAnimation; -import android.widget.PopupWindow; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/10/18 - * desc : PopupWindow辅助类 - */ -public final class PopupWindowHelper implements PopupWindow.OnDismissListener { - - /** PopupWindow 对象 */ - private PopupWindow mPopupWindow; - /** PopupWindow 显示的 View */ - private final View mPopupView; - /** 记录PopupWindow销毁时间 */ - private long mDismissTime; - - public PopupWindowHelper(View popupView) { - mPopupView = popupView; - } - - public PopupWindowHelper(Context context, int id) { - mPopupView = View.inflate(context, id, null); - } - - /** - * 初始化PopupWindow - */ - private void initPopupWindow() { - // 给PopupWindow的View设置缩放动画 - ScaleAnimation sa = new ScaleAnimation(0.5f, 1.0f, 0.5f, 1.0f, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); - sa.setDuration(200); - mPopupView.startAnimation(sa); - - mPopupWindow = new PopupWindow(mPopupView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - // 需要在popupWindow使用动画,必须先设置背景,否则动画不能显示出效果,为了不和当前的背景冲突,这里设置全透明背景的图片 - mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - // 设置外部可触的,点击其他地方会自动消失 - mPopupWindow.setTouchable(true); - mPopupWindow.setFocusable(true); - mPopupWindow.setOutsideTouchable(true); - // 监听PopupWindow销毁监听 - mPopupWindow.setOnDismissListener(this); - } - - /** - * {@link PopupWindow.OnDismissListener} - */ - @Override - public void onDismiss() { - // 记录当前销毁的时间 - mDismissTime = System.currentTimeMillis(); - } - - /** - * 显示一个PopupWindow - * - * @param clickView PopupWindow显示在什么View的下方 - */ - public void show(View clickView) { - - // 如果PopupWindow还未初始化就先进行初始化 - if (mPopupWindow == null) { - initPopupWindow(); - } - - // 避免用户点击clickView导致的销毁后再次显示的Bug - if (System.currentTimeMillis() - mDismissTime < 500) { - return; - } - - /* - //获取某个view对象在窗口的位置,然后计算出PopupWindow的位置 - int[] location = new int[2]; - mClickView.getLocationInWindow(location); - - //将PopupWindow显示出来 - mPopupWindow.showAtLocation(mParentView, Gravity.LEFT + Gravity.TOP, 0, location[1] + mClickView.getHeight()); - */ - - mPopupWindow.showAsDropDown(clickView); - } - - /** - * 销毁当前的PopupWindow - */ - public void dismiss() { - if (isShowing()) { - mPopupWindow.dismiss(); - } - } - - /** - * 当前PopupWindow是否已经显示 - */ - public boolean isShowing() { - return mPopupWindow != null && mPopupWindow.isShowing(); - } - - /** - * 获取当前的PopupWindow对象 - */ - @Nullable - public PopupWindow getPopupWindow() { - return mPopupWindow; - } - - /** - * 获取当前的PopupWindow的View对象 - */ - @NonNull - public View getPopupView() { - return mPopupView; - } -} diff --git a/app/src/main/java/com/hjq/demo/helper/RadioButtonGroupHelper.java b/app/src/main/java/com/hjq/demo/helper/RadioButtonGroupHelper.java deleted file mode 100644 index f561647a..00000000 --- a/app/src/main/java/com/hjq/demo/helper/RadioButtonGroupHelper.java +++ /dev/null @@ -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 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("The resource id must be set for the RadioButton"); - } - 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); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/helper/WebViewLifecycleUtils.java b/app/src/main/java/com/hjq/demo/helper/WebViewLifecycleUtils.java deleted file mode 100644 index 30e7cbb9..00000000 --- a/app/src/main/java/com/hjq/demo/helper/WebViewLifecycleUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.hjq.demo.helper; - -import android.view.ViewGroup; -import android.webkit.WebView; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/10/18 - * desc : WebView 生命周期优化工具 - */ -public final class WebViewLifecycleUtils { - - public static void onResume(WebView webView) { - webView.onResume(); - webView.resumeTimers(); - } - - public static void onPause(WebView webView) { - webView.onPause(); - webView.pauseTimers(); - } - - public static void onDestroy(WebView webView) { - ((ViewGroup) webView.getParent()).removeView(webView); - //清除历史记录 - webView.clearHistory(); - //停止加载 - webView.stopLoading(); - //加载一个空白页 - webView.loadUrl("about:blank"); - webView.setWebChromeClient(null); - webView.setWebViewClient(null); - //移除WebView所有的View对象 - webView.removeAllViews(); - //销毁此的WebView的内部状态 - webView.destroy(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/CopyApi.java b/app/src/main/java/com/hjq/demo/http/api/CopyApi.java new file mode 100644 index 00000000..e11b1f4a --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/CopyApi.java @@ -0,0 +1,23 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 可进行拷贝的副本 + */ +public final class CopyApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return ""; + } + + public final static class Bean { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.java b/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.java new file mode 100644 index 00000000..1eee3606 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.java @@ -0,0 +1,27 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 获取验证码 + */ +public final class GetCodeApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "code/get"; + } + + /** 手机号 */ + private String phone; + + public GetCodeApi setPhone(String phone) { + this.phone = phone; + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/LoginApi.java b/app/src/main/java/com/hjq/demo/http/api/LoginApi.java new file mode 100644 index 00000000..b0abf283 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/LoginApi.java @@ -0,0 +1,43 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 用户登录 + */ +public final class LoginApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "user/login"; + } + + /** 手机号 */ + private String phone; + /** 登录密码 */ + private String password; + + public LoginApi setPhone(String phone) { + this.phone = phone; + return this; + } + + public LoginApi setPassword(String password) { + this.password = password; + return this; + } + + public final static class Bean { + + private String token; + + public String getToken() { + return token; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/LogoutApi.java b/app/src/main/java/com/hjq/demo/http/api/LogoutApi.java new file mode 100644 index 00000000..f025893c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/LogoutApi.java @@ -0,0 +1,19 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 退出登录 + */ +public final class LogoutApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "user/logout"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/PasswordApi.java b/app/src/main/java/com/hjq/demo/http/api/PasswordApi.java new file mode 100644 index 00000000..59b75b45 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/PasswordApi.java @@ -0,0 +1,41 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 修改密码 + */ +public final class PasswordApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "user/password"; + } + + /** 手机号(已登录可不传) */ + private String phone; + /** 验证码 */ + private String code; + /** 密码 */ + private String password; + + public PasswordApi setPhone(String phone) { + this.phone = phone; + return this; + } + + public PasswordApi setCode(String code) { + this.code = code; + return this; + } + + public PasswordApi setPassword(String password) { + this.password = password; + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/PhoneApi.java b/app/src/main/java/com/hjq/demo/http/api/PhoneApi.java new file mode 100644 index 00000000..e881b6e0 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/PhoneApi.java @@ -0,0 +1,42 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 修改手机 + */ +public final class PhoneApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "user/phone"; + } + + /** 旧手机号验证码(没有绑定情况下可不传) */ + private String preCode; + + /** 新手机号 */ + private String phone; + /** 新手机号验证码 */ + private String code; + + public PhoneApi setPreCode(String preCode) { + this.preCode = preCode; + return this; + } + + public PhoneApi setPhone(String phone) { + this.phone = phone; + return this; + } + + public PhoneApi setCode(String code) { + this.code = code; + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/RegisterApi.java b/app/src/main/java/com/hjq/demo/http/api/RegisterApi.java new file mode 100644 index 00000000..f7e562ad --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/RegisterApi.java @@ -0,0 +1,45 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 用户注册 + */ +public final class RegisterApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "user/register"; + } + + /** 手机号 */ + private String phone; + /** 验证码 */ + private String code; + /** 密码 */ + private String password; + + public RegisterApi setPhone(String phone) { + this.phone = phone; + return this; + } + + public RegisterApi setCode(String code) { + this.code = code; + return this; + } + + public RegisterApi setPassword(String password) { + this.password = password; + return this; + } + + public final static class Bean { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.java b/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.java new file mode 100644 index 00000000..a27da66f --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.java @@ -0,0 +1,28 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; +import java.io.File; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 上传图片 + */ +public final class UpdateImageApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "update/image"; + } + + /** 图片文件 */ + private File image; + + public UpdateImageApi setImage(File image) { + this.image = image; + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.java b/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.java new file mode 100644 index 00000000..e7c33d0d --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.java @@ -0,0 +1,21 @@ +package com.hjq.demo.http.api; + +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 获取用户信息 + */ +public final class UserInfoApi implements IRequestApi { + + @Override + public String getApi() { + return "user/info"; + } + + public final static class Bean { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.java b/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.java new file mode 100644 index 00000000..4cff60b2 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.java @@ -0,0 +1,34 @@ +package com.hjq.demo.http.api; + +import androidx.annotation.NonNull; +import com.hjq.http.config.IRequestApi; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 验证码校验 + */ +public final class VerifyCodeApi implements IRequestApi { + + @NonNull + @Override + public String getApi() { + return "code/checkout"; + } + + /** 手机号 */ + private String phone; + /** 验证码 */ + private String code; + + public VerifyCodeApi setPhone(String phone) { + this.phone = phone; + return this; + } + + public VerifyCodeApi setCode(String code) { + this.code = code; + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/exception/ResultException.java b/app/src/main/java/com/hjq/demo/http/exception/ResultException.java new file mode 100644 index 00000000..24289522 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/exception/ResultException.java @@ -0,0 +1,32 @@ +package com.hjq.demo.http.exception; + +import androidx.annotation.NonNull; +import com.hjq.demo.http.model.HttpData; +import com.hjq.http.exception.HttpException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2021/12/19 + * desc : 返回结果异常 + */ +public final class ResultException extends HttpException { + + @NonNull + private final HttpData mData; + + public ResultException(@NonNull String message, @NonNull HttpData data) { + super(message); + mData = data; + } + + public ResultException(@NonNull String message, @NonNull Throwable cause, @NonNull HttpData data) { + super(message, cause); + mData = data; + } + + @NonNull + public HttpData getHttpData() { + return mData; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/exception/TokenException.java b/app/src/main/java/com/hjq/demo/http/exception/TokenException.java new file mode 100644 index 00000000..da6b51dc --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/exception/TokenException.java @@ -0,0 +1,21 @@ +package com.hjq.demo.http.exception; + +import androidx.annotation.NonNull; +import com.hjq.http.exception.HttpException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2021/12/19 + * desc : Token 失效异常 + */ +public final class TokenException extends HttpException { + + public TokenException(@NonNull String message) { + super(message); + } + + public TokenException(@NonNull String message, @NonNull Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.java b/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.java new file mode 100644 index 00000000..672ade3e --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.java @@ -0,0 +1,81 @@ +package com.hjq.demo.http.glide; + +import android.content.Context; +import androidx.annotation.NonNull; +import com.bumptech.glide.Glide; +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; +import com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper; +import com.bumptech.glide.load.engine.cache.LruResourceCache; +import com.bumptech.glide.load.engine.cache.MemorySizeCalculator; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.module.AppGlideModule; +import com.bumptech.glide.request.RequestOptions; +import com.hjq.demo.R; +import com.hjq.http.EasyConfig; +import java.io.File; +import java.io.InputStream; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/15 + * desc : Glide 全局配置 + */ +@GlideModule +public final class GlideConfig extends AppGlideModule { + + /** 本地图片缓存文件最大值 */ + private static final int IMAGE_DISK_CACHE_MAX_SIZE = 500 * 1024 * 1024; + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Override + public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { + // 读写外部缓存目录不需要申请存储权限 + File diskCacheFile = new File(context.getCacheDir(), "glide"); + // 如果这个路径是一个文件 + if (diskCacheFile.exists() && diskCacheFile.isFile()) { + // 执行删除操作 + // noinspection ResultOfMethodCallIgnored + diskCacheFile.delete(); + } + // 如果这个路径不存在 + if (!diskCacheFile.exists()) { + // 创建多级目录 + if (!diskCacheFile.mkdirs()) { + // 如果创建失败,并且文件夹不存在 + return; + } + } + builder.setDiskCache(() -> DiskLruCacheWrapper.create(diskCacheFile, IMAGE_DISK_CACHE_MAX_SIZE)); + + MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context).build(); + int defaultMemoryCacheSize = calculator.getMemoryCacheSize(); + int defaultBitmapPoolSize = calculator.getBitmapPoolSize(); + + long customMemoryCacheSize = (long) (1.2 * defaultMemoryCacheSize); + long customBitmapPoolSize = (long) (1.2 * defaultBitmapPoolSize); + + builder.setMemoryCache(new LruResourceCache(customMemoryCacheSize)); + builder.setBitmapPool(new LruBitmapPool(customBitmapPoolSize)); + + builder.setDefaultRequestOptions(new RequestOptions() + // 设置默认加载中占位图 + .placeholder(R.drawable.image_loading_ic) + // 设置默认加载出错占位图 + .error(R.drawable.image_error_ic)); + } + + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + // Glide 默认使用的是 HttpURLConnection 来做网络请求,这里切换成更高效的 OkHttp + registry.replace(GlideUrl.class, InputStream.class, new OkHttpLoader.Factory(EasyConfig.getInstance().getClient())); + } + + @Override + public boolean isManifestParsingEnabled() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.java b/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.java new file mode 100644 index 00000000..0287c7a5 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.java @@ -0,0 +1,106 @@ +package com.hjq.demo.http.glide; + +import androidx.annotation.NonNull; +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.HttpException; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.util.ContentLengthInputStream; +import com.bumptech.glide.util.Preconditions; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/15 + * desc : OkHttp 加载器 + */ +public final class OkHttpFetcher implements DataFetcher, Callback { + + private final Call.Factory mCallFactory; + private final GlideUrl mGlideUrl; + private InputStream mInputStream; + private ResponseBody mResponseBody; + private DataCallback mDataCallback; + private volatile Call mCall; + + OkHttpFetcher(Call.Factory factory, GlideUrl url) { + mCallFactory = factory; + mGlideUrl = url; + } + + @Override + public void loadData(@NonNull Priority priority, @NonNull final DataFetcher.DataCallback callback) { + Request.Builder requestBuilder = new Request.Builder().url(mGlideUrl.toStringUrl()); + for (Map.Entry headerEntry : mGlideUrl.getHeaders().entrySet()) { + String key = headerEntry.getKey(); + requestBuilder.addHeader(key, headerEntry.getValue()); + } + Request request = requestBuilder.build(); + mDataCallback = callback; + + mCall = mCallFactory.newCall(request); + mCall.enqueue(this); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + mDataCallback.onLoadFailed(e); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + mResponseBody = response.body(); + if (response.isSuccessful()) { + long contentLength = Preconditions.checkNotNull(mResponseBody).contentLength(); + mInputStream = ContentLengthInputStream.obtain(mResponseBody.byteStream(), contentLength); + mDataCallback.onDataReady(mInputStream); + } else { + mDataCallback.onLoadFailed(new HttpException(response.message(), response.code())); + } + } + + @Override + public void cleanup() { + try { + if (mInputStream != null) { + mInputStream.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + if (mResponseBody != null) { + mResponseBody.close(); + } + mDataCallback = null; + } + + @Override + public void cancel() { + if (mCall == null) { + return; + } + mCall.cancel(); + } + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.REMOTE; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.java b/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.java new file mode 100644 index 00000000..d15e1189 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.java @@ -0,0 +1,57 @@ +package com.hjq.demo.http.glide; + +import androidx.annotation.NonNull; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; +import java.io.InputStream; +import okhttp3.Call; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/15 + * desc : OkHttp 加载模型 + */ +public final class OkHttpLoader implements ModelLoader { + + @NonNull + private final Call.Factory mFactory; + + private OkHttpLoader(@NonNull Call.Factory factory) { + mFactory = factory; + } + + @Override + public boolean handles(@NonNull GlideUrl url) { + return true; + } + + @Override + public LoadData buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) { + return new LoadData<>(model, new OkHttpFetcher(mFactory, model)); + } + + public static class Factory implements ModelLoaderFactory { + + @NonNull + private final Call.Factory mFactory; + + Factory(@NonNull Call.Factory factory) { + mFactory = factory; + } + + @NonNull + @Override + public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { + return new OkHttpLoader(mFactory); + } + + @Override + public void teardown() { + // default implementation ignored + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpCacheManager.java b/app/src/main/java/com/hjq/demo/http/model/HttpCacheManager.java new file mode 100644 index 00000000..aaf059d3 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpCacheManager.java @@ -0,0 +1,99 @@ +package com.hjq.demo.http.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.http.config.IRequestApi; +import com.hjq.http.request.HttpRequest; +import com.tencent.mmkv.MMKV; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2022/03/22 + * desc : Http 缓存管理器 + */ +public final class HttpCacheManager { + + @NonNull + private static final MMKV HTTP_CACHE_CONTENT = MMKV.mmkvWithID("http_cache_content");; + + @NonNull + private static final MMKV HTTP_CACHE_TIME = MMKV.mmkvWithID("http_cache_time"); + + /** + * 生成缓存的 key + */ + @NonNull + public static String generateCacheKey(@NonNull HttpRequest httpRequest) { + IRequestApi requestApi = httpRequest.getRequestApi(); + return "请替换成当前的用户 id" + "\n" + requestApi.getApi() + "\n" + GsonFactory.getSingletonGson().toJson(requestApi); + } + + /** + * 读取缓存 + */ + @Nullable + public static String readHttpCache(@NonNull String cacheKey) { + String cacheValue = HTTP_CACHE_CONTENT.getString(cacheKey, null); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return null; + } + return cacheValue; + } + + /** + * 写入缓存 + */ + public static boolean writeHttpCache(@NonNull String cacheKey, @NonNull String cacheValue) { + return HTTP_CACHE_CONTENT.putString(cacheKey, cacheValue).commit(); + } + + /** + * 删除缓存 + */ + public static boolean deleteHttpCache(@NonNull String cacheKey) { + return HTTP_CACHE_CONTENT.remove(cacheKey).commit(); + } + + /** + * 清理缓存 + */ + public static void clearCache() { + HTTP_CACHE_CONTENT.clearMemoryCache(); + HTTP_CACHE_CONTENT.clearAll(); + + HTTP_CACHE_TIME.clearMemoryCache(); + HTTP_CACHE_TIME.clearAll(); + } + + /** + * 获取 Http 写入缓存的时间 + */ + public static long getHttpCacheTime(@NonNull String cacheKey) { + return HTTP_CACHE_TIME.getLong(cacheKey, 0); + } + + /** + * 设置 Http 写入缓存的时间 + */ + public static boolean setHttpCacheTime(@NonNull String cacheKey, long cacheTime) { + return HTTP_CACHE_TIME.putLong(cacheKey, cacheTime).commit(); + } + + /** + * 判断缓存是否过期 + */ + public static boolean isCacheInvalidate(@NonNull String cacheKey, long maxCacheTime) { + if (maxCacheTime == Long.MAX_VALUE) { + // 表示缓存长期有效,永远不会过期 + return false; + } + long httpCacheTime = getHttpCacheTime(cacheKey); + if (httpCacheTime == 0) { + // 表示不知道缓存的时间,这里默认当做已经过期了 + return true; + } + return httpCacheTime + maxCacheTime < System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpCacheStrategy.java b/app/src/main/java/com/hjq/demo/http/model/HttpCacheStrategy.java new file mode 100644 index 00000000..091b4205 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpCacheStrategy.java @@ -0,0 +1,74 @@ +package com.hjq.demo.http.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.http.EasyLog; +import com.hjq.http.config.IHttpCacheStrategy; +import com.hjq.http.request.HttpRequest; +import java.lang.reflect.Type; +import okhttp3.Response; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2025/03/23 + * desc : 请求缓存策略实现类 + */ +public final class HttpCacheStrategy implements IHttpCacheStrategy { + + @Nullable + @Override + public Object readCache(@NonNull HttpRequest httpRequest, @NonNull Type type, long cacheTime) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + String cacheValue = HttpCacheManager.readHttpCache(cacheKey); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return null; + } + EasyLog.printLog(httpRequest, "----- read cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + EasyLog.printLog(httpRequest, "----- read cache value -----"); + EasyLog.printJson(httpRequest, cacheValue); + EasyLog.printLog(httpRequest, "cacheTime = " + cacheTime); + boolean cacheInvalidate = HttpCacheManager.isCacheInvalidate(cacheKey, cacheTime); + EasyLog.printLog(httpRequest, "cacheInvalidate = " + cacheInvalidate); + if (cacheInvalidate) { + // 表示缓存已经过期了,直接返回 null 给外层,表示缓存不可用 + return null; + } + return GsonFactory.getSingletonGson().fromJson(cacheValue, type); + } + + @Override + public boolean writeCache(@NonNull HttpRequest httpRequest, @NonNull Response response, @NonNull Object result) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + String cacheValue = GsonFactory.getSingletonGson().toJson(result); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return false; + } + EasyLog.printLog(httpRequest, "----- write cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + EasyLog.printLog(httpRequest, "----- write cache value -----"); + EasyLog.printJson(httpRequest, cacheValue); + boolean writeHttpCacheResult = HttpCacheManager.writeHttpCache(cacheKey, cacheValue); + EasyLog.printLog(httpRequest, "writeHttpCacheResult = " + writeHttpCacheResult); + boolean refreshHttpCacheTimeResult = HttpCacheManager.setHttpCacheTime(cacheKey, System.currentTimeMillis()); + EasyLog.printLog(httpRequest, "refreshHttpCacheTimeResult = " + refreshHttpCacheTimeResult); + return writeHttpCacheResult && refreshHttpCacheTimeResult; + } + + @Override + public boolean deleteCache(@NonNull HttpRequest httpRequest) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + EasyLog.printLog(httpRequest, "----- delete cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + boolean deleteHttpCacheResult = HttpCacheManager.deleteHttpCache(cacheKey); + EasyLog.printLog(httpRequest, "deleteHttpCacheResult = " + deleteHttpCacheResult); + return deleteHttpCacheResult; + } + + @Override + public void clearCache() { + HttpCacheManager.clearCache(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpData.java b/app/src/main/java/com/hjq/demo/http/model/HttpData.java new file mode 100644 index 00000000..8a1fa7d5 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpData.java @@ -0,0 +1,69 @@ +package com.hjq.demo.http.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.Map; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 统一接口数据结构 + */ +public class HttpData { + + /** 响应头 */ + @Nullable + private Map responseHeaders; + + /** 返回码 */ + private int code; + + /** 提示语 */ + @Nullable + private String msg; + + /** 数据 */ + @Nullable + private T data; + + public void setResponseHeaders(@Nullable Map responseHeaders) { + this.responseHeaders = responseHeaders; + } + + @Nullable + public Map getResponseHeaders() { + return responseHeaders; + } + + public int getCode() { + return code; + } + + @NonNull + public String getMessage() { + if (msg == null) { + return ""; + } + return msg; + } + + @Nullable + public T getData() { + return data; + } + + /** + * 是否请求成功 + */ + public boolean isRequestSuccess() { + return code == 200; + } + + /** + * 是否 Token 失效 + */ + public boolean isTokenInvalidation() { + return code == 1001; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpListData.java b/app/src/main/java/com/hjq/demo/http/model/HttpListData.java new file mode 100644 index 00000000..472c2430 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpListData.java @@ -0,0 +1,57 @@ +package com.hjq.demo.http.model; + +import androidx.annotation.Nullable; +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/10/07 + * desc : 统一接口列表数据结构 + */ +public class HttpListData extends HttpData> { + + public static class ListBean { + + /** 当前页码 */ + private int pageIndex; + /** 页大小 */ + private int pageSize; + /** 总数量 */ + private int totalNumber; + /** 数据 */ + @Nullable + private List items; + + /** + * 判断是否是最后一页 + */ + public boolean isLastPage() { + if (items == null) { + return true; + } + if (pageSize == 0) { + // 避免出现除零异常 + return true; + } + return Math.ceil((float) totalNumber / pageSize) <= pageIndex; + } + + public int getTotalNumber() { + return totalNumber; + } + + public int getPageIndex() { + return pageIndex; + } + + public int getPageSize() { + return pageSize; + } + + @Nullable + public List getItems() { + return items; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/RequestHandler.java b/app/src/main/java/com/hjq/demo/http/model/RequestHandler.java new file mode 100644 index 00000000..9ee53b00 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/RequestHandler.java @@ -0,0 +1,200 @@ +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.annotation.NonNull; +import com.hjq.core.manager.ActivityManager; +import com.hjq.demo.R; +import com.hjq.demo.http.exception.ResultException; +import com.hjq.demo.http.exception.TokenException; +import com.hjq.demo.ui.activity.account.LoginActivity; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.http.EasyLog; +import com.hjq.http.config.IRequestHandler; +import com.hjq.http.exception.CancelException; +import com.hjq.http.exception.DataException; +import com.hjq.http.exception.FileMd5Exception; +import com.hjq.http.exception.HttpException; +import com.hjq.http.exception.NetworkException; +import com.hjq.http.exception.NullBodyException; +import com.hjq.http.exception.ResponseException; +import com.hjq.http.exception.ServerException; +import com.hjq.http.exception.TimeoutException; +import com.hjq.http.request.HttpRequest; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; +import okhttp3.Headers; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 请求处理类 + */ +public final class RequestHandler implements IRequestHandler { + + private final Application mApplication; + + public RequestHandler(Application application) { + mApplication = application; + } + + @NonNull + @Override + public Object requestSuccess(@NonNull HttpRequest httpRequest, @NonNull Response response, @NonNull Type type) throws Throwable { + if (Response.class.equals(type)) { + return response; + } + + if (!response.isSuccessful()) { + throw new ResponseException(String.format(mApplication.getString(R.string.http_response_error), + response.code(), response.message()), response); + } + + if (Object.class.equals(type)) { + return ""; + } + + if (Headers.class.equals(type)) { + return response.headers(); + } + + ResponseBody body = response.body(); + if (body == null) { + throw new NullBodyException(mApplication.getString(R.string.http_response_null_body)); + } + + if (InputStream.class.equals(type)) { + return body.byteStream(); + } + + if (Bitmap.class.equals(type)) { + return BitmapFactory.decodeStream(body.byteStream()); + } + + String text; + try { + text = body.string(); + } catch (IOException e) { + // 返回结果读取异常 + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + + // 打印这个 Json 或者文本 + EasyLog.printJson(httpRequest, text); + + if (String.class.equals(type)) { + return text; + } + + final Object result; + + try { + result = GsonFactory.getSingletonGson().fromJson(text, type); + } catch (Exception e) { + // 返回结果读取异常 + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + + if (result instanceof HttpData) { + HttpData model = (HttpData) result; + Headers headers = response.headers(); + int headersSize = headers.size(); + Map headersMap = new HashMap<>(headersSize); + for (int i = 0; i < headersSize; i++) { + headersMap.put(headers.name(i), headers.value(i)); + } + // Github issue 地址:https://github.com/getActivity/EasyHttp/issues/233 + model.setResponseHeaders(headersMap); + + if (model.isRequestSuccess()) { + // 代表执行成功 + return result; + } + + if (model.isTokenInvalidation()) { + // 代表登录失效,需要重新登录 + throw new TokenException(mApplication.getString(R.string.http_token_error)); + } + + // 代表执行失败 + throw new ResultException(model.getMessage(), model); + } + return result; + } + + @NonNull + @Override + public Throwable requestFail(@NonNull HttpRequest httpRequest, @NonNull Throwable throwable) { + if (throwable instanceof HttpException) { + if (throwable instanceof TokenException) { + // 登录信息失效,跳转到登录页 + Application application = ActivityManager.getInstance().getApplication(); + Intent intent = new Intent(application, LoginActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + application.startActivity(intent); + // 销毁除了登录页之外的 Activity + ActivityManager.getInstance().finishAllActivities(LoginActivity.class); + } + return throwable; + } + + if (throwable instanceof SocketTimeoutException) { + return new TimeoutException(mApplication.getString(R.string.http_server_out_time), throwable); + } + + if (throwable instanceof UnknownHostException) { + NetworkInfo info = ((ConnectivityManager) mApplication.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); + // 判断网络是否连接 + if (info == null || !info.isConnected()) { + // 没有连接就是网络异常 + return new NetworkException(mApplication.getString(R.string.http_network_error), throwable); + } + + // 有连接就是服务器的问题 + return new ServerException(mApplication.getString(R.string.http_server_error), throwable); + } + + if (throwable instanceof IOException) { + // 出现该异常的两种情况 + // 1. 调用 EasyHttp.cancel + // 2. 网络请求被中断 + return new CancelException(mApplication.getString(R.string.http_request_cancel), throwable); + } + + return new HttpException(throwable.getMessage(), throwable); + } + + @NonNull + @Override + public Throwable downloadFail(@NonNull HttpRequest httpRequest, @NonNull Throwable throwable) { + if (throwable instanceof ResponseException) { + ResponseException responseException = ((ResponseException) throwable); + Response response = responseException.getResponse(); + responseException.setMessage(String.format(mApplication.getString(R.string.http_response_error), + response.code(), response.message())); + return responseException; + } else if (throwable instanceof NullBodyException) { + NullBodyException nullBodyException = ((NullBodyException) throwable); + nullBodyException.setMessage(mApplication.getString(R.string.http_response_null_body)); + return nullBodyException; + } else if (throwable instanceof FileMd5Exception) { + FileMd5Exception fileMd5Exception = ((FileMd5Exception) throwable); + fileMd5Exception.setMessage(mApplication.getString(R.string.http_response_md5_error)); + return fileMd5Exception; + } + return requestFail(httpRequest, throwable); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/RequestServer.java b/app/src/main/java/com/hjq/demo/http/model/RequestServer.java new file mode 100644 index 00000000..9195ab8b --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/RequestServer.java @@ -0,0 +1,29 @@ +package com.hjq.demo.http.model; + +import androidx.annotation.NonNull; +import com.hjq.demo.other.AppConfig; +import com.hjq.http.config.IHttpPostBodyStrategy; +import com.hjq.http.config.IRequestServer; +import com.hjq.http.model.RequestBodyType; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/10/02 + * desc : 服务器配置 + */ +public class RequestServer implements IRequestServer { + + @NonNull + @Override + public String getHost() { + return AppConfig.getHostUrl() + "api/"; + } + + @NonNull + @Override + public IHttpPostBodyStrategy getBodyType() { + // 以表单的形式提交参数 + return RequestBodyType.FORM; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/DialogManager.java b/app/src/main/java/com/hjq/demo/manager/DialogManager.java new file mode 100644 index 00000000..462fc2f0 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/DialogManager.java @@ -0,0 +1,143 @@ +package com.hjq.demo.manager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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; +import java.util.Map; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2021/01/29 + * desc : Dialog 显示管理类 + */ +@SuppressWarnings("MapOrSetKeyShouldOverrideHashCodeEquals") +public final class DialogManager implements LifecycleEventObserver, BaseDialog.OnDismissListener { + + @NonNull + private static final Map 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; + } + + @NonNull + private final List mDialogList = new ArrayList<>(); + + @NonNull + private final Map mDialogPriority = new HashMap<>(); + + private DialogManager(LifecycleOwner lifecycleOwner) { + lifecycleOwner.getLifecycle().addObserver(this); + } + + /** + * 获取所有排队显示的对话框 + */ + @NonNull + public List getDialogList() { + return mDialogList; + } + + public void addDialog(@Nullable BaseDialog dialog) { + addDialog(dialog, 0); + } + + /** + * 添加 Dialog 对象 + * + * @param priority 弹窗优先级 + */ + public void addDialog(@Nullable BaseDialog dialog, int priority) { + if (dialog == null) { + return; + } + + if (mDialogList.contains(dialog)) { + return; + } + + int dialogIndex = mDialogList.size(); + for (int i = 0; i < mDialogList.size(); i++) { + BaseDialog itemDialog = mDialogList.get(i); + Integer itemPriority = mDialogPriority.get(itemDialog); + if (itemPriority == null) { + continue; + } + if (priority > itemPriority && !itemDialog.isShowing()) { + dialogIndex = i; + } + } + mDialogList.add(dialogIndex, dialog); + mDialogPriority.put(dialog, priority); + } + + /** + * 排队显示 Dialog + */ + public void startShow() { + if (mDialogList.isEmpty()) { + return; + } + BaseDialog firstDialog = mDialogList.get(0); + if (!firstDialog.isShowing()) { + firstDialog.addOnDismissListener(this); + firstDialog.show(); + } + } + + /** + * 取消所有 Dialog 的显示 + */ + public void clearShow() { + if (mDialogList.isEmpty()) { + return; + } + BaseDialog firstDialog = mDialogList.get(0); + if (firstDialog.isShowing()) { + firstDialog.removeOnDismissListener(this); + firstDialog.dismiss(); + } + mDialogList.clear(); + mDialogPriority.clear(); + } + + @Override + public void onDismiss(@NonNull BaseDialog dialog) { + dialog.removeOnDismissListener(this); + mDialogList.remove(dialog); + mDialogPriority.remove(dialog); + for (BaseDialog nextDialog : mDialogList) { + 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; + } + DIALOG_MANAGER.remove(source); + source.getLifecycle().removeObserver(this); + clearShow(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/InitManager.java b/app/src/main/java/com/hjq/demo/manager/InitManager.java new file mode 100644 index 00000000..f77e1951 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/InitManager.java @@ -0,0 +1,226 @@ +package com.hjq.demo.manager; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.Network; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import com.chuckerteam.chucker.api.ChuckerInterceptor; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonToken; +import com.hjq.bar.TitleBar; +import com.hjq.core.manager.ActivityManager; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.demo.http.model.HttpCacheStrategy; +import com.hjq.demo.http.model.RequestHandler; +import com.hjq.demo.http.model.RequestServer; +import com.hjq.demo.other.AppConfig; +import com.hjq.demo.other.CrashHandler; +import com.hjq.demo.other.DebugLoggerTree; +import com.hjq.demo.other.MaterialHeader; +import com.hjq.demo.other.SmartBallPulseFooter; +import com.hjq.demo.other.TitleBarStyle; +import com.hjq.demo.other.ToastInterceptor; +import com.hjq.demo.other.ToastStyle; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.gson.factory.ParseExceptionCallback; +import com.hjq.http.EasyConfig; +import com.hjq.http.config.IRequestInterceptor; +import com.hjq.http.model.HttpHeaders; +import com.hjq.http.model.HttpParams; +import com.hjq.http.request.HttpRequest; +import com.hjq.toast.Toaster; +import com.hjq.umeng.sdk.UmengClient; +import com.scwang.smart.refresh.layout.SmartRefreshLayout; +import com.tencent.bugly.library.Bugly; +import com.tencent.bugly.library.BuglyBuilder; +import com.tencent.mmkv.MMKV; +import okhttp3.OkHttpClient; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2023/06/24 + * desc : 初始化管理器 + */ +public final class InitManager { + + /** 隐私政策配置文件 */ + private static final String AGREE_PRIVACY_NAME = "agree_privacy_config"; + /** 隐私政策同意结果 */ + private static final String KEY_AGREE_PRIVACY_RESULT = "key_agree_privacy_result"; + + /** + * 是否同意了隐私协议 + */ + public static boolean isAgreePrivacy(@NonNull Context context) { + SharedPreferences sharedPreferences = context.getSharedPreferences(AGREE_PRIVACY_NAME, Context.MODE_PRIVATE); + return sharedPreferences.getBoolean(KEY_AGREE_PRIVACY_RESULT, false); + } + + /** + * 设置隐私协议结果 + */ + public static void setAgreePrivacy(@NonNull Context context, boolean result) { + SharedPreferences sharedPreferences = context.getSharedPreferences(AGREE_PRIVACY_NAME, Context.MODE_PRIVATE); + sharedPreferences.edit().putBoolean(KEY_AGREE_PRIVACY_RESULT, result).apply(); + } + + /** + * 预初始化第三方 SDK + */ + public static void preInitSdk(@NonNull Application application) { + // 初始化日志打印 + if (AppConfig.isLogEnable()) { + Timber.plant(new DebugLoggerTree()); + } + + // 设置标题栏全局样式 + TitleBar.setGlobalStyle(new TitleBarStyle()); + + // 设置全局的 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); + + // 关闭框架预埋的彩蛋 + // https://github.com/scwang90/SmartRefreshLayout/issues/1105 + layout.getLayout().setTag("close egg"); + }); + + // 初始化吐司 + Toaster.init(application, new ToastStyle()); + // 设置调试模式 + Toaster.setDebugMode(AppConfig.isDebug()); + // 设置 Toast 拦截器 + Toaster.setInterceptor(new ToastInterceptor()); + + // 本地异常捕捉 + CrashHandler.register(application); + + // Bugly 异常捕捉 + BuglyBuilder builder = new BuglyBuilder(AppConfig.getBuglyId(), AppConfig.getBuglyKey()); + builder.debugMode = AppConfig.isDebug(); + Bugly.init(application, builder); + + // Activity 栈管理初始化 + ActivityManager.getInstance().init(application); + + // MMKV 初始化 + MMKV.initialize(application); + + // 网络请求框架初始化 + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new ChuckerInterceptor(application)) + .build(); + + EasyConfig.with(okHttpClient) + // 是否打印日志 + .setLogEnabled(AppConfig.isLogEnable()) + // 设置服务器配置 + .setServer(new RequestServer()) + // 设置请求处理策略 + .setHandler(new RequestHandler(application)) + // 设置请求缓存实现策略(非必须) + .setCacheStrategy(new HttpCacheStrategy()) + // 设置请求重试次数 + .setRetryCount(1) + .setInterceptor(new IRequestInterceptor() { + @Override + public void interceptArguments(@NonNull HttpRequest httpRequest, + @NonNull HttpParams params, + @NonNull HttpHeaders headers) { + // 添加全局请求头 + headers.put("token", "66666666666"); + headers.put("deviceOaid", UmengClient.getDeviceOaid()); + headers.put("versionName", AppConfig.getVersionName()); + headers.put("versionCode", String.valueOf(AppConfig.getVersionCode())); + // 添加全局请求参数 + // params.put("6666666", "6666666"); + } + }) + .into(); + + // 设置 Json 解析容错监听 + GsonFactory.setParseExceptionCallback(new ParseExceptionCallback() { + + @Override + public void onParseObjectException(TypeToken typeToken, String fieldName, JsonToken jsonToken) { + handlerGsonParseException("解析对象析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken); + } + + @Override + public void onParseListItemException(TypeToken typeToken, String fieldName, JsonToken listItemJsonToken) { + handlerGsonParseException("解析 List 异常:" + typeToken + "#" + fieldName + ",后台返回的条目类型为:" + listItemJsonToken); + } + + @Override + public void onParseMapItemException(TypeToken typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) { + handlerGsonParseException("解析 Map 异常:" + typeToken + "#" + fieldName + ",mapItemKey = " + mapItemKey + ",后台返回的条目类型为:" + mapItemJsonToken); + } + + private void handlerGsonParseException(String message) { + IllegalArgumentException e = new IllegalArgumentException(message); + if (AppConfig.isDebug()) { + throw e; + } else { + // 上报到 Bugly 错误列表中 + Bugly.handleCatchException(Thread.currentThread(), e, e.getMessage(), null, true); + } + } + }); + + // 注册网络状态变化监听 + ConnectivityManager connectivityManager = ContextCompat.getSystemService(application, ConnectivityManager.class); + if (connectivityManager != null && AndroidVersion.isAndroid7()) { + connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { + + @Override + public void onLost(@NonNull Network network) { + Activity topActivity = ActivityManager.getInstance().getTopActivity(); + if (!(topActivity instanceof LifecycleOwner)) { + return; + } + + LifecycleOwner lifecycleOwner = ((LifecycleOwner) topActivity); + if (lifecycleOwner.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED) { + return; + } + + Toaster.show(R.string.common_network_error); + } + }); + } + + // 预初始化友盟 SDK + UmengClient.preInit(application, AppConfig.isLogEnable()); + } + + /** + * 初始化第三方 SDK + */ + public static void initSdk(@NonNull Application application) { + // 友盟统计、登录、分享 SDK + UmengClient.init(application, AppConfig.isLogEnable()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/IMvpView.java b/app/src/main/java/com/hjq/demo/mvp/IMvpView.java deleted file mode 100644 index bc45af0f..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/IMvpView.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.hjq.demo.mvp; - -import android.content.Context; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : MVP 通用性接口 - */ -public interface IMvpView { - - /** - * 获取上下文对象 - */ - Context getContext(); - - /** - * 加载中 - */ - void onLoading(); - - /** - * 加载完成 - */ - void onComplete(); - - /** - * 用于请求的数据为空的状态 - */ - void onEmpty(); - - /** - * 用于请求数据出错 - */ - void onError(); -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/MvpActivity.java b/app/src/main/java/com/hjq/demo/mvp/MvpActivity.java deleted file mode 100644 index f4c95f73..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/MvpActivity.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.hjq.demo.mvp; - -import android.content.Context; - -import com.hjq.demo.common.MyActivity; -import com.hjq.demo.mvp.proxy.IMvpPresenterProxy; -import com.hjq.demo.mvp.proxy.MvpPresenterProxyImpl; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : MVP Activity 基类 - */ -public abstract class MvpActivity extends MyActivity implements IMvpView { - - private IMvpPresenterProxy mMvpProxy; - - @Override - public void initActivity() { - mMvpProxy = createPresenterProxy(); - mMvpProxy.bindPresenter(); - super.initActivity(); - } - - protected IMvpPresenterProxy createPresenterProxy() { - return new MvpPresenterProxyImpl(this); - } - - @Override - protected void onDestroy() { - mMvpProxy.unbindPresenter(); - super.onDestroy(); - } - - @Override - public Context getContext() { - return this; - } - - @Override - public void onLoading() { - showLoading(); - } - - @Override - public void onComplete() { - showComplete(); - } - - @Override - public void onEmpty() { - showEmpty(); - } - - @Override - public void onError() { - showError(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/MvpInject.java b/app/src/main/java/com/hjq/demo/mvp/MvpInject.java deleted file mode 100644 index 9f932431..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/MvpInject.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.hjq.demo.mvp; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/05/11 - * desc : Mvp 实例化注解 - */ -@Target(ElementType.FIELD) // 字段注解 -@Retention(RetentionPolicy.RUNTIME) // 运行时注解 -public @interface MvpInject {} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/MvpLazyFragment.java b/app/src/main/java/com/hjq/demo/mvp/MvpLazyFragment.java deleted file mode 100644 index 981be9ac..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/MvpLazyFragment.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.hjq.demo.mvp; - -import com.hjq.demo.common.MyLazyFragment; -import com.hjq.demo.mvp.proxy.IMvpPresenterProxy; -import com.hjq.demo.mvp.proxy.MvpPresenterProxyImpl; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : MVP 懒加载 Fragment 基类 - */ -public abstract class MvpLazyFragment extends MyLazyFragment implements IMvpView { - - private IMvpPresenterProxy mMvpProxy; - - @Override - protected void initFragment() { - mMvpProxy = createPresenterProxy(); - mMvpProxy.bindPresenter(); - super.initFragment(); - } - - protected IMvpPresenterProxy createPresenterProxy() { - return new MvpPresenterProxyImpl(this); - } - - @Override - public void onDestroy() { - if (mMvpProxy != null) { - mMvpProxy.unbindPresenter(); - } - super.onDestroy(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/MvpModel.java b/app/src/main/java/com/hjq/demo/mvp/MvpModel.java deleted file mode 100644 index 029eab4e..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/MvpModel.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.hjq.demo.mvp; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : MVP 模型基类 - */ -public abstract class MvpModel { - - private L mListener; - - public void setListener(L listener) { - mListener = listener; - } - - public L getListener() { - return mListener; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/MvpPresenter.java b/app/src/main/java/com/hjq/demo/mvp/MvpPresenter.java deleted file mode 100644 index 6060e42e..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/MvpPresenter.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.hjq.demo.mvp; - -import android.content.Context; - -import androidx.annotation.StringRes; - -import com.hjq.demo.mvp.proxy.IMvpModelProxy; -import com.hjq.demo.mvp.proxy.MvpModelProxyImpl; -import com.hjq.toast.ToastUtils; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : MVP 业务基类 - */ -public abstract class MvpPresenter implements InvocationHandler { - - /** View 层 */ - private V mView; - - /** 代理对象 */ - private V mProxyView; - - private IMvpModelProxy mMvpProxy; - - protected IMvpModelProxy createModelProxyImpl() { - return new MvpModelProxyImpl(this); - } - - @SuppressWarnings("unchecked") - public void attachView(V view) { - mView = view; - // 使用动态代理,解决 getView 方法可能为空的问题 - mProxyView = (V) Proxy.newProxyInstance(view.getClass().getClassLoader(), view.getClass().getInterfaces(), this); - // V 层解绑了 P 层,那么 getView 就为空,调用 V 层就会发生空指针异常 - // 如果在 P 层的每个子类中都进行 getView() != null 防空判断会导致开发成本非常高,并且容易出现遗漏 - mMvpProxy = createModelProxyImpl(); - mMvpProxy.bindModel(); - } - - /** - * 动态代理接口,每次调用了代理对象的方法最终也会回到到这里 - * - * {@link InvocationHandler} - */ - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // 如果当前还是绑定状态就执行 View 的方法,否则就不执行 - return isAttached() ? method.invoke(mView, args) : null; - } - - public void detachView() { - mView = null; - // 这里注意不能把代理对象置空 - // mProxyView = null; - mMvpProxy.unbindModel(); - } - - public boolean isAttached() { - return mProxyView != null && mView != null; - } - - public V getView() { - return mProxyView; - } - - /** - * 获取上下文 - */ - public Context getContext() { - return getView().getContext(); - } - - /** - * 显示吐司 - */ - public void toast(CharSequence text) { - ToastUtils.show(text); - } - - public void toast(@StringRes int id) { - ToastUtils.show(id); - } - - public void toast(Object object) { - ToastUtils.show(object); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/copy/CopyContract.java b/app/src/main/java/com/hjq/demo/mvp/copy/CopyContract.java deleted file mode 100644 index 885760b1..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyContract.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.hjq.demo.mvp.copy; - -import com.hjq.demo.mvp.IMvpView; - -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : 可进行拷贝的契约类 - */ -public final class CopyContract { - - public interface View extends IMvpView { - - void loginSuccess(List data); - - void loginError(String msg); - } - - public interface Presenter { - - void login(String account, String password); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/copy/CopyModel.java b/app/src/main/java/com/hjq/demo/mvp/copy/CopyModel.java deleted file mode 100644 index 91579a35..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyModel.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.hjq.demo.mvp.copy; - -import com.hjq.demo.mvp.MvpModel; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : 可进行拷贝的接口实现类 - */ -public final class CopyModel extends MvpModel { - - private String mAccount; - private String mPassword; - - public CopyModel() { - // 在这里做一些初始化操作 - } - - public void setAccount(String account) { - this.mAccount = account; - } - - public void setPassword(String password) { - this.mPassword = password; - } - - public void login() { - // 为了省事,这里直接回调成功 - if ("账户".equals(mAccount) && "密码".equals(mPassword)) { - getListener().onSucceed(null); - } else { - getListener().onFail("账户或密码不对哦"); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/copy/CopyMvpActivity.java b/app/src/main/java/com/hjq/demo/mvp/copy/CopyMvpActivity.java deleted file mode 100644 index 3e501af2..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyMvpActivity.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.hjq.demo.mvp.copy; - -import android.view.View; - -import com.hjq.demo.R; -import com.hjq.demo.mvp.MvpActivity; -import com.hjq.demo.mvp.MvpInject; - -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : 可进行拷贝的MVP Activity 类 - */ -public final class CopyMvpActivity extends MvpActivity implements CopyContract.View { - - @MvpInject - CopyPresenter mPresenter; - - @Override - protected int getLayoutId() { - return R.layout.activity_copy; - } - - @Override - protected void initView() { - - } - - @Override - protected void initData() { - - } - - public void onLogin(View view) { - // 登录操作 - mPresenter.login("账户", "密码"); - } - - /** - * {@link CopyContract.View} - */ - - @Override - public void loginError(String msg) { - toast(msg); - } - - @Override - public void loginSuccess(List data) { - toast("登录成功了"); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/copy/CopyOnListener.java b/app/src/main/java/com/hjq/demo/mvp/copy/CopyOnListener.java deleted file mode 100644 index 4cf44c51..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyOnListener.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.hjq.demo.mvp.copy; - -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : 可进行拷贝的监听器 - */ -public interface CopyOnListener { - - void onSucceed(List data); - - void onFail(String msg); -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/copy/CopyPresenter.java b/app/src/main/java/com/hjq/demo/mvp/copy/CopyPresenter.java deleted file mode 100644 index ef01ebd2..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyPresenter.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.hjq.demo.mvp.copy; - -import com.hjq.demo.mvp.MvpInject; -import com.hjq.demo.mvp.MvpPresenter; - -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2018/11/17 - * desc : 可进行拷贝的业务处理类 - */ -public final class CopyPresenter extends MvpPresenter - implements CopyContract.Presenter, CopyOnListener { - - @MvpInject - CopyModel mModel; - - /** - * {@link CopyContract.Presenter} - */ - - @Override - public void login(String account, String password) { - mModel.setAccount(account); - mModel.setPassword(password); - mModel.setListener(this); - mModel.login(); - } - - /** - * {@link CopyOnListener} - */ - - @Override - public void onSucceed(List data) { - getView().loginSuccess(data); - } - - @Override - public void onFail(String msg) { - getView().loginError(msg); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/proxy/IMvpModelProxy.java b/app/src/main/java/com/hjq/demo/mvp/proxy/IMvpModelProxy.java deleted file mode 100644 index 4b0d7c8c..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/proxy/IMvpModelProxy.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.hjq.demo.mvp.proxy; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/05/11 - * desc : 模型层代理接口 - */ -public interface IMvpModelProxy { - /** - * 绑定 Model - */ - void bindModel(); - - /** - * 解绑 Model - */ - void unbindModel(); -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/proxy/IMvpPresenterProxy.java b/app/src/main/java/com/hjq/demo/mvp/proxy/IMvpPresenterProxy.java deleted file mode 100644 index 5b893c80..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/proxy/IMvpPresenterProxy.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.hjq.demo.mvp.proxy; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/05/11 - * desc : 逻辑层代理接口 - */ -public interface IMvpPresenterProxy { - /** - * 绑定 Presenter - */ - void bindPresenter(); - - /** - * 解绑 Presenter - */ - void unbindPresenter(); -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/proxy/MvpModelProxyImpl.java b/app/src/main/java/com/hjq/demo/mvp/proxy/MvpModelProxyImpl.java deleted file mode 100644 index 5f530910..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/proxy/MvpModelProxyImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.hjq.demo.mvp.proxy; - -import com.hjq.demo.mvp.MvpInject; -import com.hjq.demo.mvp.MvpModel; -import com.hjq.demo.mvp.MvpPresenter; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/05/11 - * desc : 模型层代理实现 - */ -public class MvpModelProxyImpl implements IMvpModelProxy { - - private MvpPresenter mPresenter; - private List mModels; - - public MvpModelProxyImpl(MvpPresenter presenter) { - mPresenter = presenter; - } - - @SuppressWarnings("all") - @Override - public void bindModel() { - mModels = new ArrayList<>(); - - Field[] fields = mPresenter.getClass().getDeclaredFields(); - for (Field field : fields) { - MvpInject inject = field.getAnnotation(MvpInject.class); - if(inject != null){ - try { - Class clazz = (Class) field.getType(); - MvpModel model = clazz.newInstance(); - field.setAccessible(true); - field.set(mPresenter, model); - mModels.add(model); - } catch (IllegalAccessException | InstantiationException | ClassCastException e) { - e.printStackTrace(); - /** - * IllegalAccessException - * field.set:没有权限访问,请检查注解对象的修饰符 - */ - /** - * InstantiationException - * clazz.newInstance:检查一下注解的对象有没有空的构造函数 - */ - /** - * ClassCastException - * clazz.newInstance:检查一下自己注解的对象类型是否正确 - * field.set:检查一下自己的 M 层类型是否正确 - */ - throw new IllegalStateException("are you ok?"); - } - } - } - } - - @Override - public void unbindModel() { - mModels.clear(); - mModels = null; - mPresenter = null; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/mvp/proxy/MvpPresenterProxyImpl.java b/app/src/main/java/com/hjq/demo/mvp/proxy/MvpPresenterProxyImpl.java deleted file mode 100644 index 32b8e23e..00000000 --- a/app/src/main/java/com/hjq/demo/mvp/proxy/MvpPresenterProxyImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.hjq.demo.mvp.proxy; - -import com.hjq.demo.mvp.IMvpView; -import com.hjq.demo.mvp.MvpInject; -import com.hjq.demo.mvp.MvpPresenter; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/05/11 - * desc : 模型层代理实现 - */ -public class MvpPresenterProxyImpl implements IMvpPresenterProxy { - - private IMvpView mView; - private List mPresenters; - - public MvpPresenterProxyImpl(IMvpView view){ - mView = view; - } - - @SuppressWarnings("all") - @Override - public void bindPresenter() { - mPresenters = new ArrayList<>(); - - Field[] fields = mView.getClass().getDeclaredFields(); - for (Field field : fields) { - MvpInject inject = field.getAnnotation(MvpInject.class); - if(inject != null){ - try { - Class clazz = (Class) field.getType(); - MvpPresenter presenter = clazz.newInstance(); - field.setAccessible(true); - field.set(mView, presenter); - presenter.attachView(mView); - mPresenters.add(presenter); - } catch (IllegalAccessException | InstantiationException | ClassCastException e) { - e.printStackTrace(); - /** - * IllegalAccessException - * field.set:没有权限访问,请检查注解对象的修饰符 - */ - /** - * InstantiationException - * clazz.newInstance:检查一下注解的对象有没有空的构造函数 - */ - /** - * ClassCastException - * clazz.newInstance:检查一下自己注解的对象类型是否正确 - * field.set:检查一下自己的 V 层(Activity 或 Fragment)有没有实现 P 层对应的接口 - */ - throw new IllegalStateException("are you ok?"); - } - } - } - } - - @Override - public void unbindPresenter() { - // 一定要解绑 - for (MvpPresenter presenter : mPresenters) { - presenter.detachView(); - } - mPresenters.clear(); - mPresenters = null; - mView = null; - } -} diff --git a/app/src/main/java/com/hjq/demo/other/AppConfig.java b/app/src/main/java/com/hjq/demo/other/AppConfig.java index 2dd2ffc4..b6ba0b4f 100644 --- a/app/src/main/java/com/hjq/demo/other/AppConfig.java +++ b/app/src/main/java/com/hjq/demo/other/AppConfig.java @@ -11,12 +11,26 @@ 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,23 @@ public static int getVersionCode() { } /** - * 获取当前应用的渠道名 + * 获取服务器主机地址 + */ + public static String getHostUrl() { + return BuildConfig.HOST_URL; + } + + /** + * 获取 BuglyId + */ + public static String getBuglyId() { + return BuildConfig.BUGLY_ID; + } + + /** + * 获取 BuglyKey */ - public static String getProductFlavors() { - return BuildConfig.FLAVOR; + public static String getBuglyKey() { + return BuildConfig.BUGLY_KEY; } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ArrowDrawable.java b/app/src/main/java/com/hjq/demo/other/ArrowDrawable.java new file mode 100644 index 00000000..4bc2687c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/ArrowDrawable.java @@ -0,0 +1,357 @@ +package com.hjq.demo.other; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.view.View; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; + +/** + * author : 王浩 & Android 轮子哥 + * github : https://github.com/bingoogolapple/BGATransformersTip-Android + * time : 2019/08/19 + * desc : 带箭头背景的 Drawable + */ +@SuppressLint("RtlHardcoded") +public final class ArrowDrawable extends Drawable { + + private final Builder mBuilder; + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Path mPath; + + private ArrowDrawable(Builder builder) { + mBuilder = builder; + mPaint.setStyle(Paint.Style.FILL); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (mBuilder.mShadowSize > 0) { + mPaint.setMaskFilter(new BlurMaskFilter(mBuilder.mShadowSize, BlurMaskFilter.Blur.OUTER)); + mPaint.setColor(mBuilder.mShadowColor); + canvas.drawPath(mPath, mPaint); + } + mPaint.setMaskFilter(null); + mPaint.setColor(mBuilder.mBackgroundColor); + canvas.drawPath(mPath, mPaint); + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + mPaint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @SuppressWarnings("SuspiciousNameCombination") + @Override + protected void onBoundsChange(@NonNull Rect viewRect) { + if (mPath == null) { + mPath = new Path(); + } else { + mPath.reset(); + } + + RectF excludeShadowRectF = new RectF(viewRect); + excludeShadowRectF.inset(mBuilder.mShadowSize, mBuilder.mShadowSize); + + PointF centerPointF = new PointF(); + + // 判断箭头的位置 + switch (mBuilder.mArrowOrientation) { + case Gravity.LEFT: + excludeShadowRectF.left += mBuilder.mArrowHeight; + centerPointF.x = excludeShadowRectF.left; + break; + case Gravity.RIGHT: + excludeShadowRectF.right -= mBuilder.mArrowHeight; + centerPointF.x = excludeShadowRectF.right; + break; + case Gravity.TOP: + excludeShadowRectF.top += mBuilder.mArrowHeight; + centerPointF.y = excludeShadowRectF.top; + break; + case Gravity.BOTTOM: + excludeShadowRectF.bottom -= mBuilder.mArrowHeight; + centerPointF.y = excludeShadowRectF.bottom; + break; + default: + break; + } + + // 判断箭头的重心 + switch (mBuilder.mArrowGravity) { + case Gravity.LEFT: + centerPointF.x = excludeShadowRectF.left + mBuilder.mArrowHeight; + break; + case Gravity.CENTER_HORIZONTAL: + centerPointF.x = viewRect.width() / 2f; + break; + case Gravity.RIGHT: + centerPointF.x = excludeShadowRectF.right - mBuilder.mArrowHeight; + break; + case Gravity.TOP: + centerPointF.y = excludeShadowRectF.top + mBuilder.mArrowHeight; + break; + case Gravity.CENTER_VERTICAL: + centerPointF.y = viewRect.height() / 2f; + break; + case Gravity.BOTTOM: + centerPointF.y = excludeShadowRectF.bottom - mBuilder.mArrowHeight; + break; + default: + break; + } + + // 更新箭头偏移量 + centerPointF.x += mBuilder.mArrowOffsetX; + centerPointF.y += mBuilder.mArrowOffsetY; + + switch (mBuilder.mArrowGravity) { + case Gravity.LEFT: + case Gravity.RIGHT: + case Gravity.CENTER_HORIZONTAL: + centerPointF.x = Math.max(centerPointF.x, excludeShadowRectF.left + mBuilder.mRadius + mBuilder.mArrowHeight); + centerPointF.x = Math.min(centerPointF.x, excludeShadowRectF.right - mBuilder.mRadius - mBuilder.mArrowHeight); + break; + case Gravity.TOP: + case Gravity.BOTTOM: + case Gravity.CENTER_VERTICAL: + centerPointF.y = Math.max(centerPointF.y, excludeShadowRectF.top + mBuilder.mRadius + mBuilder.mArrowHeight); + centerPointF.y = Math.min(centerPointF.y, excludeShadowRectF.bottom - mBuilder.mRadius - mBuilder.mArrowHeight); + break; + default: + break; + } + + switch (mBuilder.mArrowOrientation) { + case Gravity.LEFT: + case Gravity.RIGHT: + centerPointF.x = Math.max(centerPointF.x, excludeShadowRectF.left); + centerPointF.x = Math.min(centerPointF.x, excludeShadowRectF.right); + break; + case Gravity.TOP: + case Gravity.BOTTOM: + centerPointF.y = Math.max(centerPointF.y, excludeShadowRectF.top); + centerPointF.y = Math.min(centerPointF.y, excludeShadowRectF.bottom); + break; + default: + break; + } + + // 箭头区域(其实是旋转了 90 度后的正方形区域) + Path arrowPath = new Path(); + arrowPath.moveTo(centerPointF.x - mBuilder.mArrowHeight, centerPointF.y); + arrowPath.lineTo(centerPointF.x, centerPointF.y - mBuilder.mArrowHeight); + arrowPath.lineTo(centerPointF.x + mBuilder.mArrowHeight, centerPointF.y); + arrowPath.lineTo(centerPointF.x, centerPointF.y + mBuilder.mArrowHeight); + arrowPath.close(); + + mPath.addRoundRect(excludeShadowRectF, mBuilder.mRadius, mBuilder.mRadius, Path.Direction.CW); + mPath.addPath(arrowPath); + + invalidateSelf(); + } + + public static final class Builder { + + /** 上下文对象 */ + @NonNull + private final Context mContext; + /** 箭头高度 */ + private int mArrowHeight; + /** 背景圆角大小 */ + private int mRadius; + /** 箭头方向 */ + private int mArrowOrientation; + /** 箭头重心 */ + private int mArrowGravity; + /** 箭头水平方向偏移 */ + private int mArrowOffsetX; + /** 箭头垂直方向偏移 */ + private int mArrowOffsetY; + /** 阴影大小 */ + private int mShadowSize; + /** 背景颜色 */ + private int mBackgroundColor; + /** 阴影颜色 */ + private int mShadowColor; + + public Builder(@NonNull Context context) { + mContext = context; + mBackgroundColor = ContextCompat.getColor(context, R.color.black); + mShadowColor = ContextCompat.getColor(context, R.color.black20); + mArrowHeight = (int) SmallestWidthAdaptation.dp2px(context, 6); + mRadius = (int) SmallestWidthAdaptation.dp2px(context, 4); + mShadowSize = 0; + mArrowOffsetX = 0; + mArrowOffsetY = 0; + mArrowOrientation = Gravity.NO_GRAVITY; + mArrowGravity = Gravity.NO_GRAVITY; + } + + /** + * 设置背景色 + */ + public Builder setBackgroundColor(@ColorInt int color) { + mBackgroundColor = color; + return this; + } + + /** + * 设置阴影色 + */ + public Builder setShadowColor(@ColorInt int color) { + mShadowColor = color; + return this; + } + + /** + * 设置箭头高度 + */ + public Builder setArrowHeight(int height) { + mArrowHeight = height; + return this; + } + + /** + * 设置浮窗圆角半径 + */ + public Builder setRadius(int radius) { + mRadius = radius; + return this; + } + + /** + * 设置箭头方向(左上右下) + */ + public Builder setArrowOrientation(int orientation) { + switch (orientation = Gravity.getAbsoluteGravity(orientation, mContext.getResources().getConfiguration().getLayoutDirection())) { + case Gravity.LEFT: + case Gravity.TOP: + case Gravity.RIGHT: + case Gravity.BOTTOM: + mArrowOrientation = orientation; + break; + default: + // 箭头只能在左上右下这四个位置 + throw new IllegalArgumentException("The arrow can only be in the four positions: left, top, right, and bottom"); + } + return this; + } + + /** + * 设置箭头布局重心 + */ + public Builder setArrowGravity(int gravity) { + gravity = Gravity.getAbsoluteGravity(gravity, mContext.getResources().getConfiguration().getLayoutDirection()); + if (gravity == Gravity.CENTER) { + switch (mArrowOrientation) { + case Gravity.LEFT: + case Gravity.RIGHT: + gravity = Gravity.CENTER_VERTICAL; + break; + case Gravity.TOP: + case Gravity.BOTTOM: + gravity = Gravity.CENTER_HORIZONTAL; + break; + default: + break; + } + } + switch (gravity) { + case Gravity.LEFT: + case Gravity.RIGHT: + if (mArrowOrientation == Gravity.LEFT || mArrowOrientation == Gravity.RIGHT) { + throw new IllegalArgumentException("The arrow direction cannot be the same as the arrow gravity"); + } + break; + case Gravity.TOP: + case Gravity.BOTTOM: + if (mArrowOrientation == Gravity.TOP || mArrowOrientation == Gravity.BOTTOM) { + throw new IllegalArgumentException("The arrow direction cannot be the same as the arrow gravity"); + } + break; + case Gravity.CENTER_VERTICAL: + case Gravity.CENTER_HORIZONTAL: + break; + default: + // 箭头只能在左上右下这四个位置 + throw new IllegalArgumentException("The arrow can only be in the four positions: left, top, right, and bottom"); + } + mArrowGravity = gravity; + return this; + } + + /** + * 设置箭头在 x 轴的偏移量 + */ + public Builder setArrowOffsetX(int offsetX) { + mArrowOffsetX = offsetX; + return this; + } + + /** + * 设置箭头在 y 轴的偏移量 + */ + public Builder setArrowOffsetY(int offsetY) { + mArrowOffsetY = offsetY; + return this; + } + + /** + * 设置阴影宽度 + */ + public Builder setShadowSize(int size) { + mShadowSize = size; + return this; + } + + /** + * 构建 Drawable + */ + public ArrowDrawable build() { + if (mArrowOrientation == Gravity.NO_GRAVITY || mArrowGravity == Gravity.NO_GRAVITY) { + // 必须要先设置箭头的方向及重心 + throw new IllegalArgumentException("You must set the direction and gravity of the arrow"); + } + return new ArrowDrawable(this); + } + + /** + * 应用到 View + */ + public void apply(View view) { + view.setBackground(build()); + if (mShadowSize > 0 || mArrowHeight > 0) { + if (view.getPaddingTop() == 0 && view.getBottom() == 0 && + view.getPaddingLeft() == 0 && view.getPaddingRight() == 0) { + view.setPadding(mShadowSize, mShadowSize + mArrowHeight, mShadowSize, mShadowSize); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/CrashHandler.java b/app/src/main/java/com/hjq/demo/other/CrashHandler.java new file mode 100644 index 00000000..d2c78b13 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/CrashHandler.java @@ -0,0 +1,73 @@ +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.common.CrashActivity; +import com.hjq.demo.ui.activity.common.RestartActivity; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/02/03 + * desc : Crash 处理类 + */ +public final class CrashHandler implements Thread.UncaughtExceptionHandler { + + /** Crash 文件名 */ + private static final String CRASH_FILE_NAME = "crash_config"; + /** Crash 时间记录 */ + private static final String KEY_CRASH_TIME = "key_crash_time"; + + /** + * 注册 Crash 监听 + */ + public static void register(Application application) { + Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(application)); + } + + private final Application mApplication; + private final Thread.UncaughtExceptionHandler mNextHandler; + + private CrashHandler(Application application) { + mApplication = application; + mNextHandler = Thread.getDefaultUncaughtExceptionHandler(); + if (mNextHandler != null && getClass().getName().equals(mNextHandler.getClass().getName())) { + // 请不要重复注册 Crash 监听 + throw new IllegalStateException("CrashHandler has already been registered"); + } + } + + @SuppressLint("ApplySharedPref") + @Override + public void uncaughtException(@NonNull Thread thread, @NonNull Throwable 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(); + + if (AppConfig.isDebug()) { + if (currentCrashTime - lastCrashTime > 1000 * 5) { + CrashActivity.start(mApplication, throwable); + } + } else { + // 致命异常标记:如果上次崩溃的时间距离当前崩溃小于 5 分钟,那么判定为致命异常 + if (currentCrashTime - lastCrashTime > 1000 * 60 * 5) { + // 如果不是致命的异常就自动重启应用 + 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.java b/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.java new file mode 100644 index 00000000..fa244f6c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.java @@ -0,0 +1,29 @@ +package com.hjq.demo.other; + +import com.hjq.core.tools.AndroidVersion; +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 8.0 被移除 + if (tag.length() <= MAX_TAG_LENGTH || AndroidVersion.isAndroid8()) { + return tag; + } + return tag.substring(0, MAX_TAG_LENGTH); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/EventBusManager.java b/app/src/main/java/com/hjq/demo/other/EventBusManager.java deleted file mode 100644 index f8f796cb..00000000 --- a/app/src/main/java/com/hjq/demo/other/EventBusManager.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.hjq.demo.other; - -import androidx.collection.ArrayMap; - -import com.hjq.demo.MyEventBusIndex; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.greenrobot.eventbus.meta.SubscriberInfoIndex; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/04/02 - * desc : EventBus 管理类 - */ -public final class EventBusManager { - - /** EventBus 索引类 */ - private static final SubscriberInfoIndex SUBSCRIBE_INDEX = new MyEventBusIndex(); - - /** 这个类是否需要注册 EventBus */ - private static final ArrayMap SUBSCRIBE_EVENT = new ArrayMap<>(); - - /** 不允许被外部实例化 */ - private EventBusManager() {} - - /** - * 初始化 EventBus - */ - public static void init() { - EventBus.builder() - // 使用 Apt 插件 - .ignoreGeneratedIndex(false) - // 添加索引类 - .addIndex(SUBSCRIBE_INDEX) - // 作为默认配置 - .installDefaultEventBus(); - } - - /** - * 注册 EventBus - */ - public static void register(Object subscriber) { - if (canSubscribeEvent(subscriber)) { - EventBus.getDefault().register(subscriber); - } - } - - /** - * 反注册 EventBus - */ - public static void unregister(Object subscriber) { - if (canSubscribeEvent(subscriber) && EventBus.getDefault().isRegistered(subscriber)) { - EventBus.getDefault().unregister(subscriber); - } - } - - /** - * 判断是否使用了 EventBus 注解 - * - * @param subscriber 被订阅的类 - */ - private static boolean canSubscribeEvent(Object subscriber) { - Class clazz = subscriber.getClass(); - // 这个 Class 类型有没有遍历过 - Boolean result = SUBSCRIBE_EVENT.get(clazz.getName()); - if (result != null) { - // 有的话直接返回结果 - return result; - } - - // 没有的话进行遍历 - while (clazz != null) { - // 如果索引集合中有这个 Class 类型的订阅信息,则这个类型的对象都需要注册 EventBus - if (SUBSCRIBE_INDEX.getSubscriberInfo(clazz) != null) { - // 这个类需要注册 EventBus - result = true; - clazz = null; - } else { - String clazzName = clazz.getName(); - // 跳过系统类(忽略 java. javax. android. androidx. 等开头包名的类) - if (clazzName.startsWith("java") || clazzName.startsWith("android")) { - clazz = null; - } else { - // 往上查找 - clazz = clazz.getSuperclass(); - } - } - } - // 这个类不需要注册 EventBus - if (result == null) { - result = false; - } - SUBSCRIBE_EVENT.put(subscriber.getClass().getName(), result); - return result; - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventBus(EventBusManager helper) { - // 占位,只为了能生成 MyEventBusIndex 索引类 - // 如果项目中已经有用到 @Subscribe 去注解方法,这个方法可以直接删除 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/IntentKey.java b/app/src/main/java/com/hjq/demo/other/IntentKey.java deleted file mode 100644 index 53a1ef2e..00000000 --- a/app/src/main/java/com/hjq/demo/other/IntentKey.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.hjq.demo.other; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/05/09 - * desc : Intent Key 管理 - */ -public final class IntentKey { - - // 常用相关 - - /** id */ - public static final String ID = "id"; - /** token */ - public static final String TOKEN = "token"; - /** 索引 */ - public static final String INDEX = "index"; - /** 位置 */ - public static final String POSITION = "position"; - /** 状态 */ - public static final String STATUS = "status"; - /** 类型 */ - public static final String TYPE = "type"; - /** 订单 */ - public static final String ORDER = "order"; - /** 余额 */ - public static final String BALANCE = "balance"; - /** 时间 */ - public static final String TIME = "time"; - /** 代码 */ - public static final String CODE = "code"; - /** URL */ - public static final String URL = "url"; - /** 路径 */ - public static final String PATH = "path"; - /** 数量 */ - public static final String AMOUNT = "amount"; - /** 总数 */ - public static final String COUNT = "count"; - /** 其他 */ - public static final String OTHER = "other"; - - // 个人信息 - - /** 姓名 */ - public static final String NAME = "name"; - /** 年龄 */ - public static final String AGE = "age"; - /** 性别 */ - public static final String SEX = "sex"; - /** 手机 */ - public static final String PHONE = "phone"; - /** 密码 */ - public static final String PASSWORD = "password"; - /** 会员 */ - public static final String VIP = "vip"; - /** 描述 */ - public static final String DESCRIBE = "describe"; - /** 备注 */ - public static final String REMARK = "remark"; - /** 星座 */ - public static final String CONSTELLATION = "constellation"; - - // 地方 - - /** 地址 */ - public static final String ADDRESS = "address"; - /** 省 */ - public static final String PROVINCE = "province"; - /** 市 */ - public static final String CITY = "city"; - /** 区 */ - public static final String AREA = "area"; - - // 文件类型相关 - - /** 文本 */ - public static final String TXT = "txt"; - /** 图片 */ - public static final String PICTURE = "picture"; - /** 音频 */ - public static final String VOICE = "voice"; - /** 视频 */ - public static final String VIDEO = "video"; - - // 支付相关 - - /** 余额支付 */ - public static final String BALANCE_PAY = "balance_pay"; - /** 微信支付 */ - public static final String WECHAT_PAY = "wechat_pay"; - /** 支付宝支付 */ - public static final String ALI_PAY = "ali_pay"; - /** 银联支付 */ - public static final String UNION_PAY = "union_pay"; -} diff --git a/app/src/main/java/com/hjq/demo/other/KeyboardWatcher.java b/app/src/main/java/com/hjq/demo/other/KeyboardWatcher.java deleted file mode 100644 index 0ad0dfdd..00000000 --- a/app/src/main/java/com/hjq/demo/other/KeyboardWatcher.java +++ /dev/null @@ -1,150 +0,0 @@ -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; -import android.view.Window; -import android.view.WindowManager; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/07/04 - * desc : 软键盘监听类 - */ -public final class KeyboardWatcher implements - ViewTreeObserver.OnGlobalLayoutListener, - Application.ActivityLifecycleCallbacks { - - private Activity mActivity; - private View mContentView; - private SoftKeyboardStateListener mListeners; - private boolean isSoftKeyboardOpened; - private int mStatusBarHeight; - - public static KeyboardWatcher with(Activity activity) { - return new KeyboardWatcher(activity); - } - - private KeyboardWatcher(Activity activity) { - mActivity = activity; - mContentView = activity.findViewById(Window.ID_ANDROID_CONTENT); - - mActivity.getApplication().registerActivityLifecycleCallbacks(this); - mContentView.getViewTreeObserver().addOnGlobalLayoutListener(this); - - // 获取 status_bar_height 资源的 ID - int resourceId = mActivity.getResources().getIdentifier("status_bar_height", "dimen", "android"); - if (resourceId > 0) { - //根据资源 ID 获取响应的尺寸值 - mStatusBarHeight = mActivity.getResources().getDimensionPixelSize(resourceId); - } - } - - /** - * {@link ViewTreeObserver.OnGlobalLayoutListener} - */ - - @Override - public void onGlobalLayout() { - final Rect r = new Rect(); - //r will be populated with the coordinates of your view that area still visible. - mContentView.getWindowVisibleDisplayFrame(r); - - final int heightDiff = mContentView.getRootView().getHeight() - (r.bottom - r.top); - if (!isSoftKeyboardOpened && heightDiff > mContentView.getRootView().getHeight() / 4) { - isSoftKeyboardOpened = true; - if ((mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != WindowManager.LayoutParams.FLAG_FULLSCREEN) { - if (mListeners != null) { - mListeners.onSoftKeyboardOpened(heightDiff - mStatusBarHeight); - } - } else { - if (mListeners != null) { - mListeners.onSoftKeyboardOpened(heightDiff); - } - } - - } else if (isSoftKeyboardOpened && heightDiff < mContentView.getRootView().getHeight() / 4) { - isSoftKeyboardOpened = false; - if (mListeners != null) { - mListeners.onSoftKeyboardClosed(); - } - } - } - - /** - * 设置软键盘弹出监听 - */ - public void setListener(SoftKeyboardStateListener listener) { - mListeners = listener; - } - - /** - * {@link Application.ActivityLifecycleCallbacks} - */ - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - - } - - @Override - public void onActivityStarted(Activity activity) { - - } - - @Override - public void onActivityResumed(Activity activity) { - - } - - @Override - public void onActivityPaused(Activity activity) { - - } - - @Override - public void onActivityStopped(Activity activity) { - - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - - } - - @Override - public void onActivityDestroyed(Activity activity) { - if (mActivity == activity) { - mActivity.getApplication().unregisterActivityLifecycleCallbacks(this); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - mContentView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - - mActivity = null; - mContentView = null; - mListeners = null; - } - } - - /** - * 软键盘状态监听器 - */ - public interface SoftKeyboardStateListener { - - /** - * 软键盘弹出了 - * @param keyboardHeight 软键盘高度 - */ - void onSoftKeyboardOpened(int keyboardHeight); - - /** - * 软键盘收起了 - */ - void onSoftKeyboardClosed(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/LinkClickableSpan.java b/app/src/main/java/com/hjq/demo/other/LinkClickableSpan.java new file mode 100644 index 00000000..fa6577cc --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/LinkClickableSpan.java @@ -0,0 +1,26 @@ +package com.hjq.demo.other; + +import android.text.style.ClickableSpan; +import android.view.View; +import androidx.annotation.NonNull; +import com.hjq.demo.ui.activity.common.BrowserActivity; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2023/06/24 + * desc : 点击跳转链接的 ClickableSpan + */ +public class LinkClickableSpan extends ClickableSpan { + + private final String mTargetUrl; + + public LinkClickableSpan(@NonNull String url) { + mTargetUrl = url; + } + + @Override + public void onClick(@NonNull View widget) { + BrowserActivity.start(widget.getContext(), mTargetUrl); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/MaterialHeader.java b/app/src/main/java/com/hjq/demo/other/MaterialHeader.java new file mode 100644 index 00000000..04670e27 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/MaterialHeader.java @@ -0,0 +1,306 @@ +package com.hjq.demo.other; + +import static android.view.View.MeasureSpec.getSize; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.widget.ImageView; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; +import com.scwang.smart.refresh.header.material.CircleImageView; +import com.scwang.smart.refresh.header.material.MaterialProgressDrawable; +import com.scwang.smart.refresh.layout.api.RefreshHeader; +import com.scwang.smart.refresh.layout.api.RefreshKernel; +import com.scwang.smart.refresh.layout.api.RefreshLayout; +import com.scwang.smart.refresh.layout.constant.RefreshState; +import com.scwang.smart.refresh.layout.constant.SpinnerStyle; +import com.scwang.smart.refresh.layout.simple.SimpleComponent; + +/** + * author : 树朾 & Android 轮子哥 + * github : https://github.com/scwang90/SmartRefreshLayout/tree/master/refresh-header-material + * time : 2021/02/28 + * desc : Material 风格的刷新球,参考 {@link com.scwang.smart.refresh.header.MaterialHeader} + */ +public final class MaterialHeader extends SimpleComponent implements RefreshHeader { + + /** 刷新球大样式 */ + public static final int BALL_STYLE_LARGE = 0; + /** 刷新球默认样式 */ + public static final int BALL_STYLE_DEFAULT = 1; + + private static final int CIRCLE_BG_LIGHT = Color.parseColor("#FAFAFA"); + private static final float MAX_PROGRESS_ANGLE = 0.8f; + + private boolean mFinished; + private int mCircleDiameter; + private final ImageView mCircleView; + private final MaterialProgressDrawable mProgressDrawable; + + private int mWaveHeight; + private int mHeadHeight; + private final Path mBezierPath; + private final Paint mBezierPaint; + private RefreshState mRefreshState; + private boolean mShowBezierWave = false; + private boolean mScrollableWhenRefreshing = true; + + public MaterialHeader(@NonNull Context context) { + this(context, null); + } + + public MaterialHeader(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs, 0); + + mSpinnerStyle = SpinnerStyle.MatchLayout; + setMinimumHeight((int) SmallestWidthAdaptation.dp2px(context, 100)); + + mProgressDrawable = new MaterialProgressDrawable(this); + mProgressDrawable.setColorSchemeColors( + Color.parseColor("#0099CC"), + Color.parseColor("#FF4444"), + Color.parseColor("#669900"), + Color.parseColor("#AA66CC"), + Color.parseColor("#FF8800")); + mCircleView = new CircleImageView(context, CIRCLE_BG_LIGHT); + mCircleView.setImageDrawable(mProgressDrawable); + mCircleView.setAlpha(0f); + addView(mCircleView); + + mCircleDiameter = (int) SmallestWidthAdaptation.dp2px(context, 40); + + mBezierPath = new Path(); + mBezierPaint = new Paint(); + mBezierPaint.setAntiAlias(true); + mBezierPaint.setStyle(Paint.Style.FILL); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialHeader); + mShowBezierWave = typedArray.getBoolean(R.styleable.MaterialHeader_srlShowBezierWave, mShowBezierWave); + mScrollableWhenRefreshing = typedArray.getBoolean(R.styleable.MaterialHeader_srlScrollableWhenRefreshing, mScrollableWhenRefreshing); + mBezierPaint.setColor(typedArray.getColor(R.styleable.MaterialHeader_srlPrimaryColor, Color.parseColor("#11BBFF"))); + if (typedArray.hasValue(R.styleable.MaterialHeader_srlShadowRadius)) { + int radius = typedArray.getDimensionPixelOffset(R.styleable.MaterialHeader_srlShadowRadius, 0); + int color = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, Color.parseColor("#000000")); + mBezierPaint.setShadowLayer(radius, 0, 0, color); + setLayerType(LAYER_TYPE_SOFTWARE, null); + } + + mShowBezierWave = typedArray.getBoolean(R.styleable.MaterialHeader_mhShowBezierWave, mShowBezierWave); + mScrollableWhenRefreshing = typedArray.getBoolean(R.styleable.MaterialHeader_mhScrollableWhenRefreshing, mScrollableWhenRefreshing); + if (typedArray.hasValue(R.styleable.MaterialHeader_mhPrimaryColor)) { + mBezierPaint.setColor(typedArray.getColor(R.styleable.MaterialHeader_mhPrimaryColor, Color.parseColor("#11BBFF"))); + } + if (typedArray.hasValue(R.styleable.MaterialHeader_mhShadowRadius)) { + int radius = typedArray.getDimensionPixelOffset(R.styleable.MaterialHeader_mhShadowRadius, 0); + int color = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, Color.parseColor("#000000")); + mBezierPaint.setShadowLayer(radius, 0, 0, color); + setLayerType(LAYER_TYPE_SOFTWARE, null); + } + + typedArray.recycle(); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.setMeasuredDimension(getSize(widthMeasureSpec), getSize(heightMeasureSpec)); + mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (getChildCount() == 0) { + return; + } + final int width = getMeasuredWidth(); + int circleWidth = mCircleView.getMeasuredWidth(); + int circleHeight = mCircleView.getMeasuredHeight(); + + if (isInEditMode() && mHeadHeight > 0) { + int circleTop = mHeadHeight - circleHeight / 2; + mCircleView.layout((width / 2 - circleWidth / 2), circleTop, + (width / 2 + circleWidth / 2), circleTop + circleHeight); + + mProgressDrawable.showArrow(true); + mProgressDrawable.setStartEndTrim(0f, MAX_PROGRESS_ANGLE); + mProgressDrawable.setArrowScale(1); + mCircleView.setAlpha(1f); + mCircleView.setVisibility(VISIBLE); + } else { + mCircleView.layout((width / 2 - circleWidth / 2), -circleHeight, (width / 2 + circleWidth / 2), 0); + } + } + + @Override + protected void dispatchDraw(@NonNull Canvas canvas) { + if (mShowBezierWave) { + // 重置画笔 + mBezierPath.reset(); + mBezierPath.lineTo(0, mHeadHeight); + // 绘制贝塞尔曲线 + mBezierPath.quadTo(getMeasuredWidth() / 2f, mHeadHeight + mWaveHeight * 1.9f, getMeasuredWidth(), mHeadHeight); + mBezierPath.lineTo(getMeasuredWidth(), 0); + canvas.drawPath(mBezierPath, mBezierPaint); + } + super.dispatchDraw(canvas); + } + + @Override + public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) { + if (!mShowBezierWave) { + kernel.requestDefaultTranslationContentFor(this, false); + } + if (isInEditMode()) { + mWaveHeight = mHeadHeight = height / 2; + } + } + + @Override + public void onMoving(boolean dragging, float percent, int offset, int height, int maxDragHeight) { + if (mRefreshState == RefreshState.Refreshing) { + return; + } + + if (mShowBezierWave) { + mHeadHeight = Math.min(offset, height); + mWaveHeight = Math.max(0, offset - height); + postInvalidate(); + } + + if (dragging || (!mProgressDrawable.isRunning() && !mFinished)) { + + if (mRefreshState != RefreshState.Refreshing) { + float originalDragPercent = 1f * offset / height; + + float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); + float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; + float extraOs = Math.abs(offset) - height; + float tensionSlingshotPercent = Math.max(0, Math.min(extraOs, (float) height * 2) + / (float) height); + float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( + (tensionSlingshotPercent / 4), 2)) * 2f; + float strokeStart = adjustedPercent * .8f; + mProgressDrawable.showArrow(true); + mProgressDrawable.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); + mProgressDrawable.setArrowScale(Math.min(1f, adjustedPercent)); + + float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; + mProgressDrawable.setProgressRotation(rotation); + } + + float targetY = offset / 2f + mCircleDiameter / 2f; + mCircleView.setTranslationY(Math.min(offset, targetY)); + mCircleView.setAlpha(Math.min(1f, 4f * offset / mCircleDiameter)); + } + } + + @Override + public void onReleased(@NonNull RefreshLayout layout, int height, int maxDragHeight) { + mProgressDrawable.start(); + } + + @Override + public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) { + mRefreshState = newState; + if (newState == RefreshState.PullDownToRefresh) { + mFinished = false; + mCircleView.setVisibility(VISIBLE); + mCircleView.setTranslationY(0); + mCircleView.setScaleX(1); + mCircleView.setScaleY(1); + } + } + + @Override + public int onFinish(@NonNull RefreshLayout layout, boolean success) { + mProgressDrawable.stop(); + mCircleView.animate().scaleX(0).scaleY(0); + mFinished = true; + return 0; + } + + /** + * 设置背景色 + */ + public MaterialHeader setProgressBackgroundResource(@ColorRes int id) { + setProgressBackgroundColor(ContextCompat.getColor(getContext(), id)); + return this; + } + + public MaterialHeader setProgressBackgroundColor(@ColorInt int color) { + mCircleView.setBackgroundColor(color); + return this; + } + + /** + * 设置 ColorScheme + * + * @param colors ColorScheme + */ + public MaterialHeader setColorSchemeColors(@ColorInt int... colors) { + mProgressDrawable.setColorSchemeColors(colors); + return this; + } + + /** + * 设置 ColorScheme + * + * @param ids ColorSchemeResources + */ + public MaterialHeader setColorSchemeResources(@ColorRes int... ids) { + int[] colors = new int[ids.length]; + for (int i = 0; i < ids.length; i++) { + colors[i] = ContextCompat.getColor(getContext(), ids[i]); + } + return setColorSchemeColors(colors); + } + + /** + * 设置刷新球样式 + * + * @param style 可传入:{@link #BALL_STYLE_LARGE,#BALL_STYLE_DEFAULT} + */ + public MaterialHeader setBallStyle(int style) { + if (style != BALL_STYLE_LARGE && style != BALL_STYLE_DEFAULT) { + return this; + } + if (style == BALL_STYLE_LARGE) { + mCircleDiameter = (int) SmallestWidthAdaptation.dp2px(getContext(), 56); + } else { + mCircleDiameter = (int) SmallestWidthAdaptation.dp2px(getContext(), 40); + } + // force the bounds of the progress circle inside the circle view to + // update by setting it to null before updating its size and then + // re-setting it + mCircleView.setImageDrawable(null); + mProgressDrawable.updateSizes(style); + mCircleView.setImageDrawable(mProgressDrawable); + return this; + } + + /** + * 是否显示贝塞尔图形 + */ + public MaterialHeader setShowBezierWave(boolean show) { + mShowBezierWave = show; + return this; + } + + /** + * 设置实在正在刷新的时候可以上下滚动 Header + */ + public MaterialHeader setScrollableWhenRefreshing(boolean scrollable) { + mScrollableWhenRefreshing = scrollable; + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/PhotoSpaceDecoration.java b/app/src/main/java/com/hjq/demo/other/PhotoSpaceDecoration.java deleted file mode 100644 index 51959d78..00000000 --- a/app/src/main/java/com/hjq/demo/other/PhotoSpaceDecoration.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.hjq.demo.other; - -import android.graphics.Canvas; -import android.graphics.Rect; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/07/25 - * desc : 图片选择列表分割线 - */ -public final class PhotoSpaceDecoration extends RecyclerView.ItemDecoration { - - private final int mSpace; - - public PhotoSpaceDecoration(int space) { - mSpace = space; - } - - @Override - public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.State state) {} - - @SuppressWarnings("all") - @Override - public void getItemOffsets(@NonNull Rect rect, @NonNull View view, RecyclerView recyclerView, @NonNull RecyclerView.State state) { - int position = recyclerView.getChildAdapterPosition(view); - int spanCount = ((GridLayoutManager) recyclerView.getLayoutManager()).getSpanCount(); - - // 每一行的最后一个才留出右边间隙 - if ((position + 1) % spanCount == 0) { - rect.right = mSpace; - } - - // 只有第一行才留出顶部间隙 - if (position < spanCount) { - rect.top = mSpace; - } - - rect.bottom = mSpace; - rect.left = mSpace; - } - - @Override - public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.State state) {} -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.java b/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.java new file mode 100644 index 00000000..084b0bb8 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.java @@ -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.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.hjq.smallest.width.SmallestWidthAdaptation; +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; + +/** + * 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 = Color.parseColor("#EEEEEE"); + private int[] mAnimatingColor = { + Color.parseColor("#30B399"), + Color.parseColor("#FF4600"), + Color.parseColor("#142DCC")}; + + private final float mCircleSpacing; + + private long mStartTime = 0; + private boolean mStarted = false; + + private final float mTextWidth; + + public SmartBallPulseFooter(@NonNull Context context) { + this(context, null); + } + + public SmartBallPulseFooter(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs, 0); + + setMinimumHeight((int) SmallestWidthAdaptation.dp2px(context, 60)); + + mPaint = new Paint(); + mPaint.setColor(Color.WHITE); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setAntiAlias(true); + + mSpinnerStyle = SpinnerStyle.Translate; + + mCircleSpacing = SmallestWidthAdaptation.dp2px(context, 2); + mPaint.setTextSize(SmallestWidthAdaptation.sp2px(context, 14)); + mTextWidth = mPaint.measureText(getContext().getString(R.string.common_no_more_data)); + } + + @Override + protected void dispatchDraw(@NonNull Canvas canvas) { + final int width = getWidth(); + final int height = getHeight(); + if (mNoMoreData) { + mPaint.setColor(Color.parseColor("#898989")); + 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(Color.parseColor("#99FFFFFF"), 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/StatusManager.java b/app/src/main/java/com/hjq/demo/other/StatusManager.java deleted file mode 100644 index cb5b4d37..00000000 --- a/app/src/main/java/com/hjq/demo/other/StatusManager.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.hjq.demo.other; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.DrawableRes; -import androidx.annotation.RequiresPermission; -import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; - -import com.hjq.base.BaseDialog; -import com.hjq.demo.R; -import com.hjq.demo.ui.dialog.WaitDialog; -import com.hjq.widget.layout.HintLayout; - -import static android.Manifest.permission.ACCESS_NETWORK_STATE; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/04/18 - * desc : 界面状态管理类 - */ -public final class StatusManager { - - /** 加载对话框 */ - private BaseDialog mDialog; - - /** 提示布局 */ - private HintLayout mHintLayout; - - /** - * 显示加载中 - */ - public void showLoading(FragmentActivity activity) { - showLoading(activity, activity.getString(R.string.common_loading)); - } - - public void showLoading(FragmentActivity activity, CharSequence text) { - if (activity == null || activity.isFinishing()) { - return; - } - - if (mDialog == null) { - mDialog = new WaitDialog.Builder(activity) - .setMessage(text) - .create(); - } - - if (!mDialog.isShowing()) { - mDialog.show(); - } - } - - /** - * 显示加载完成 - */ - public void showComplete() { - - if (mDialog != null && mDialog.isShowing()) { - mDialog.dismiss(); - } - - if (mHintLayout != null && mHintLayout.isShow()) { - mHintLayout.hide(); - } - } - - /** - * 显示空提示 - */ - public void showEmpty(View view) { - showLayout(view, R.drawable.icon_hint_empty, R.string.hint_layout_no_data); - } - - /** - * 显示错误提示 - */ - public void showError(View view) { - // 判断当前网络是否可用 - if (isNetworkAvailable(view.getContext())) { - showLayout(view, R.drawable.icon_hint_request, R.string.hint_layout_error_request); - } else { - showLayout(view, R.drawable.icon_hint_nerwork, R.string.hint_layout_error_network); - } - } - - /** - * 显示自定义提示 - */ - public void showLayout(View view, @DrawableRes int drawableId, @StringRes int stringId) { - showLayout(view, ContextCompat.getDrawable(view.getContext(), drawableId), view.getResources().getString(stringId)); - } - - public void showLayout(View view, Drawable drawable, CharSequence hint) { - if (mDialog != null && mDialog.isShowing()) { - mDialog.dismiss(); - } - - if (mHintLayout == null) { - - if (view instanceof HintLayout) { - mHintLayout = (HintLayout) view; - } else if (view instanceof ViewGroup) { - mHintLayout = findHintLayout((ViewGroup) view); - } - - if (mHintLayout == null) { - // 必须在布局中定义一个 HintLayout - throw new IllegalStateException("You didn't add this HintLayout to your layout"); - } - } - mHintLayout.show(); - mHintLayout.setIcon(drawable); - mHintLayout.setHint(hint); - } - - /** - * 智能获取布局中的 HintLayout 对象 - */ - private static HintLayout findHintLayout(ViewGroup group) { - for (int i = 0; i < group.getChildCount(); i++) { - View view = group.getChildAt(i); - if ((view instanceof HintLayout)) { - return (HintLayout) view; - } else if (view instanceof ViewGroup) { - HintLayout layout = findHintLayout((ViewGroup) view); - if (layout != null) { - return layout; - } - } - } - return null; - } - - /** - * 判断网络功能是否可用 - */ - @RequiresPermission(ACCESS_NETWORK_STATE) - private static boolean isNetworkAvailable(Context context){ - NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); - return (info != null && info.isConnected()); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/TitleBarStyle.java b/app/src/main/java/com/hjq/demo/other/TitleBarStyle.java new file mode 100644 index 00000000..e791a3d3 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/TitleBarStyle.java @@ -0,0 +1,111 @@ +package com.hjq.demo.other; + +import android.content.Context; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.core.content.ContextCompat; +import com.hjq.bar.style.LightBarStyle; +import com.hjq.custom.widget.view.PressAlphaTextView; +import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2021/02/27 + * desc : 标题栏初始器 + */ +public final class TitleBarStyle extends LightBarStyle { + + @Override + public TextView newTitleView(@NonNull Context context) { + return new AppCompatTextView(context); + } + + @Override + public TextView newLeftView(@NonNull Context context) { + return new PressAlphaTextView(context); + } + + @Override + public TextView newRightView(@NonNull Context context) { + return new PressAlphaTextView(context); + } + + @Override + public Drawable getTitleBarBackground(@NonNull Context context) { + return new ColorDrawable(ContextCompat.getColor(context, R.color.common_primary_color)); + } + + @Override + public Drawable getBackButtonDrawable(@NonNull Context context) { + if (context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + return ContextCompat.getDrawable(context, R.drawable.arrows_right_ic); + } + return ContextCompat.getDrawable(context, R.drawable.arrows_left_ic); + } + + @Override + public Drawable getLeftTitleBackground(@NonNull Context context) { + return null; + } + + @Override + public Drawable getRightTitleBackground(@NonNull Context context) { + return null; + } + + @Override + public int getTitleHorizontalPadding(@NonNull Context context) { + return 0; + } + + @Override + public int getLeftHorizontalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 10); + } + + @Override + public int getRightHorizontalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 10); + } + + @Override + public int getChildVerticalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 14); + } + + @Override + public float getTitleSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 15); + } + + @Override + public float getLeftTitleSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 13); + } + + @Override + public float getRightTitleSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 13); + } + + @Override + public int getTitleIconPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 2); + } + + @Override + public int getLeftIconPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 2); + } + + @Override + public int getRightIconPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 2); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ToastInterceptor.java b/app/src/main/java/com/hjq/demo/other/ToastInterceptor.java new file mode 100644 index 00000000..dc6da580 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/ToastInterceptor.java @@ -0,0 +1,24 @@ +package com.hjq.demo.other; + +import com.hjq.toast.ToastLogInterceptor; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/11/04 + * desc : 自定义 Toast 拦截器(用于追踪 Toast 调用的位置) + */ +public final class ToastInterceptor extends ToastLogInterceptor { + + @Override + protected boolean isLogEnable() { + return AppConfig.isLogEnable(); + } + + @Override + protected void printLog(String msg) { + Timber.tag("Toaster"); + Timber.i(msg); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ToastStyle.java b/app/src/main/java/com/hjq/demo/other/ToastStyle.java new file mode 100644 index 00000000..a37bfcae --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/ToastStyle.java @@ -0,0 +1,43 @@ +package com.hjq.demo.other; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import androidx.annotation.NonNull; +import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; +import com.hjq.toast.style.BlackToastStyle; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2021/02/27 + * desc : Toast 样式配置 + */ +public final class ToastStyle extends BlackToastStyle { + + @Override + protected Drawable getBackgroundDrawable(@NonNull Context context) { + GradientDrawable drawable = new GradientDrawable(); + // 设置颜色 + drawable.setColor(0X88000000); + // 设置圆角 + drawable.setCornerRadius((int) context.getResources().getDimension(R.dimen.button_circle_size)); + return drawable; + } + + @Override + protected float getTextSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 14); + } + + @Override + protected int getHorizontalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.sp2px(context, 24); + } + + @Override + protected int getVerticalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.sp2px(context, 16); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/permission/PermissionConverter.java b/app/src/main/java/com/hjq/demo/permission/PermissionConverter.java new file mode 100644 index 00000000..41c9a8f6 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/permission/PermissionConverter.java @@ -0,0 +1,329 @@ +package com.hjq.demo.permission; + +import android.content.Context; +import android.text.TextUtils; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.permissions.permission.PermissionGroups; +import com.hjq.permissions.permission.PermissionNames; +import com.hjq.permissions.permission.base.IPermission; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/XXPermissions + * time : 2025/05/30 + * desc : 权限转换器(根据权限获取对应的名称和说明) + */ +public final class PermissionConverter { + + /** 权限名称映射(为了适配多语种,这里存储的是 StringId,而不是 String) */ + private static final Map PERMISSION_NAME_MAP = new HashMap<>(); + + /** 权限描述映射(为了适配多语种,这里存储的是 StringId,而不是 String) */ + private static final Map PERMISSION_DESCRIPTION_MAP = new HashMap<>(); + + static { + PERMISSION_NAME_MAP.put(PermissionGroups.STORAGE, R.string.common_permission_storage); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_storage, R.string.common_permission_storage_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.IMAGE_AND_VIDEO_MEDIA, R.string.common_permission_image_and_video); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_image_and_video, R.string.common_permission_image_and_video_description); + + PERMISSION_NAME_MAP.put(PermissionNames.READ_MEDIA_AUDIO, R.string.common_permission_music_and_audio); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_music_and_audio, R.string.common_permission_music_and_audio_description); + + PERMISSION_NAME_MAP.put(PermissionNames.CAMERA, R.string.common_permission_camera); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_camera, R.string.common_permission_camera_description); + + PERMISSION_NAME_MAP.put(PermissionNames.RECORD_AUDIO, R.string.common_permission_microphone); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_microphone, R.string.common_permission_microphone_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.NEARBY_DEVICES, R.string.common_permission_nearby_devices); + // 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组 + if (AndroidVersion.isAndroid13()) { + // 需要填充文案:蓝牙权限描述 + WIFI 权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_nearby_devices, R.string.common_permission_nearby_devices_description); + } else { + // 需要填充文案:蓝牙权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_nearby_devices, R.string.common_permission_nearby_devices_description); + } + + PERMISSION_NAME_MAP.put(PermissionGroups.LOCATION, R.string.common_permission_location); + // 注意:在 Android 12 的时候,蓝牙相关的权限已经归到附近设备的权限组了,但是在 Android 12 之前,蓝牙相关的权限归属定位权限组 + // 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组 + if (AndroidVersion.isAndroid13()) { + // 需要填充文案:前台定位权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); + } else if (AndroidVersion.isAndroid12()) { + // 需要填充文案:前台定位权限描述 + WIFI 权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); + } else { + // 需要填充文案:前台定位权限描述 + 蓝牙权限描述 + WIFI 权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); + } + + // 后台定位权限虽然属于定位权限组,但是只要是属于后台权限,都有独属于自己的一套规则 + PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_BACKGROUND_LOCATION, R.string.common_permission_location_background); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location_background, R.string.common_permission_location_background_description); + + int sensorsPermissionNameStringId; + if (AndroidVersion.isAndroid16()) { + sensorsPermissionNameStringId = R.string.common_permission_health_data; + } else { + sensorsPermissionNameStringId = R.string.common_permission_body_sensors; + } + PERMISSION_NAME_MAP.put(PermissionGroups.SENSORS, sensorsPermissionNameStringId); + PERMISSION_DESCRIPTION_MAP.put(sensorsPermissionNameStringId, R.string.common_permission_body_sensors_description); + + // 后台传感器权限虽然属于传感器权限组,但是只要是属于后台权限,都有独属于自己的一套规则 + int bodySensorsBackgroundPermissionNameStringId; + if (AndroidVersion.isAndroid16()) { + bodySensorsBackgroundPermissionNameStringId = R.string.common_permission_health_data_background; + } else { + bodySensorsBackgroundPermissionNameStringId = R.string.common_permission_body_sensors_background; + } + PERMISSION_NAME_MAP.put(PermissionNames.BODY_SENSORS_BACKGROUND, bodySensorsBackgroundPermissionNameStringId); + PERMISSION_DESCRIPTION_MAP.put(bodySensorsBackgroundPermissionNameStringId, R.string.common_permission_body_sensors_background_description); + + // Android 16 这个版本开始,传感器权限被进行了精细化拆分,拆分成了无数个健康权限 + PERMISSION_NAME_MAP.put(PermissionGroups.HEALTH, R.string.common_permission_health_data); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data, R.string.common_permission_health_data_description); + + PERMISSION_NAME_MAP.put(PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND, R.string.common_permission_health_data_background); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data_background, R.string.common_permission_health_data_background_description); + + PERMISSION_NAME_MAP.put(PermissionNames.READ_HEALTH_DATA_HISTORY, R.string.common_permission_health_data_past); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data_past, R.string.common_permission_health_data_past_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.CALL_LOG, R.string.common_permission_call_logs); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_call_logs, R.string.common_permission_call_logs_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.PHONE, R.string.common_permission_phone); + // 注意:在 Android 9.0 的时候,读写通话记录权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组 + if (AndroidVersion.isAndroid9()) { + // 需要填充文案:电话权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_phone, R.string.common_permission_phone_description); + } else { + // 需要填充文案:电话权限描述 + 通话记录权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_phone, R.string.common_permission_phone_description); + } + + PERMISSION_NAME_MAP.put(PermissionGroups.CONTACTS, R.string.common_permission_contacts); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_contacts, R.string.common_permission_contacts_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.CALENDAR, R.string.common_permission_calendar); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_calendar, R.string.common_permission_calendar_description); + + // 注意:在 Android 10 的版本,这个权限的名称为《健身运动权限》,但是到了 Android 11 的时候,这个权限的名称被修改成了《身体活动权限》 + // 没错就改了一下权限的叫法,其他的一切没有变,Google 产品经理真的是闲的蛋疼,但是吐槽归吐槽,框架也要灵活应对一下,避免小白用户跳转到设置页找不到对应的选项 + int activityRecognitionPermissionNameStringId = AndroidVersion.isAndroid11() ? R.string.common_permission_activity_recognition_api30 : R.string.common_permission_activity_recognition_api29; + PERMISSION_NAME_MAP.put(PermissionNames.ACTIVITY_RECOGNITION, activityRecognitionPermissionNameStringId); + PERMISSION_DESCRIPTION_MAP.put(activityRecognitionPermissionNameStringId, R.string.common_permission_activity_recognition_description); + + PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_MEDIA_LOCATION, R.string.common_permission_access_media_location_information); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_access_media_location_information, R.string.common_permission_access_media_location_information_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.SMS, R.string.common_permission_sms); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_sms, R.string.common_permission_sms_description); + + PERMISSION_NAME_MAP.put(PermissionNames.GET_INSTALLED_APPS, R.string.common_permission_get_installed_apps); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_get_installed_apps, R.string.common_permission_get_installed_apps_description); + + PERMISSION_NAME_MAP.put(PermissionNames.MANAGE_EXTERNAL_STORAGE, R.string.common_permission_all_file_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_all_file_access, R.string.common_permission_all_file_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.REQUEST_INSTALL_PACKAGES, R.string.common_permission_install_unknown_apps); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_install_unknown_apps, R.string.common_permission_install_unknown_apps_description); + + PERMISSION_NAME_MAP.put(PermissionNames.SYSTEM_ALERT_WINDOW, R.string.common_permission_display_over_other_apps); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_display_over_other_apps, R.string.common_permission_display_over_other_apps_description); + + PERMISSION_NAME_MAP.put(PermissionNames.WRITE_SETTINGS, R.string.common_permission_modify_system_settings); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_modify_system_settings, R.string.common_permission_modify_system_settings_description); + + PERMISSION_NAME_MAP.put(PermissionNames.NOTIFICATION_SERVICE, R.string.common_permission_allow_notifications); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_allow_notifications, R.string.common_permission_allow_notifications_description); + + PERMISSION_NAME_MAP.put(PermissionNames.POST_NOTIFICATIONS, R.string.common_permission_post_notifications); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_post_notifications, R.string.common_permission_post_notifications_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_NOTIFICATION_LISTENER_SERVICE, R.string.common_permission_allow_notifications_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_allow_notifications_access, R.string.common_permission_allow_notifications_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.PACKAGE_USAGE_STATS, R.string.common_permission_apps_with_usage_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_apps_with_usage_access, R.string.common_permission_apps_with_usage_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.SCHEDULE_EXACT_ALARM, R.string.common_permission_alarms_reminders); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_alarms_reminders, R.string.common_permission_alarms_reminders_description); + + PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_NOTIFICATION_POLICY, R.string.common_permission_do_not_disturb_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_do_not_disturb_access, R.string.common_permission_do_not_disturb_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, R.string.common_permission_ignore_battery_optimize); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_ignore_battery_optimize, R.string.common_permission_ignore_battery_optimize_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_VPN_SERVICE, R.string.common_permission_vpn); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_vpn, R.string.common_permission_vpn_description); + + PERMISSION_NAME_MAP.put(PermissionNames.PICTURE_IN_PICTURE, R.string.common_permission_picture_in_picture); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_picture_in_picture, R.string.common_permission_picture_in_picture_description); + + PERMISSION_NAME_MAP.put(PermissionNames.USE_FULL_SCREEN_INTENT, R.string.common_permission_full_screen_notifications); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_full_screen_notifications, R.string.common_permission_full_screen_notifications_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_DEVICE_ADMIN, R.string.common_permission_device_admin); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_device_admin, R.string.common_permission_device_admin_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_ACCESSIBILITY_SERVICE, R.string.common_permission_accessibility_service); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_accessibility_service, R.string.common_permission_accessibility_service_description); + + PERMISSION_NAME_MAP.put(PermissionNames.MANAGE_MEDIA, R.string.common_permission_manage_media); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_manage_media, R.string.common_permission_manage_media_description); + } + + /** + * 通过权限获得名称 + */ + @NonNull + public static String getNickNamesByPermissions(@NonNull Context context, @NonNull List permissions) { + List permissionNameList = getNickNameListByPermissions(context, permissions, true); + + StringBuilder builder = new StringBuilder(); + for (String permissionName : permissionNameList) { + if (TextUtils.isEmpty(permissionName)) { + continue; + } + if (builder.length() == 0) { + builder.append(permissionName); + } else { + builder.append(context.getString(R.string.common_permission_comma)) + .append(permissionName); + } + } + if (builder.length() == 0) { + // 如果没有获得到任何信息,则返回一个默认的文本 + return context.getString(R.string.common_permission_unknown); + } + return builder.toString(); + } + + @NonNull + public static List getNickNameListByPermissions(@NonNull Context context, @NonNull List permissions, boolean filterHighVersionPermissions) { + List permissionNickNameList = new ArrayList<>(); + for (IPermission permission : permissions) { + // 如果当前设置了过滤高版本权限,并且这个权限是高版本系统才出现的权限,则不继续往下执行 + // 避免出现在低版本上面执行拒绝权限后,连带高版本的名称也一起显示出来,但是在低版本上面是没有这个权限的 + if (filterHighVersionPermissions && permission.getFromAndroidVersion(context) > AndroidVersion.getSdkVersion()) { + continue; + } + String permissionName = getNickNameByPermission(context, permission); + if (TextUtils.isEmpty(permissionName)) { + continue; + } + if (permissionNickNameList.contains(permissionName)) { + continue; + } + permissionNickNameList.add(permissionName); + } + return permissionNickNameList; + } + + public static String getNickNameByPermission(@NonNull Context context, @NonNull IPermission permission) { + Integer permissionNameStringId = getPermissionNickNameStringId(context, permission); + if (permissionNameStringId == null || permissionNameStringId == 0) { + return ""; + } + return context.getString(permissionNameStringId); + } + + /** + * 通过权限获得描述 + */ + @NonNull + public static String getDescriptionsByPermissions(@NonNull Context context, @NonNull List permissions) { + List descriptionList = getDescriptionListByPermissions(context, permissions); + + StringBuilder builder = new StringBuilder(); + for (String description : descriptionList) { + if (TextUtils.isEmpty(description)) { + continue; + } + if (builder.length() == 0) { + builder.append(description); + } else { + builder.append("\n") + .append(description); + } + } + return builder.toString(); + } + + @NonNull + public static List getDescriptionListByPermissions(@NonNull Context context, @NonNull List permissions) { + List descriptionList = new ArrayList<>(); + for (IPermission permission : permissions) { + String permissionDescription = getDescriptionByPermission(context, permission); + if (TextUtils.isEmpty(permissionDescription)) { + continue; + } + if (descriptionList.contains(permissionDescription)) { + continue; + } + descriptionList.add(permissionDescription); + } + return descriptionList; + } + + /** + * 通过权限获得描述 + */ + @NonNull + public static String getDescriptionByPermission(@NonNull Context context, @NonNull IPermission permission) { + Integer permissionNameStringId = getPermissionNickNameStringId(context, permission); + if (permissionNameStringId == null || permissionNameStringId == 0) { + return ""; + } + String permissionNickName = context.getString(permissionNameStringId); + Integer permissionDescriptionStringId = getPermissionDescriptionStringId(permissionNameStringId); + String permissionDescription; + if (permissionDescriptionStringId == null || permissionDescriptionStringId == 0) { + permissionDescription = ""; + } else { + permissionDescription = context.getString(permissionDescriptionStringId); + } + return permissionNickName + context.getString(R.string.common_permission_colon) + permissionDescription; + } + + /** + * 获取这个权限对应的别名 StringId + */ + @Nullable + public static Integer getPermissionNickNameStringId(@NonNull Context context, @NonNull IPermission permission) { + String permissionName = permission.getPermissionName(); + String permissionGroup = permission.getPermissionGroup(context); + Integer permissionNameStringId = PERMISSION_NAME_MAP.get(permissionName); + if (permissionNameStringId != null && permissionNameStringId > 0) { + return permissionNameStringId; + } + Integer permissionGroupStringId = PERMISSION_NAME_MAP.get(permissionGroup); + if (permissionGroupStringId != null && permissionGroupStringId > 0) { + return permissionGroupStringId; + } + return permissionNameStringId; + } + + /** + * 获取这个权限对应的描述 StringId + */ + @Nullable + public static Integer getPermissionDescriptionStringId(@IdRes int permissionNickNameStringId) { + return PERMISSION_DESCRIPTION_MAP.get(permissionNickNameStringId); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/permission/PermissionDescription.java b/app/src/main/java/com/hjq/demo/permission/PermissionDescription.java new file mode 100644 index 00000000..abaf5681 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/permission/PermissionDescription.java @@ -0,0 +1,233 @@ +package com.hjq.demo.permission; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.PopupWindow; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.demo.R; +import com.hjq.demo.ui.dialog.common.MessageDialog; +import com.hjq.demo.ui.popup.PermissionDescriptionPopup; +import com.hjq.permissions.OnPermissionDescription; +import com.hjq.permissions.permission.PermissionPageType; +import com.hjq.permissions.permission.base.IPermission; +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/XXPermissions + * time : 2025/05/30 + * desc : 权限请求描述实现 + */ +public final class PermissionDescription implements OnPermissionDescription { + + /** 消息处理 Handler 对象 */ + public static final Handler HANDLER = new Handler(Looper.getMainLooper()); + + /** 权限请求描述弹窗显示类型:Dialog */ + private static final int DESCRIPTION_WINDOW_TYPE_DIALOG = 0; + /** 权限请求描述弹窗显示类型:PopupWindow */ + private static final int DESCRIPTION_WINDOW_TYPE_POPUP = 1; + + /** 权限请求描述弹窗显示类型 */ + private int mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; + + /** 消息 Token */ + @NonNull + private final Object mHandlerToken = new Object(); + + /** 权限申请说明弹窗 */ + @Nullable + private PopupWindow mPermissionPopupWindow; + + /** 权限申请说明对话框 */ + @Nullable + private Dialog mPermissionDialog; + + @Override + public void askWhetherRequestPermission(@NonNull Activity activity, + @NonNull List requestList, + @NonNull Runnable continueRequestRunnable, + @NonNull Runnable breakRequestRunnable) { + // 以下情况使用 Dialog 来展示权限说明弹窗,否则使用 PopupWindow 来展示权限说明弹窗 + // 1. 如果请求的权限显示的系统界面是不透明的 Activity + // 2. 如果当前 Activity 的屏幕是横屏状态的话,要求物理尺寸要够大,否则显示的顶部弹窗会被遮挡住, + // 设备的物理屏幕尺寸还小于 8.5 寸(目前大多数小屏平板大多数集中在 8、8.7、8.8、10 寸), + // 实测 8 寸的平板获取到的物理尺寸到只有 7.958788793906728,所以这里的代码判断基本上是针对 8.5 寸及以上的平板做优化。 + if (isActivityLandscape(activity) && getPhysicalScreenSize(activity) < 8.5) { + mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; + } else { + mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_POPUP; + for (IPermission permission : requestList) { + if (permission.getPermissionPageType(activity) == PermissionPageType.OPAQUE_ACTIVITY) { + mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; + } + } + } + + if (mDescriptionWindowType == DESCRIPTION_WINDOW_TYPE_POPUP) { + continueRequestRunnable.run(); + return; + } + + showDialog(activity, activity.getString(R.string.common_permission_description_title), + generatePermissionDescription(activity, requestList), + activity.getString(R.string.common_permission_confirm), dialog -> { + dialog.dismiss(); + continueRequestRunnable.run(); + }); + } + + @Override + public void onRequestPermissionStart(@NonNull Activity activity, @NonNull List requestList) { + if (mDescriptionWindowType != DESCRIPTION_WINDOW_TYPE_POPUP) { + return; + } + + Runnable showPopupRunnable = () -> showPopupWindow(activity, generatePermissionDescription(activity, requestList)); + // 这里解释一下为什么要延迟一段时间再显示 PopupWindow,这是因为系统没有开放任何 API 给外层直接获取权限是否永久拒绝 + // 目前只有申请过了权限才能通过 shouldShowRequestPermissionRationale 判断是不是永久拒绝,如果此前没有申请过权限,则无法判断 + // 针对这个问题能想到最佳的解决方案是:先申请权限,如果极短的时间内,权限申请没有结束,则证明权限之前没有被用户勾选了《不再询问》 + // 此时系统的权限弹窗正在显示给用户,这个时候再去显示应用的 PopupWindow 权限说明弹窗给用户看,所以这个 PopupWindow 是在发起权限申请后才显示的 + // 这样做是为了避免 PopupWindow 显示了又马上消失,这样就不会出现 PopupWindow 一闪而过的效果,提升用户的视觉体验 + // 最后补充一点:350 毫秒只是一个经验值,经过测试可覆盖大部分机型,具体可根据实际情况进行调整,这里不做强制要求 + // 相关 Github issue 地址:https://github.com/getActivity/XXPermissions/issues/366 + HANDLER.postAtTime(showPopupRunnable, mHandlerToken, SystemClock.uptimeMillis() + 350); + } + + @Override + public void onRequestPermissionEnd(@NonNull Activity activity, @NonNull List requestList) { + // 移除跟这个 Token 有关但是没有还没有执行的消息 + HANDLER.removeCallbacksAndMessages(mHandlerToken); + // 销毁当前正在显示的弹窗 + dismissPopupWindow(); + dismissDialog(); + } + + /** + * 生成权限描述文案 + */ + private String generatePermissionDescription(@NonNull Activity activity, @NonNull List requestList) { + return PermissionConverter.getDescriptionsByPermissions(activity, requestList); + } + + /** + * 显示 Dialog + * + * @param dialogTitle 对话框标题 + * @param dialogMessage 对话框消息 + * @param confirmButtonText 对话框确认按钮文本 + * @param listener 对话框监听事件 + */ + private void showDialog(@NonNull Activity activity, @Nullable String dialogTitle, @Nullable String dialogMessage, + @Nullable String confirmButtonText, @Nullable MessageDialog.OnListener listener) { + if (mPermissionDialog != null) { + dismissDialog(); + } + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + // 另外这里需要判断 Activity 的类型来申请权限,这是因为只有 AppCompatActivity 才能调用 AndroidX 库的 AlertDialog 来显示,否则会出现报错 + // java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity + // 为什么不直接用系统包 AlertDialog 来显示,而是两套规则?因为系统包 AlertDialog 是系统自带的类,不同 Android 版本展现的样式可能不太一样 + // 如果这个 Android 版本比较低,那么这个对话框的样式就会变得很丑,准确来讲也不能说丑,而是当时系统的 UI 设计就是那样,它只是跟随系统的样式而已 + mPermissionDialog = new MessageDialog.Builder(activity) + .setTitle(dialogTitle) + .setMessage(dialogMessage) + .setConfirm(confirmButtonText) + .setCancelable(false) + .setListener(listener) + .create(); + mPermissionDialog.show(); + } + + /** + * 销毁 Dialog + */ + private void dismissDialog() { + if (mPermissionDialog == null) { + return; + } + if (!mPermissionDialog.isShowing()) { + return; + } + mPermissionDialog.dismiss(); + mPermissionDialog = null; + } + + /** + * 显示 PopupWindow + * + * @param content 弹窗显示的内容 + */ + private void showPopupWindow(@NonNull Activity activity, @NonNull String content) { + if (mPermissionPopupWindow != null) { + dismissPopupWindow(); + } + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + mPermissionPopupWindow = new PermissionDescriptionPopup.Builder(activity) + .setDescription(content) + .create(); + mPermissionPopupWindow.showAtLocation(decorView, Gravity.TOP, 0, 0); + } + + /** + * 销毁 PopupWindow + */ + private void dismissPopupWindow() { + if (mPermissionPopupWindow == null) { + return; + } + if (!mPermissionPopupWindow.isShowing()) { + return; + } + mPermissionPopupWindow.dismiss(); + mPermissionPopupWindow = null; + } + + /** + * 判断当前 Activity 是否是横盘显示 + */ + public static boolean isActivityLandscape(@NonNull Activity activity) { + return activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 获取当前设备的物理屏幕尺寸 + */ + @SuppressWarnings("deprecation") + public static double getPhysicalScreenSize(@NonNull Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display defaultDisplay = windowManager.getDefaultDisplay(); + if (defaultDisplay == null) { + return 0; + } + + DisplayMetrics metrics = new DisplayMetrics(); + defaultDisplay.getMetrics(metrics); + + float screenWidthInInches; + float screenHeightInInches; + Point point = new Point(); + defaultDisplay.getRealSize(point); + screenWidthInInches = point.x / metrics.xdpi; + screenHeightInInches = point.y / metrics.ydpi; + + // 勾股定理:直角三角形的两条直角边的平方和等于斜边的平方 + return Math.sqrt(Math.pow(screenWidthInInches, 2) + Math.pow(screenHeightInInches, 2)); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/permission/PermissionInterceptor.java b/app/src/main/java/com/hjq/demo/permission/PermissionInterceptor.java new file mode 100644 index 00000000..00c97f21 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/permission/PermissionInterceptor.java @@ -0,0 +1,235 @@ +package com.hjq.demo.permission; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.demo.ui.dialog.common.MessageDialog; +import com.hjq.permissions.OnPermissionCallback; +import com.hjq.permissions.OnPermissionInterceptor; +import com.hjq.permissions.XXPermissions; +import com.hjq.permissions.permission.PermissionGroups; +import com.hjq.permissions.permission.PermissionNames; +import com.hjq.permissions.permission.base.IPermission; +import com.hjq.toast.Toaster; +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/XXPermissions + * time : 2021/01/04 + * desc : 权限申请拦截器 + */ +public final class PermissionInterceptor implements OnPermissionInterceptor { + + @Override + public void onRequestPermissionEnd(@NonNull Activity activity, boolean skipRequest, + @NonNull List requestList, + @NonNull List grantedList, + @NonNull List deniedList, + @Nullable OnPermissionCallback callback) { + if (callback != null) { + callback.onResult(grantedList, deniedList); + } + + if (deniedList.isEmpty()) { + return; + } + boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList); + String permissionHint = generatePermissionHint(activity, deniedList, doNotAskAgain); + if (!doNotAskAgain) { + // 如果没有勾选不再询问选项,就弹 Toast 提示给用户 + Toaster.show(permissionHint); + return; + } + + // 如果勾选了不再询问选项,就弹 Dialog 引导用户去授权 + showPermissionSettingDialog(activity, requestList, deniedList, callback, permissionHint); + } + + private void showPermissionSettingDialog(@NonNull Activity activity, + @NonNull List requestList, + @NonNull List deniedList, + @Nullable OnPermissionCallback callback, + @NonNull String permissionHint) { + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + + new MessageDialog.Builder(activity) + .setTitle(R.string.common_permission_alert) + .setMessage(permissionHint) + .setConfirm(R.string.common_permission_go_to_authorization) + .setListener(dialog -> { + dialog.dismiss(); + XXPermissions.startPermissionActivity(activity, deniedList, (grantedList, deniedList1) -> { + List latestDeniedList = XXPermissions.getDeniedPermissions(activity, requestList); + boolean allGranted = latestDeniedList.isEmpty(); + if (!allGranted) { + // 递归显示对话框,让提示用户授权,只不过对话框是可取消的,用户不想授权了,随时可以点击返回键或者对话框蒙层来取消显示 + showPermissionSettingDialog(activity, requestList, latestDeniedList, callback, + generatePermissionHint(activity, latestDeniedList, true)); + return; + } + + if (callback == null) { + return; + } + // 用户全部授权了,回调成功给外层监听器,免得用户还要再发起权限申请 + callback.onResult(requestList, latestDeniedList); + }); + }) + .show(); + } + + /** + * 生成权限提示文案 + */ + @NonNull + private String generatePermissionHint(@NonNull Activity activity, @NonNull List deniedList, boolean doNotAskAgain) { + int deniedPermissionCount = deniedList.size(); + int deniedLocationPermissionCount = 0; + int deniedSensorsPermissionCount = 0; + int deniedHealthPermissionCount = 0; + for (IPermission deniedPermission : deniedList) { + String permissionGroup = deniedPermission.getPermissionGroup(activity); + if (TextUtils.isEmpty(permissionGroup)) { + continue; + } + if (PermissionGroups.LOCATION.equals(permissionGroup)) { + deniedLocationPermissionCount++; + } else if (PermissionGroups.SENSORS.equals(permissionGroup)) { + deniedSensorsPermissionCount++; + } else if (XXPermissions.isHealthPermission(deniedPermission)) { + deniedHealthPermissionCount++; + } + } + + if (deniedLocationPermissionCount == deniedPermissionCount && AndroidVersion.isAndroid10()) { + if (deniedLocationPermissionCount == 1) { + if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.ACCESS_BACKGROUND_LOCATION)) { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_location_background), + getBackgroundPermissionOptionLabel(activity)); + } else if (AndroidVersion.isAndroid12() && + XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.ACCESS_FINE_LOCATION)) { + // 如果请求的定位权限中,既包含了精确定位权限,又包含了模糊定位权限或者后台定位权限, + // 但是用户只同意了模糊定位权限的情况或者后台定位权限,并没有同意精确定位权限的情况,就提示用户开启确切位置选项 + // 需要注意的是 Android 12 才将模糊定位权限和精确定位权限的授权选项进行分拆,之前的版本没有区分得那么仔细 + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_location_fine), + activity.getString(R.string.common_permission_location_fine_option)); + } + } else { + if (XXPermissions.containsPermission(deniedList, PermissionNames.ACCESS_BACKGROUND_LOCATION)) { + if (AndroidVersion.isAndroid12() && + XXPermissions.containsPermission(deniedList, PermissionNames.ACCESS_FINE_LOCATION)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_location), + getBackgroundPermissionOptionLabel(activity), + activity.getString(R.string.common_permission_location_fine_option)); + } else { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_location), + getBackgroundPermissionOptionLabel(activity)); + } + } + } + } else if (deniedSensorsPermissionCount == deniedPermissionCount && AndroidVersion.isAndroid13()) { + if (deniedPermissionCount == 1) { + if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.BODY_SENSORS_BACKGROUND)) { + if (AndroidVersion.isAndroid16()) { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_health_data_background_option)); + } else { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_body_sensors_background), + getBackgroundPermissionOptionLabel(activity)); + } + } + } else { + if (doNotAskAgain) { + if (AndroidVersion.isAndroid16()) { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_health_data), + activity.getString(R.string.common_permission_allow_all_option)); + } else { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_body_sensors), + getBackgroundPermissionOptionLabel(activity)); + } + } + } + } else if (deniedHealthPermissionCount == deniedPermissionCount && AndroidVersion.isAndroid16()) { + + switch (deniedPermissionCount) { + case 1: + if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_health_data_background_option)); + } else if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.READ_HEALTH_DATA_HISTORY)) { + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_health_data_past), + activity.getString(R.string.common_permission_health_data_past_option)); + } + break; + case 2: + if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY) && + XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_health_data_past) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_health_data_past_option) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background_option)); + } else if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_past), + activity.getString(R.string.common_permission_allow_all_option), + activity.getString(R.string.common_permission_health_data_background_option)); + } else if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_allow_all_option), + activity.getString(R.string.common_permission_health_data_background_option)); + } + break; + default: + if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY) && + XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_past) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_allow_all_option), + activity.getString(R.string.common_permission_health_data_past_option) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background_option)); + } + break; + } + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_health_data), + activity.getString(R.string.common_permission_allow_all_option)); + } + + return activity.getString(doNotAskAgain ? R.string.common_permission_fail_assign_hint_1 : + R.string.common_permission_fail_assign_hint_2, + PermissionConverter.getNickNamesByPermissions(activity, deniedList)); + } + + /** + * 获取后台权限的《始终允许》选项的文案 + */ + @NonNull + private String getBackgroundPermissionOptionLabel(@NonNull Context context) { + PackageManager packageManager = context.getPackageManager(); + if (packageManager != null && AndroidVersion.isAndroid11()) { + CharSequence backgroundPermissionOptionLabel = packageManager.getBackgroundPermissionOptionLabel(); + if (!TextUtils.isEmpty(backgroundPermissionOptionLabel)) { + return backgroundPermissionOptionLabel.toString(); + } + } + + return context.getString(R.string.common_permission_allow_all_the_time_option); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java b/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java index 38847f32..9e80dc34 100644 --- a/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java +++ b/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java @@ -1,7 +1,9 @@ package com.hjq.demo.ui.activity; +import android.view.View; +import androidx.annotation.Nullable; import com.hjq.demo.R; -import com.hjq.demo.common.MyActivity; +import com.hjq.demo.app.AppActivity; /** * author : Android 轮子哥 @@ -9,11 +11,11 @@ * time : 2018/10/18 * desc : 关于界面 */ -public final class AboutActivity extends MyActivity { +public final class AboutActivity extends AppActivity { @Override protected int getLayoutId() { - return R.layout.activity_about; + return R.layout.about_activity; } @Override @@ -25,4 +27,10 @@ protected void initView() { protected void initData() { } + + @Nullable + @Override + public View getImmersionBottomView() { + return findViewById(R.id.tv_about_copyright); + } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.java b/app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.java index d00dc196..3fd8e249 100644 --- a/app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.java +++ b/app/src/main/java/com/hjq/demo/ui/activity/CopyActivity.java @@ -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,11 +9,11 @@ * time : 2018/10/18 * desc : 可进行拷贝的副本 */ -public final class CopyActivity extends MyActivity { +public final class CopyActivity extends AppActivity { @Override protected int getLayoutId() { - return R.layout.activity_copy; + return R.layout.copy_activity; } @Override diff --git a/app/src/main/java/com/hjq/demo/ui/activity/CrashActivity.java b/app/src/main/java/com/hjq/demo/ui/activity/CrashActivity.java deleted file mode 100644 index f6cda734..00000000 --- a/app/src/main/java/com/hjq/demo/ui/activity/CrashActivity.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.hjq.demo.ui.activity; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.DialogInterface; -import android.util.TypedValue; -import android.view.View; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; - -import com.hjq.demo.R; -import com.hjq.demo.common.MyActivity; - -import butterknife.OnClick; -import cat.ereza.customactivityoncrash.CustomActivityOnCrash; -import cat.ereza.customactivityoncrash.config.CaocConfig; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/06/27 - * desc : 崩溃捕捉界面 - */ -public final class CrashActivity extends MyActivity { - - private CaocConfig mConfig; - - @Override - protected int getLayoutId() { - return R.layout.activity_crash; - } - - @Override - protected void initView() { - - } - - @Override - protected void initData() { - mConfig = CustomActivityOnCrash.getConfigFromIntent(getIntent()); - if (mConfig == null) { - // 这种情况永远不会发生,只要完成该活动就可以避免递归崩溃。 - finish(); - } - } - - @OnClick({R.id.btn_crash_restart, R.id.btn_crash_log}) - public void onClick(View v) { - switch (v.getId()) { - case R.id.btn_crash_restart: - CustomActivityOnCrash.restartApplication(CrashActivity.this, mConfig); - break; - case R.id.btn_crash_log: - AlertDialog dialog = new AlertDialog.Builder(CrashActivity.this) - .setTitle(R.string.crash_error_details) - .setMessage(CustomActivityOnCrash.getAllErrorDetailsFromIntent(CrashActivity.this, getIntent())) - .setPositiveButton(R.string.crash_close, null) - .setNeutralButton(R.string.crash_copy_log, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - copyErrorToClipboard(); - } - }) - .show(); - TextView textView = dialog.findViewById(android.R.id.message); - if (textView != null) { - textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); - } - break; - default: - break; - } - } - - /** - * 复制报错信息到剪贴板 - */ - @SuppressWarnings("all") - private void copyErrorToClipboard() { - String errorInformation = CustomActivityOnCrash.getAllErrorDetailsFromIntent(CrashActivity.this, getIntent()); - ContextCompat.getSystemService(this, ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(getString(R.string.crash_error_info), errorInformation)); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java b/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java index 91861cd5..0b62c2fb 100644 --- a/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java +++ b/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java @@ -1,38 +1,39 @@ package com.hjq.demo.ui.activity; -import android.content.Intent; import android.view.Gravity; -import android.view.KeyEvent; import android.view.View; -import android.widget.ImageView; - +import android.widget.Button; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import com.hjq.bar.TitleBar; import com.hjq.base.BaseDialog; -import com.hjq.base.BaseDialogFragment; +import com.hjq.base.BasePopupWindow; import com.hjq.demo.R; -import com.hjq.demo.common.MyActivity; +import com.hjq.demo.aop.SingleClick; +import com.hjq.demo.app.AppActivity; +import com.hjq.demo.manager.DialogManager; import com.hjq.demo.ui.dialog.PayPasswordDialog; -import com.hjq.demo.ui.dialog.ShareDialog; +import com.hjq.demo.ui.dialog.SafeDialog; import com.hjq.demo.ui.dialog.UpdateDialog; -import com.hjq.demo.wxapi.WXEntryActivity; -import com.hjq.demo.ui.dialog.AddressDialog; -import com.hjq.demo.ui.dialog.DateDialog; -import com.hjq.demo.ui.dialog.InputDialog; -import com.hjq.demo.ui.dialog.MenuDialog; -import com.hjq.demo.ui.dialog.MessageDialog; -import com.hjq.demo.ui.dialog.TimeDialog; -import com.hjq.demo.ui.dialog.ToastDialog; -import com.hjq.demo.ui.dialog.WaitDialog; -import com.hjq.umeng.Platform; -import com.hjq.umeng.UmengClient; -import com.hjq.umeng.UmengShare; - +import com.hjq.demo.ui.dialog.common.AddressDialog; +import com.hjq.demo.ui.dialog.common.DateDialog; +import com.hjq.demo.ui.dialog.common.InputDialog; +import com.hjq.demo.ui.dialog.common.MenuDialog; +import com.hjq.demo.ui.dialog.common.MessageDialog; +import com.hjq.demo.ui.dialog.common.SelectDialog; +import com.hjq.demo.ui.dialog.common.ShareDialog; +import com.hjq.demo.ui.dialog.common.TimeDialog; +import com.hjq.demo.ui.dialog.common.TipsDialog; +import com.hjq.demo.ui.dialog.common.WaitDialog; +import com.hjq.demo.ui.popup.ListPopup; +import com.hjq.umeng.sdk.Platform; +import com.hjq.umeng.sdk.UmengShare; +import com.umeng.socialize.media.UMImage; +import com.umeng.socialize.media.UMWeb; import java.util.ArrayList; import java.util.Calendar; import java.util.List; - -import butterknife.OnClick; +import java.util.Map; /** * author : Android 轮子哥 @@ -40,16 +41,31 @@ * time : 2018/12/02 * desc : 对话框使用案例 */ -public final class DialogActivity extends MyActivity { +public final class DialogActivity extends AppActivity { + + /** 等待对话框 */ + private BaseDialog mWaitDialog; + + /** 菜单弹窗 */ + private BasePopupWindow mListPopup; @Override protected int getLayoutId() { - return R.layout.activity_dialog; + return R.layout.dialog_activity; } @Override protected void initView() { - + setOnClickListener(R.id.btn_dialog_message, R.id.btn_dialog_input, + R.id.btn_dialog_bottom_menu, R.id.btn_dialog_center_menu, + R.id.btn_dialog_single_select, R.id.btn_dialog_more_select, + R.id.btn_dialog_succeed_toast, R.id.btn_dialog_fail_toast, + R.id.btn_dialog_warn_toast, R.id.btn_dialog_wait, + R.id.btn_dialog_pay, R.id.btn_dialog_address, + R.id.btn_dialog_date, R.id.btn_dialog_time, + R.id.btn_dialog_update, R.id.btn_dialog_share, + R.id.btn_dialog_safe, R.id.btn_dialog_custom, + R.id.btn_dialog_multi); } @Override @@ -57,376 +73,472 @@ protected void initData() { } - @OnClick({R.id.btn_dialog_message, R.id.btn_dialog_input, R.id.btn_dialog_bottom_menu, R.id.btn_dialog_center_menu, - R.id.btn_dialog_succeed_toast, R.id.btn_dialog_fail_toast, R.id.btn_dialog_warn_toast, R.id.btn_dialog_wait, - R.id.btn_dialog_pay, R.id.btn_dialog_address, R.id.btn_dialog_date, R.id.btn_dialog_time, - R.id.btn_dialog_update, R.id.btn_dialog_share, R.id.btn_dialog_custom}) - public void onClick(View v) { - switch (v.getId()) { + @Nullable + @Override + public View getImmersionBottomView() { + return findViewById(R.id.ll_dialog_content); + } + + @SingleClick + @Override + public void onClick(@NonNull View view) { + int viewId = view.getId(); + if (viewId == R.id.btn_dialog_message) { + // 消息对话框 - 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() { - - @Override - public void onConfirm(BaseDialog dialog) { - toast("确定了"); - } - - @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, String content) { - toast("确定了:" + content); - } - - @Override - public void onCancel(BaseDialog dialog) { - toast("取消了"); - } - }) - .show(); - break; - case R.id.btn_dialog_bottom_menu: - List data = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - data.add("我是数据" + i); - } - // 底部选择框 - new MenuDialog.Builder(this) - // 设置 null 表示不显示取消按钮 - //.setCancel(getString(R.string.common_cancel)) - // 设置点击按钮后不关闭对话框 - //.setAutoDismiss(false) - .setList(data) - .setListener(new MenuDialog.OnListener() { - - @Override - public void onSelected(BaseDialog dialog, int position, String string) { - toast("位置:" + position + ",文本:" + string); - } - - @Override - public void onCancel(BaseDialog dialog) { - toast("取消了"); - } - }) - .show(); - break; - case R.id.btn_dialog_center_menu: - List data1 = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - data1.add("我是数据" + i); - } - // 居中选择框 - new MenuDialog.Builder(this) - .setGravity(Gravity.CENTER) - // 设置 null 表示不显示取消按钮 - //.setCancel(null) - // 设置点击按钮后不关闭对话框 - //.setAutoDismiss(false) - .setList(data1) - .setListener(new MenuDialog.OnListener() { - - @Override - public void onSelected(BaseDialog dialog, int position, String string) { - toast("位置:" + position + ",文本:" + string); - } - - @Override - public void onCancel(BaseDialog dialog) { - toast("取消了"); - } - }) - .show(); - break; - case R.id.btn_dialog_succeed_toast: - // 成功对话框 - new ToastDialog.Builder(this) - .setType(ToastDialog.Type.FINISH) - .setMessage("完成") - .show(); - break; - case R.id.btn_dialog_fail_toast: - // 失败对话框 - new ToastDialog.Builder(this) - .setType(ToastDialog.Type.ERROR) - .setMessage("错误") - .show(); - break; - case R.id.btn_dialog_warn_toast: - // 警告对话框 - new ToastDialog.Builder(this) - .setType(ToastDialog.Type.WARN) - .setMessage("警告") - .show(); - break; - case R.id.btn_dialog_wait: - // 等待对话框 - final BaseDialog dialog = new WaitDialog.Builder(this) + 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() { + + @Override + public void onConfirm(@NonNull BaseDialog dialog) { + toast("确定了"); + } + + @Override + public void onCancel(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == 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(@NonNull BaseDialog dialog, String content) { + toast("确定了:" + content); + } + + @Override + public void onCancel(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == R.id.btn_dialog_bottom_menu) { + + List data = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + data.add("我是数据" + (i + 1)); + } + // 底部选择框 + new MenuDialog.Builder(this) + // 设置 null 表示不显示取消按钮 + //.setCancel(getString(R.string.common_cancel)) + // 设置点击按钮后不关闭对话框 + //.setAutoDismiss(false) + .setList(data) + .setListener(new MenuDialog.OnListener() { + + @Override + public void onSelected(@NonNull BaseDialog dialog, int position, String data) { + toast("位置:" + position + ",文本:" + data); + } + + @Override + public void onCancel(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == R.id.btn_dialog_center_menu) { + + List data = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + data.add("我是数据" + (i + 1)); + } + // 居中选择框 + new MenuDialog.Builder(this) + .setGravity(Gravity.CENTER) + // 设置 null 表示不显示取消按钮 + //.setCancel(null) + // 设置点击按钮后不关闭对话框 + //.setAutoDismiss(false) + .setList(data) + .setListener(new MenuDialog.OnListener() { + + @Override + public void onSelected(@NonNull BaseDialog dialog, int position, String data) { + toast("位置:" + position + ",文本:" + data); + } + + @Override + public void onCancel(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == R.id.btn_dialog_single_select) { + + // 单选对话框 + new SelectDialog.Builder(this) + .setTitle("请选择你的性别") + .setList("男", "女") + // 设置单选模式 + .setSingleSelect() + // 设置默认选中 + .setSelect(0) + .setSingleListener(new SelectDialog.OnSingleListener() { + + @Override + public void onSelected(@NonNull BaseDialog dialog, int position, String data) { + toast("位置:" + position + ",数据:" + data); + } + + @Override + public void onCancel(@NonNull 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) + .setMultiListener(new SelectDialog.OnMultiListener() { + + @Override + public void onSelected(@NonNull BaseDialog dialog, Map data) { + toast("确定了:" + data.toString()); + } + + @Override + public void onCancel(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == R.id.btn_dialog_succeed_toast) { + + // 成功对话框 + new TipsDialog.Builder(this) + .setIcon(TipsDialog.ICON_FINISH) + .setMessage("完成") + .show(); + + } else if (viewId == R.id.btn_dialog_fail_toast) { + + // 失败对话框 + new TipsDialog.Builder(this) + .setIcon(TipsDialog.ICON_ERROR) + .setMessage("错误") + .show(); + + } else if (viewId == R.id.btn_dialog_warn_toast) { + + // 警告对话框 + new TipsDialog.Builder(this) + .setIcon(TipsDialog.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(new Runnable() { - @Override - public void run() { - dialog.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() { - - @Override - public void onCompleted(BaseDialog dialog, String password) { - toast(password); - } - - @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() { - - @Override - public void onSelected(BaseDialog dialog, String province, String city, String area) { - toast(province + city + area); - } - - @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)); - - // 如果不指定时分秒则默认为现在的时间 - 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(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() { - - @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(); - break; - case R.id.btn_dialog_share: - toast("记得改好第三方 AppID 和 AppKey,否则会调不起来哦"); - toast("也别忘了改微信 " + WXEntryActivity.class.getSimpleName() + " 类所在的包名哦"); - // 分享对话框 - new ShareDialog.Builder(this) - // 分享标题 - .setShareTitle("Github") - // 分享描述 - .setShareDescription("AndroidProject") - // 分享缩略图 - .setShareLogo("https://avatars1.githubusercontent.com/u/28616817?s=460&v=4") - // 分享链接 - .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(); - break; - case R.id.btn_dialog_update: - new UpdateDialog.Builder(this) - // 版本名 - .setVersionName("v 2.0") - // 文件大小 - .setFileSize("10 M") - // 是否强制更新 - .setForceUpdate(false) - // 更新日志 - .setUpdateLog("到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥") - // 下载 url - .setDownloadUrl("https://raw.githubusercontent.com/getActivity/AndroidProject/master/AndroidProject.apk") - .show(); - break; - case R.id.btn_dialog_custom: - // 自定义对话框 - new BaseDialogFragment.Builder(this) - .setContentView(R.layout.dialog_custom) - //.setText(id, "我是预设置的文本") - .setOnClickListener(R.id.btn_dialog_custom_ok, new BaseDialog.OnClickListener() { - - @Override - public void onClick(BaseDialog dialog, ImageView view) { - dialog.dismiss(); - } - }) - .addOnShowListener(new BaseDialog.OnShowListener() { - @Override - public void onShow(BaseDialog dialog) { - toast("Dialog 显示了"); - } - }) - .addOnCancelListener(new BaseDialog.OnCancelListener() { - @Override - public void onCancel(BaseDialog dialog) { - toast("Dialog 取消了"); - } - }) - .addOnDismissListener(new BaseDialog.OnDismissListener() { - @Override - public void onDismiss(BaseDialog dialog) { - toast("Dialog 销毁了"); - } - }) - .setOnKeyListener(new BaseDialog.OnKeyListener() { - @Override - public boolean onKey(BaseDialog dialog, KeyEvent event) { - toast("按键代码:" + event.getKeyCode()); - return false; - } - }) - .show(); - break; - default: - break; + .create(); + } + if (!mWaitDialog.isShowing()) { + mWaitDialog.show(); + postDelayed(mWaitDialog::dismiss, 2000); + } + + } else if (viewId == 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() { + + @Override + public void onCompleted(@NonNull BaseDialog dialog, String password) { + toast(password); + } + + @Override + public void onCancel(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == R.id.btn_dialog_address) { + + // 选择地区对话框 + new AddressDialog.Builder(this) + .setTitle(getString(R.string.address_title)) + // 设置默认省份 + //.setProvince("广东省") + // 设置默认城市(必须要先设置默认省份) + //.setCity("广州市") + // 不选择县级区域 + //.setIgnoreArea() + .setListener(new AddressDialog.OnListener() { + + @Override + public void onSelected(@NonNull BaseDialog dialog, @NonNull String province, @NonNull String city, @NonNull String area) { + toast(province + city + area); + } + + @Override + public void onCancel(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == 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(@NonNull 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)); + + // 如果不指定时分秒则默认为现在的时间 + 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(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == 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() { + + @Override + public void onSelected(@NonNull 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(@NonNull BaseDialog dialog) { + toast("取消了"); + } + }) + .show(); + + } else if (viewId == R.id.btn_dialog_share) { + + toast("记得改好第三方 AppID 和 Secret,否则会调不起来哦"); + + UMWeb umWeb = new UMWeb("https://github.com/getActivity/AndroidProject"); + umWeb.setTitle("Github"); + umWeb.setThumb(new UMImage(this, R.mipmap.launcher_ic)); + umWeb.setDescription(getString(R.string.app_name)); + + /* UMImage umImage = new UMImage(this, R.mipmap.launcher_ic); */ + + // 分享对话框 + new ShareDialog.Builder(this) + .setShareLink(umWeb) + .setListener(new UmengShare.OnShareListener() { + + @Override + public void onShareSuccess(@NonNull Platform platform) { + toast("分享成功"); + } + + @Override + public void onShareFail(@NonNull Platform platform, @NonNull Throwable throwable) { + toast(throwable.getMessage()); + } + + @Override + public void onShareCancel(@NonNull 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到底更新了啥\n到底更新了啥") + // 下载 URL + .setDownloadUrl("https://dldir1.qq.com/weixin/android/weixin8015android2020_arm64.apk") + // 文件 MD5 + .setFileMd5("b05b25d4738ea31091dd9f80f4416469") + .show(); + + } else if (viewId == R.id.btn_dialog_safe) { + + // 身份校验对话框 + new SafeDialog.Builder(this) + .setListener(new SafeDialog.OnListener() { + + @Override + public void onConfirm(@NonNull BaseDialog dialog, @NonNull String phone, @NonNull String code) { + toast("手机号:" + phone + "\n验证码:" + code); + } + + @Override + public void onCancel(@NonNull 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) + .setOnCreateListener(dialog -> toast("Dialog 创建了")) + .addOnShowListener(dialog -> toast("Dialog 显示了")) + .addOnCancelListener(dialog -> toast("Dialog 取消了")) + .addOnDismissListener(dialog -> toast("Dialog 销毁了")) + .setOnKeyListener((dialog, event) -> { + toast("按键代码:" + event.getKeyCode()); + return false; + }) + .setOnClickListenerByView(R.id.btn_dialog_custom_ok, (BaseDialog.OnClickListener