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 d3b08413..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/IMvpView.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.hjq.demo.mvp;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : MVP 通用性接口
- */
-public interface IMvpView {
-
- /**
- * 用于页面请求数据时显示加载状态
- */
- void showLoading();
-
- /**
- * 用于请求的数据为空的状态
- */
- void showEmpty();
-
- /**
- * 用于请求数据出错
- */
- void showError();
-
- /**
- * 用于请求数据完成
- */
- void loadingComplete();
-}
\ 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 f1e2f647..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/MvpActivity.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.hjq.demo.mvp;
-
-import com.hjq.demo.base.MyActivity;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : MVP Activity 基类
- */
-public abstract class MvpActivity extends MyActivity {
-
- private P mPresenter;
-
- @Override
- public void init() {
- mPresenter = initPresenter();
- mPresenter.attach(this);
- mPresenter.start();
- super.init();
- }
-
- @Override
- protected void onDestroy() {
- mPresenter.detach();
- super.onDestroy();
- }
-
- public P getPresenter() {
- return mPresenter;
- }
-
- protected abstract P initPresenter();
-}
\ 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 f33c023c..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/MvpLazyFragment.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.hjq.demo.mvp;
-
-import com.hjq.demo.base.MyLazyFragment;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : MVP 懒加载 Fragment 基类
- */
-public abstract class MvpLazyFragment
extends MyLazyFragment {
-
- private P mPresenter;
-
- @Override
- protected void init() {
- mPresenter = initPresenter();
- mPresenter.attach(this);
- mPresenter.start();
- super.init();
- }
-
- @Override
- public void onDestroy() {
- mPresenter.detach();
- super.onDestroy();
- }
-
- public P getPresenter() {
- return mPresenter;
- }
-
- protected abstract P initPresenter();
-}
\ 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 c1e09ff3..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/MvpModel.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.hjq.demo.mvp;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : MVP 模型基类
- */
-public abstract class MvpModel {
-
- private L mListener;
-
- public void setListener(L l) {
- mListener = l;
- }
-
- 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 c956103f..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/MvpPresenter.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.hjq.demo.mvp;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : MVP 业务基类
- */
-public abstract class MvpPresenter {
-
- private V mView;
-
- public void attach(V view){
- mView = view;
- }
-
- public void detach() {
- mView = null;
- }
-
- public boolean isAttached() {
- return mView != null;
- }
-
- public V getView() {
- return mView;
- }
-
- public abstract void start();
-}
\ 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 6c8c802b..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyContract.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.hjq.demo.mvp.copy;
-
-import com.hjq.demo.mvp.IMvpView;
-
-import java.util.List;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : 可进行拷贝的契约类
- */
-public class CopyContract {
-
- /**
- * 这里的 {@link IMvpView} 可继承可不继承,看实际情况而定
- */
- public interface View /*extends IMvpView*/ {
-
- void loginError(String msg);
-
- void loginSuccess(List data);
- }
-
- 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 6d3bffcb..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 : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : 可进行拷贝的接口实现类
- */
-public 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 d1694e09..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyMvpActivity.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.hjq.demo.mvp.copy;
-
-import android.view.View;
-
-import com.hjq.demo.mvp.MvpActivity;
-
-import java.util.List;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : 可进行拷贝的MVP Activity 类
- */
-public class CopyMvpActivity extends MvpActivity implements CopyContract.View {
-
- @Override
- protected CopyPresenter initPresenter() {
- return new CopyPresenter();
- }
-
- @Override
- protected int getLayoutId() {
- return 0;
- }
-
- @Override
- protected int getTitleBarId() {
- return 0;
- }
-
- @Override
- protected void initView() {
-
- }
-
- @Override
- protected void initData() {
-
- }
-
- public void onLogin(View view) {
- // 登录操作
- getPresenter().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 d4545cf0..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 : HJQ
- * 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 a71e859e..00000000
--- a/app/src/main/java/com/hjq/demo/mvp/copy/CopyPresenter.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.hjq.demo.mvp.copy;
-
-import com.hjq.demo.mvp.MvpPresenter;
-
-import java.util.List;
-
-/**
- * author : HJQ
- * github : https://github.com/getActivity/AndroidProject
- * time : 2018/11/17
- * desc : 可进行拷贝的业务处理类
- */
-public class CopyPresenter extends MvpPresenter implements CopyContract.Presenter {
-
- private CopyModel mModel;
-
- @Override
- public void start() {
- mModel = new CopyModel();
- }
-
- @Override
- public void login(String account, String password) {
- mModel.setAccount(account);
- mModel.setPassword(password);
- mModel.setListener(new CopyOnListener() {
-
- @Override
- public void onSucceed(List data) {
- getView().loginSuccess(data);
- }
-
- @Override
- public void onFail(String msg) {
- getView().loginError(msg);
- }
- });
- mModel.login();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/hjq/demo/other/AppConfig.java b/app/src/main/java/com/hjq/demo/other/AppConfig.java
new file mode 100644
index 00000000..b6ba0b4f
--- /dev/null
+++ b/app/src/main/java/com/hjq/demo/other/AppConfig.java
@@ -0,0 +1,75 @@
+package com.hjq.demo.other;
+
+import com.hjq.demo.BuildConfig;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/09/02
+ * desc : App 配置管理类
+ */
+public final class AppConfig {
+
+ /**
+ * 当前是否为调试模式
+ */
+ public static boolean isDebug() {
+ return BuildConfig.DEBUG;
+ }
+
+ /**
+ * 获取当前构建的模式
+ */
+ public static String getBuildType() {
+ return BuildConfig.BUILD_TYPE;
+ }
+
+ /**
+ * 当前是否要开启日志打印功能
+ */
+ public static boolean isLogEnable() {
+ return BuildConfig.LOG_ENABLE;
+ }
+
+ /**
+ * 获取当前应用的包名
+ */
+ public static String getPackageName() {
+ return BuildConfig.APPLICATION_ID;
+ }
+
+ /**
+ * 获取当前应用的版本名
+ */
+ public static String getVersionName() {
+ return BuildConfig.VERSION_NAME;
+ }
+
+ /**
+ * 获取当前应用的版本码
+ */
+ public static int getVersionCode() {
+ return BuildConfig.VERSION_CODE;
+ }
+
+ /**
+ * 获取服务器主机地址
+ */
+ public static String getHostUrl() {
+ return BuildConfig.HOST_URL;
+ }
+
+ /**
+ * 获取 BuglyId
+ */
+ public static String getBuglyId() {
+ return BuildConfig.BUGLY_ID;
+ }
+
+ /**
+ * 获取 BuglyKey
+ */
+ 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/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/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/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..8f2a40a8
--- /dev/null
+++ b/app/src/main/java/com/hjq/demo/permission/PermissionDescription.java
@@ -0,0 +1,228 @@
+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.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 com.hjq.permissions.tools.PermissionTaskHandler;
+import java.util.List;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/XXPermissions
+ * time : 2025/05/30
+ * desc : 权限请求描述实现
+ */
+public final class PermissionDescription implements OnPermissionDescription {
+
+ /** 权限请求描述弹窗显示类型: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
+ PermissionTaskHandler.sendTask(showPopupRunnable, mHandlerToken, 350);
+ }
+
+ @Override
+ public void onRequestPermissionEnd(@NonNull Activity activity, @NonNull List requestList) {
+ // 移除跟这个 Token 有关但是没有还没有执行的消息
+ PermissionTaskHandler.cancelTask(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..00a59889
--- /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.onPermissionResult(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.onPermissionResult(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 6b334011..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,24 +1,21 @@
package com.hjq.demo.ui.activity;
+import android.view.View;
+import androidx.annotation.Nullable;
import com.hjq.demo.R;
-import com.hjq.demo.base.MyActivity;
+import com.hjq.demo.app.AppActivity;
/**
- * author : HJQ
+ * author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 关于界面
*/
-public class AboutActivity extends MyActivity {
+public final class AboutActivity extends AppActivity {
@Override
protected int getLayoutId() {
- return R.layout.activity_about;
- }
-
- @Override
- protected int getTitleBarId() {
- return R.id.tb_about_title;
+ return R.layout.about_activity;
}
@Override
@@ -30,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 4a4b1028..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,24 +1,19 @@
package com.hjq.demo.ui.activity;
import com.hjq.demo.R;
-import com.hjq.demo.base.MyActivity;
+import com.hjq.demo.app.AppActivity;
/**
- * author : HJQ
+ * author : Android 轮子哥
* github : https://github.com/getActivity/AndroidProject
* time : 2018/10/18
* desc : 可进行拷贝的副本
*/
-public class CopyActivity extends MyActivity {
+public final class CopyActivity extends AppActivity {
@Override
protected int getLayoutId() {
- return R.layout.activity_copy;
- }
-
- @Override
- protected int getTitleBarId() {
- return R.id.tb_copy_title;
+ return R.layout.copy_activity;
}
@Override
@@ -30,4 +25,4 @@ protected void initView() {
protected void initData() {
}
-}
+}
\ 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
new file mode 100644
index 00000000..0b62c2fb
--- /dev/null
+++ b/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java
@@ -0,0 +1,544 @@
+package com.hjq.demo.ui.activity;
+
+import android.view.Gravity;
+import android.view.View;
+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.BasePopupWindow;
+import com.hjq.demo.R;
+import com.hjq.demo.aop.SingleClick;
+import com.hjq.demo.app.AppActivity;
+import com.hjq.demo.manager.DialogManager;
+import com.hjq.demo.ui.dialog.PayPasswordDialog;
+import com.hjq.demo.ui.dialog.SafeDialog;
+import com.hjq.demo.ui.dialog.UpdateDialog;
+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 java.util.Map;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/12/02
+ * desc : 对话框使用案例
+ */
+public final class DialogActivity extends AppActivity {
+
+ /** 等待对话框 */
+ private BaseDialog mWaitDialog;
+
+ /** 菜单弹窗 */
+ private BasePopupWindow mListPopup;
+
+ @Override
+ protected int getLayoutId() {
+ 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
+ protected void initData() {
+
+ }
+
+ @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) {
+
+ // 消息对话框
+ 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))
+ .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