diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffc5b62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +libs/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..fb22c1d --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..efc9bd2 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..1772617 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..9df256b --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bfef59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/alcoholic_jks.jks b/alcoholic_jks.jks new file mode 100644 index 0000000..c83a4aa Binary files /dev/null and b/alcoholic_jks.jks differ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..e1a4f15 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,112 @@ +/build +### Android template +# Built application files +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..7745b06 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,110 @@ +apply plugin: 'com.android.application' +apply from: '../config.gradle' +apply plugin: 'org.greenrobot.greendao' + +android { + + defaultConfig { + applicationId "com.example.alcoholic" + + // 仅保留两种架构的 so 库,根据 Bugly 统计得出 + ndk { + // armeabi:万金油架构平台(占用率:0%) + // armeabi-v7a:曾经主流的架构平台(占用率:10%) + // arm64-v8a:目前主流架构平台(占用率:90%) + abiFilters 'armeabi-v7a', 'arm64-v8a' + } + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + + //greendao配置 + greendao { + //版本号,升级时可配置 + schemaVersion 1 + daoPackage 'com.example.alcoholic.gen' + targetGenDir 'src/main/java' + } + + + applicationVariants.all { variant -> + + // Apk 输出文件名配置 + variant.outputs.all { output -> + outputFileName = rootProject.getName() + '_v' + variant.versionName + '_' + variant.buildType.name + if (variant.buildType.name == buildTypes.release.getName()) { + outputFileName += '_' + new Date().format('MMdd') + } + outputFileName += '.apk' + } + } +} + +dependencies { + // 基类库 + implementation project(':base') + // 自定义 View + implementation project(':widget') + // 吐司框架:https://github.com/getActivity/ToastUtils + implementation 'com.hjq:toast:8.6' + // 网络请求框架:https://github.com/getActivity/EasyHttp + implementation 'com.hjq:http:6.9' + // 权限请求框架:https://github.com/getActivity/XXPermissions + implementation 'com.hjq:xxpermissions:6.5' + // 标题栏框架:https://github.com/getActivity/TitleBar + implementation 'com.hjq:titlebar:6.5' + // 日志调试:https://github.com/getActivity/Logcat + debugImplementation 'com.hjq:logcat:8.2' + //日志打印 + implementation 'com.orhanobut:logger:2.2.0' + // OkHttp 框架:https://github.com/square/okhttp + implementation 'com.squareup.okhttp3:okhttp:4.2.2' + // JSON 解析框架:https://github.com/google/gson + implementation 'com.google.code.gson:gson:2.8.6' + // 状态栏沉浸:https://github.com/gyf-dev/ImmersionBar + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + // 图片加载框架:https://github.com/bumptech/glide + implementation 'com.github.bumptech.glide:glide:4.11.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' + //BRVAH 适配器 + implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.6' + //im + implementation 'com.google.android.gms:play-services-base:11.4.0' + implementation 'com.google.firebase:firebase-messaging:11.4.0' + api 'com.hyphenate:hyphenate-sdk:3.7.2' + //eventBus + implementation 'org.greenrobot:eventbus:3.2.0' + //今日头条适配 https://github.com/taowuhua/AndroidAutoSize/blob/master/README-zh.md + implementation 'me.jessyan:autosize:1.1.2' + //MMKV https://github.com/Tencent/MMKV + implementation 'com.tencent:mmkv-static:1.2.5' + //greenDao https://github.com/greenrobot/greenDAO + implementation 'org.greenrobot:greendao:3.3.0' + //rxjava https://github.com/ReactiveX/RxJava + //rxandroid https://github.com/ReactiveX/RxAndroid + implementation "io.reactivex.rxjava3:rxjava:3.0.7" + implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' + // AOP 插件库:https://mvnrepository.com/artifact/org.aspectj/aspectjrt + implementation 'org.aspectj:aspectjrt:1.9.5' + // 手势 ImageView:https://github.com/chrisbanes/PhotoView + implementation 'com.github.chrisbanes:PhotoView:2.3.0' + // ViewPager 指示器:https://github.com/romandanylyk/PageIndicatorView + implementation 'com.romandanylyk:pageindicatorview:1.0.3' + //luban https://github.com/Curzibn/Luban + implementation 'top.zibin:Luban:1.1.8' + //smartrefresh https://github.com/scwang90/SmartRefreshLayout + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'com.scwang.smart:refresh-layout-kernel:2.0.1' + implementation 'com.scwang.smart:refresh-header-classics:2.0.1' + implementation 'com.scwang.smart:refresh-footer-classics:2.0.1' + + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/Alcoholic_v1.0_release_1123.apk b/app/release/Alcoholic_v1.0_release_1123.apk new file mode 100644 index 0000000..1f2209e Binary files /dev/null and b/app/release/Alcoholic_v1.0_release_1123.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..8c56de9 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.example.alcoholic", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "properties": [], + "versionCode": 10, + "versionName": "1.0", + "enabled": true, + "outputFile": "Alcoholic_v1.0_release_1123.apk" + } + ] +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/alcoholic/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/alcoholic/ExampleInstrumentedTest.java new file mode 100644 index 0000000..645c2f9 --- /dev/null +++ b/app/src/androidTest/java/com/example/alcoholic/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.alcoholic; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.alcoholic", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fde8b8b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/action/StatusAction.java b/app/src/main/java/com/example/alcoholic/action/StatusAction.java new file mode 100644 index 0000000..77c4e7b --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/action/StatusAction.java @@ -0,0 +1,96 @@ +package com.example.alcoholic.action; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.view.View; + +import com.example.alcoholic.R; +import com.example.alcoholic.widget.HintLayout; + +import androidx.annotation.DrawableRes; +import androidx.annotation.RawRes; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/08 + * desc : 界面状态提示 + */ +public interface StatusAction { + + /** + * 获取提示布局 + */ + HintLayout getHintLayout(); + + /** + * 显示加载中 + */ + default void showLoading() { +// showLoading(R.raw.loading); + } + + default void showLoading(@RawRes int id) { + HintLayout layout = getHintLayout(); + layout.show(); + layout.setAnim(id); + layout.setHint(""); + layout.setOnClickListener(null); + } + + /** + * 显示加载完成 + */ + default void showComplete() { + HintLayout layout = getHintLayout(); + if (layout != null && layout.isShow()) { + layout.hide(); + } + } + + /** + * 显示空提示 + */ + default void showEmpty() { + showLayout(R.drawable.hint_empty_ic, R.string.hint_layout_no_data, null); + } + + /** + * 显示错误提示 + */ + default void showError(View.OnClickListener listener) { + HintLayout layout = getHintLayout(); + Context context = layout.getContext(); + ConnectivityManager manager = ContextCompat.getSystemService(context, ConnectivityManager.class); + if (manager != null) { + NetworkInfo info = manager.getActiveNetworkInfo(); + // 判断网络是否连接 + if (info == null || !info.isConnected()) { + showLayout(R.drawable.hint_nerwork_ic, R.string.hint_layout_error_network, listener); + return; + } + } + showLayout(R.drawable.hint_error_ic, R.string.hint_layout_error_request, listener); + } + + /** + * 显示自定义提示 + */ + default void showLayout(@DrawableRes int drawableId, @StringRes int stringId, View.OnClickListener listener) { + HintLayout layout = getHintLayout(); + Context context = layout.getContext(); + showLayout(ContextCompat.getDrawable(context, drawableId), context.getString(stringId), listener); + } + + default void showLayout(Drawable drawable, CharSequence hint, View.OnClickListener listener) { + HintLayout layout = getHintLayout(); + layout.show(); + layout.setIcon(drawable); + layout.setHint(hint); + layout.setOnClickListener(listener); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/action/SwipeAction.java b/app/src/main/java/com/example/alcoholic/action/SwipeAction.java new file mode 100644 index 0000000..d836ba1 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/action/SwipeAction.java @@ -0,0 +1,18 @@ +package com.example.alcoholic.action; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/08 + * desc : 侧滑意图 + */ +public interface SwipeAction { + + /** + * 是否使用侧滑 + */ + default boolean isSwipeEnable() { + // 默认开启 + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/action/TitleBarAction.java b/app/src/main/java/com/example/alcoholic/action/TitleBarAction.java new file mode 100644 index 0000000..461afda --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/action/TitleBarAction.java @@ -0,0 +1,173 @@ +package com.example.alcoholic.action; + +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.ViewGroup; + +import com.hjq.bar.OnTitleBarListener; +import com.hjq.bar.TitleBar; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/08 + * desc : 标题栏意图 + */ +public interface TitleBarAction extends OnTitleBarListener { + + @Nullable + TitleBar getTitleBar(); + + /** + * 左项被点击 + * + * @param v 被点击的左项View + */ + @Override + default void onLeftClick(View v) {} + + /** + * 标题被点击 + * + * @param v 被点击的标题View + */ + @Override + default void onTitleClick(View v){} + + /** + * 右项被点击 + * + * @param v 被点击的右项View + */ + @Override + default void onRightClick(View v) {} + + /** + * 设置标题栏的标题 + */ + default void setTitle(@StringRes int id) { + if (getTitleBar() != null) { + setTitle(getTitleBar().getResources().getString(id)); + } + } + + /** + * 设置标题栏的标题 + */ + default void setTitle(CharSequence title) { + if (getTitleBar() != null) { + getTitleBar().setTitle(title); + } + } + + /** + * 设置标题栏的左标题 + */ + default void setLeftTitle(int id) { + if (getTitleBar() != null) { + getTitleBar().setLeftTitle(id); + } + } + + default void setLeftTitle(CharSequence text) { + if (getTitleBar() != null) { + getTitleBar().setLeftTitle(text); + } + } + + default CharSequence getLeftTitle() { + if (getTitleBar() != null) { + return getTitleBar().getLeftTitle(); + } + return ""; + } + + /** + * 设置标题栏的右标题 + */ + default void setRightTitle(int id) { + if (getTitleBar() != null) { + getTitleBar().setRightTitle(id); + } + } + + default void setRightTitle(CharSequence text) { + if (getTitleBar() != null) { + getTitleBar().setRightTitle(text); + } + } + + default CharSequence getRightTitle() { + if (getTitleBar() != null) { + return getTitleBar().getRightTitle(); + } + return ""; + } + + /** + * 设置标题栏的左图标 + */ + default void setLeftIcon(int id) { + if (getTitleBar() != null) { + getTitleBar().setLeftIcon(id); + } + } + + default void setLeftIcon(Drawable drawable) { + if (getTitleBar() != null) { + getTitleBar().setLeftIcon(drawable); + } + } + + @Nullable + default Drawable getLeftIcon() { + if (getTitleBar() != null) { + return getTitleBar().getLeftIcon(); + } + return null; + } + + /** + * 设置标题栏的右图标 + */ + default void setRightIcon(int id) { + if (getTitleBar() != null) { + getTitleBar().setRightIcon(id); + } + } + + default void setRightIcon(Drawable drawable) { + if (getTitleBar() != null) { + getTitleBar().setRightIcon(drawable); + } + } + + @Nullable + default Drawable getRightIcon() { + if (getTitleBar() != null) { + return getTitleBar().getRightIcon(); + } + return null; + } + + /** + * 递归获取 ViewGroup 中的 TitleBar 对象 + */ + default TitleBar obtainTitleBar(ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + View view = group.getChildAt(i); + if ((view instanceof TitleBar)) { + return (TitleBar) view; + } else if (view instanceof ViewGroup) { + TitleBar titleBar = obtainTitleBar((ViewGroup) view); + if (titleBar != null) { + return titleBar; + } + } + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/action/ToastAction.java b/app/src/main/java/com/example/alcoholic/action/ToastAction.java new file mode 100644 index 0000000..f97d0eb --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/action/ToastAction.java @@ -0,0 +1,26 @@ +package com.example.alcoholic.action; + +import com.hjq.toast.ToastUtils; + +import androidx.annotation.StringRes; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/08 + * desc : 吐司意图 + */ +public interface ToastAction { + + default void toast(CharSequence text) { + ToastUtils.show(text); + } + + default void toast(@StringRes int id) { + ToastUtils.show(id); + } + + default void toast(Object object) { + ToastUtils.show(object); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/aop/CheckNet.java b/app/src/main/java/com/example/alcoholic/aop/CheckNet.java new file mode 100644 index 0000000..253fd96 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/aop/CheckNet.java @@ -0,0 +1,16 @@ +package com.example.alcoholic.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/01/11 + * desc : 检测网络注解 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CheckNet {} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/aop/CheckNetAspect.java b/app/src/main/java/com/example/alcoholic/aop/CheckNetAspect.java new file mode 100644 index 0000000..e7aa870 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/aop/CheckNetAspect.java @@ -0,0 +1,53 @@ +package com.example.alcoholic.aop; + +import android.app.Application; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import com.example.alcoholic.R; +import com.example.alcoholic.helper.ActivityStackManager; +import com.hjq.toast.ToastUtils; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; + +import androidx.core.content.ContextCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/01/11 + * desc : 网络检测 + */ +@Aspect +public class CheckNetAspect { + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.CheckNet * *(..))") + public void method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("method() && @annotation(checkNet)") + public void aroundJoinPoint(ProceedingJoinPoint joinPoint, CheckNet checkNet) throws Throwable { + Application application = ActivityStackManager.getInstance().getApplication(); + if (application != null) { + ConnectivityManager manager = ContextCompat.getSystemService(application, ConnectivityManager.class); + if (manager != null) { + NetworkInfo info = manager.getActiveNetworkInfo(); + // 判断网络是否连接 + if (info == null || !info.isConnected()) { + ToastUtils.show(R.string.common_network); + return; + } + } + } + //执行原方法 + joinPoint.proceed(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/aop/Permissions.java b/app/src/main/java/com/example/alcoholic/aop/Permissions.java new file mode 100644 index 0000000..edffedf --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/aop/Permissions.java @@ -0,0 +1,22 @@ +package com.example.alcoholic.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : 权限申请注解 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Permissions { + + /** + * 需要申请权限的集合 + */ + String[] value(); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/aop/PermissionsAspect.java b/app/src/main/java/com/example/alcoholic/aop/PermissionsAspect.java new file mode 100644 index 0000000..6bcd76a --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/aop/PermissionsAspect.java @@ -0,0 +1,70 @@ +package com.example.alcoholic.aop; + +import android.app.Activity; + +import com.example.alcoholic.R; +import com.example.alcoholic.helper.ActivityStackManager; +import com.hjq.permissions.OnPermission; +import com.hjq.permissions.XXPermissions; +import com.hjq.toast.ToastUtils; + + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; + +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : 权限申请处理 + */ +@Aspect +public class PermissionsAspect { + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.Permissions * *(..))") + public void method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("method() && @annotation(permissions)") + public void aroundJoinPoint(final ProceedingJoinPoint joinPoint, Permissions permissions) { + Activity activity = ActivityStackManager.getInstance().getTopActivity(); + if (activity == null || activity.isFinishing() || activity.isDestroyed()) { + return; + } + XXPermissions.with(activity) + .permission(permissions.value()) + .request(new OnPermission() { + + @Override + public void hasPermission(List granted, boolean all) { + if (all) { + try { + // 获得权限,执行原方法 + joinPoint.proceed(); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + @Override + public void noPermission(List denied, boolean quick) { + if (quick) { + ToastUtils.show(R.string.common_permission_fail); + XXPermissions.startPermissionActivity(activity, false); + } else { + ToastUtils.show(R.string.common_permission_hint); + } + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/bean/AnnouncementBean.java b/app/src/main/java/com/example/alcoholic/bean/AnnouncementBean.java new file mode 100644 index 0000000..e49c2c3 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/AnnouncementBean.java @@ -0,0 +1,18 @@ +package com.example.alcoholic.bean; + +/** + * Created by + * Description:群公告加密类 + * on 2020/11/20. + */ +public class AnnouncementBean { + private String Announcement; + + public String getAnnouncement() { + return Announcement; + } + + public void setAnnouncement(String announcement) { + Announcement = announcement; + } +} diff --git a/app/src/main/java/com/example/alcoholic/bean/ContactListenerBean.java b/app/src/main/java/com/example/alcoholic/bean/ContactListenerBean.java new file mode 100644 index 0000000..524be39 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/ContactListenerBean.java @@ -0,0 +1,33 @@ +package com.example.alcoholic.bean; + +/** + * Created by + * Description: 联系人变化- 用于eventbus通知页面有新联系人变化 + * on 2020/11/19. + */ +public class ContactListenerBean { + private boolean isFriend; + private String username; + + + public ContactListenerBean(String username,boolean isFriend) { + this.username = username; + this.isFriend = isFriend; + } + + public boolean isFriend() { + return isFriend; + } + + public void setFriend(boolean friend) { + isFriend = friend; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/app/src/main/java/com/example/alcoholic/bean/ContactsBean.java b/app/src/main/java/com/example/alcoholic/bean/ContactsBean.java new file mode 100644 index 0000000..fbd7734 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/ContactsBean.java @@ -0,0 +1,101 @@ +package com.example.alcoholic.bean; + +import org.greenrobot.greendao.annotation.Entity; +import org.greenrobot.greendao.annotation.Id; +import org.greenrobot.greendao.annotation.Unique; +import org.greenrobot.greendao.annotation.Generated; + +/** + * Created by + * Description: 联系人实体类 + * on 2020/11/18. + */ +@Entity +public class ContactsBean { + /* 数据库id */ + @Id(autoincrement = true) + private Long id; + /* 标识,代表关系唯一 */ + @Unique + private String contactsTag; + /* 主体用户/属 */ + private String attribute; + /* 联系人 */ + private String contactsAccount; + /* 备注 */ + private String codeName; + /* 是否是好友 */ + private boolean isFriend; + /* 是否屏蔽 */ + private boolean isShield; + /* 是否拉黑 */ + private boolean isBlack; + @Generated(hash = 147408448) + public ContactsBean(Long id, String contactsTag, String attribute, + String contactsAccount, String codeName, boolean isFriend, + boolean isShield, boolean isBlack) { + this.id = id; + this.contactsTag = contactsTag; + this.attribute = attribute; + this.contactsAccount = contactsAccount; + this.codeName = codeName; + this.isFriend = isFriend; + this.isShield = isShield; + this.isBlack = isBlack; + } + @Generated(hash = 747317112) + public ContactsBean() { + } + public Long getId() { + return this.id; + } + public void setId(Long id) { + this.id = id; + } + public String getContactsTag() { + return this.contactsTag; + } + public void setContactsTag(String contactsTag) { + this.contactsTag = contactsTag; + } + public String getAttribute() { + return this.attribute; + } + public void setAttribute(String attribute) { + this.attribute = attribute; + } + public String getContactsAccount() { + return this.contactsAccount; + } + public void setContactsAccount(String contactsAccount) { + this.contactsAccount = contactsAccount; + } + public String getCodeName() { + return this.codeName; + } + public void setCodeName(String codeName) { + this.codeName = codeName; + } + public boolean getIsFriend() { + return this.isFriend; + } + public void setIsFriend(boolean isFriend) { + this.isFriend = isFriend; + } + public boolean getIsShield() { + return this.isShield; + } + public void setIsShield(boolean isShield) { + this.isShield = isShield; + } + public boolean getIsBlack() { + return this.isBlack; + } + public void setIsBlack(boolean isBlack) { + this.isBlack = isBlack; + } + + + + +} diff --git a/app/src/main/java/com/example/alcoholic/bean/GroupDescriptionBean.java b/app/src/main/java/com/example/alcoholic/bean/GroupDescriptionBean.java new file mode 100644 index 0000000..44a3cbc --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/GroupDescriptionBean.java @@ -0,0 +1,36 @@ +package com.example.alcoholic.bean; + +/** + * Created by + * Description: 群组公告 + * on 2020/11/19. + */ +public class GroupDescriptionBean { + private String infoTag; + private String headImg; + private String intro; + + public String getInfoTag() { + return infoTag; + } + + public void setInfoTag(String infoTag) { + this.infoTag = infoTag; + } + + public String getHeadImg() { + return headImg; + } + + public void setHeadImg(String headImg) { + this.headImg = headImg; + } + + public String getIntro() { + return intro; + } + + public void setIntro(String intro) { + this.intro = intro; + } +} diff --git a/app/src/main/java/com/example/alcoholic/bean/MessageBean.java b/app/src/main/java/com/example/alcoholic/bean/MessageBean.java new file mode 100644 index 0000000..aa633f2 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/MessageBean.java @@ -0,0 +1,250 @@ +package com.example.alcoholic.bean; + +import org.greenrobot.greendao.annotation.Entity; +import org.greenrobot.greendao.annotation.Id; +import org.greenrobot.greendao.annotation.Unique; +import org.greenrobot.greendao.annotation.Generated; + +/** + * Created by 消息实体类,把他的消息再本地存一下 + * Description: + * on 2020/11/16. + */ +@Entity +public class MessageBean { + /* 数据库id */ + @Id(autoincrement = true) + private Long id; + /* 消息id */ + @Unique + private String msgId; + /* 消息主体用户/属 */ + private String attribute; + /* 消息对象 */ + private String otherSide; + /* 发送者信息 Tag */ + private String fromUserTag; + /* 发送者账户 */ + private String fromAccount; + /* 发送者昵称 */ + private String fromName; + /* 发送者头像 */ + private String fromHead; + /* 接收者信息tag */ + private String toUserTag; + /* 接收者账户 */ + private String toAccount; + /* 接收者昵称 */ + private String toName; + /* 接收者头像 */ + private String toHead; + /* 消息内容 */ + private String msgTextContent; + /* 消息图片内容 */ + private String msgImgContent; + /* 消息时间 */ + private long msgTime; + /* 消息被读的人数 */ + private int groupAckCount; + /* 聊天类型 */ + private int chatType; + /* 消息类型 */ + private int msgType; + /* 消息状态 */ + private int status; + /* 方向 */ + private int direction; + + @Generated(hash = 1294650196) + public MessageBean(Long id, String msgId, String attribute, String otherSide, + String fromUserTag, String fromAccount, String fromName, + String fromHead, String toUserTag, String toAccount, String toName, + String toHead, String msgTextContent, String msgImgContent, + long msgTime, int groupAckCount, int chatType, int msgType, int status, + int direction) { + this.id = id; + this.msgId = msgId; + this.attribute = attribute; + this.otherSide = otherSide; + this.fromUserTag = fromUserTag; + this.fromAccount = fromAccount; + this.fromName = fromName; + this.fromHead = fromHead; + this.toUserTag = toUserTag; + this.toAccount = toAccount; + this.toName = toName; + this.toHead = toHead; + this.msgTextContent = msgTextContent; + this.msgImgContent = msgImgContent; + this.msgTime = msgTime; + this.groupAckCount = groupAckCount; + this.chatType = chatType; + this.msgType = msgType; + this.status = status; + this.direction = direction; + } + + @Generated(hash = 1588632019) + public MessageBean() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute) { + this.attribute = attribute; + } + + public String getOtherSide() { + return otherSide; + } + + public void setOtherSide(String otherSide) { + this.otherSide = otherSide; + } + + public String getFromUserTag() { + return fromUserTag; + } + + public void setFromUserTag(String fromUserTag) { + this.fromUserTag = fromUserTag; + } + + public String getFromAccount() { + return fromAccount; + } + + public void setFromAccount(String fromAccount) { + this.fromAccount = fromAccount; + } + + public String getFromName() { + return fromName; + } + + public void setFromName(String fromName) { + this.fromName = fromName; + } + + public String getFromHead() { + return fromHead; + } + + public void setFromHead(String fromHead) { + this.fromHead = fromHead; + } + + public String getToUserTag() { + return toUserTag; + } + + public void setToUserTag(String toUserTag) { + this.toUserTag = toUserTag; + } + + public String getToAccount() { + return toAccount; + } + + public void setToAccount(String toAccount) { + this.toAccount = toAccount; + } + + public String getToName() { + return toName; + } + + public void setToName(String toName) { + this.toName = toName; + } + + public String getToHead() { + return toHead; + } + + public void setToHead(String toHead) { + this.toHead = toHead; + } + + public String getMsgTextContent() { + return msgTextContent; + } + + public void setMsgTextContent(String msgTextContent) { + this.msgTextContent = msgTextContent; + } + + public String getMsgImgContent() { + return msgImgContent; + } + + public void setMsgImgContent(String msgImgContent) { + this.msgImgContent = msgImgContent; + } + + public long getMsgTime() { + return msgTime; + } + + public void setMsgTime(long msgTime) { + this.msgTime = msgTime; + } + + public int getGroupAckCount() { + return groupAckCount; + } + + public void setGroupAckCount(int groupAckCount) { + this.groupAckCount = groupAckCount; + } + + public int getChatType() { + return chatType; + } + + public void setChatType(int chatType) { + this.chatType = chatType; + } + + public int getMsgType() { + return msgType; + } + + public void setMsgType(int msgType) { + this.msgType = msgType; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getDirection() { + return direction; + } + + public void setDirection(int direction) { + this.direction = direction; + } +} diff --git a/app/src/main/java/com/example/alcoholic/bean/MessageEncryptBean.java b/app/src/main/java/com/example/alcoholic/bean/MessageEncryptBean.java new file mode 100644 index 0000000..1797619 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/MessageEncryptBean.java @@ -0,0 +1,69 @@ +package com.example.alcoholic.bean; + +/** + * Created by + * Description:消息体-加密处理 + * on 2020/11/16. + */ +public class MessageEncryptBean { + /* 用户信息 TAG */ + private String userTag; + /* 用户昵称 */ + private String userName; + /* 用户头像 */ + private String userHead; + /* 用户个签 */ + private String userIntro; + /* 文字消息内容 */ + private String textMsgContent; + /* 图片消息内容 */ + private String imgMsgContent; + + public String getUserTag() { + return userTag; + } + + public void setUserTag(String userTag) { + this.userTag = userTag; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getUserHead() { + return userHead; + } + + public void setUserHead(String userHead) { + this.userHead = userHead; + } + + public String getUserIntro() { + return userIntro; + } + + public void setUserIntro(String userIntro) { + this.userIntro = userIntro; + } + + public String getTextMsgContent() { + return textMsgContent; + } + + public void setTextMsgContent(String textMsgContent) { + this.textMsgContent = textMsgContent; + } + + public String getImgMsgContent() { + return imgMsgContent; + } + + public void setImgMsgContent(String imgMsgContent) { + this.imgMsgContent = imgMsgContent; + } +} diff --git a/app/src/main/java/com/example/alcoholic/bean/UpVersionBean.java b/app/src/main/java/com/example/alcoholic/bean/UpVersionBean.java new file mode 100644 index 0000000..847629b --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/UpVersionBean.java @@ -0,0 +1,70 @@ +package com.example.alcoholic.bean; + +/** + * Created by + * Description:版本更新实体类 + * on 2020/11/25. + */ +public class UpVersionBean { + + /*** + * { + * "minMustUpCode":1, + * "vAPKDownUrl":"www.www", + * "vCode":1, + * "vName":"1.0", + * "vUpContent":"更心新的版本" + * } + */ + + /* 版本名称 */ + private String vName; + /* 版本号 */ + private int vCode; + /* 最小强制更新版本 */ + private int minMustUpCode; + /* apk下栽链接 */ + private String vAPKDownUrl; + /* 更新说明 */ + private String vUpContent; + + public String getvName() { + return vName; + } + + public void setvName(String vName) { + this.vName = vName; + } + + public int getvCode() { + return vCode; + } + + public void setvCode(int vCode) { + this.vCode = vCode; + } + + public int getMinMustUpCode() { + return minMustUpCode; + } + + public void setMinMustUpCode(int minMustUpCode) { + this.minMustUpCode = minMustUpCode; + } + + public String getvAPKDownUrl() { + return vAPKDownUrl; + } + + public void setvAPKDownUrl(String vAPKDownUrl) { + this.vAPKDownUrl = vAPKDownUrl; + } + + public String getvUpContent() { + return vUpContent; + } + + public void setvUpContent(String vUpContent) { + this.vUpContent = vUpContent; + } +} diff --git a/app/src/main/java/com/example/alcoholic/bean/UserInfoBean.java b/app/src/main/java/com/example/alcoholic/bean/UserInfoBean.java new file mode 100644 index 0000000..fdc20a4 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/bean/UserInfoBean.java @@ -0,0 +1,101 @@ +package com.example.alcoholic.bean; + +import org.greenrobot.greendao.annotation.Entity; +import org.greenrobot.greendao.annotation.Id; +import org.greenrobot.greendao.annotation.Unique; +import org.greenrobot.greendao.annotation.Generated; + +/** + * Created by + * Description:用户信息实体嘞 + * on 2020/11/18. + */ +@Entity +public class UserInfoBean { + /* 数据库id */ + @Id(autoincrement = true) + private Long id; + /* 用户 Tag */ + private String userTag; + /* 用户账户 */ + @Unique + private String userAccount; + /* 用户昵称 */ + private String userName; + /* 用户头像 */ + private String userHead; + /* 用户签名 */ + private String userIntro; + /* 用户聊天背景 */ + private String chatBgImg; + /* 用户是否是群组 */ + private boolean isGroup; + @Generated(hash = 1985144718) + public UserInfoBean(Long id, String userTag, String userAccount, + String userName, String userHead, String userIntro, String chatBgImg, + boolean isGroup) { + this.id = id; + this.userTag = userTag; + this.userAccount = userAccount; + this.userName = userName; + this.userHead = userHead; + this.userIntro = userIntro; + this.chatBgImg = chatBgImg; + this.isGroup = isGroup; + } + @Generated(hash = 1818808915) + public UserInfoBean() { + } + public Long getId() { + return this.id; + } + public void setId(Long id) { + this.id = id; + } + public String getUserTag() { + return this.userTag; + } + public void setUserTag(String userTag) { + this.userTag = userTag; + } + public String getUserAccount() { + return this.userAccount; + } + public void setUserAccount(String userAccount) { + this.userAccount = userAccount; + } + public String getUserName() { + return this.userName; + } + public void setUserName(String userName) { + this.userName = userName; + } + public String getUserHead() { + return this.userHead; + } + public void setUserHead(String userHead) { + this.userHead = userHead; + } + public String getUserIntro() { + return this.userIntro; + } + public void setUserIntro(String userIntro) { + this.userIntro = userIntro; + } + public String getChatBgImg() { + return this.chatBgImg; + } + public void setChatBgImg(String chatBgImg) { + this.chatBgImg = chatBgImg; + } + public boolean getIsGroup() { + return this.isGroup; + } + public void setIsGroup(boolean isGroup) { + this.isGroup = isGroup; + } + + + + +} diff --git a/app/src/main/java/com/example/alcoholic/common/ImActivity.java b/app/src/main/java/com/example/alcoholic/common/ImActivity.java new file mode 100644 index 0000000..536308c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/common/ImActivity.java @@ -0,0 +1,58 @@ +package com.example.alcoholic.common; + +import com.example.alcoholic.bean.MessageBean; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +/** + * Created by + * Description: + * on 2020/11/16. + */ +public abstract class ImActivity extends MyActivity{ + + @Override + protected void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + } + + @Override + protected void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + } + + /** + * 获取到新消息 + */ + @Subscribe(threadMode = ThreadMode.MAIN) + public void onReceiveNewMessage(MessageBean newMsgBean) { + onReNewMsg(newMsgBean); + }; + + + /** + * 获取布局 ID + */ + protected abstract int getLayoutId(); + + /** + * 初始化控件 + */ + protected abstract void initView(); + + /** + * 初始化数据 + */ + protected abstract void initData(); + + /** + * 获取到新消息 + */ + protected abstract void onReNewMsg(MessageBean newMsgBean); + + +} diff --git a/app/src/main/java/com/example/alcoholic/common/MyActivity.java b/app/src/main/java/com/example/alcoholic/common/MyActivity.java new file mode 100644 index 0000000..00edb24 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/common/MyActivity.java @@ -0,0 +1,223 @@ +package com.example.alcoholic.common; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.View; + +import com.example.alcoholic.R; +import com.example.alcoholic.action.SwipeAction; +import com.example.alcoholic.action.TitleBarAction; +import com.example.alcoholic.action.ToastAction; +import com.example.alcoholic.http.model.HttpData; +import com.example.alcoholic.ui.dialog.WaitDialog; +import com.example.alcoholic.widget.HintLayout; +import com.example.base.BaseActivity; +import com.example.base.BaseDialog; +import com.gyf.immersionbar.ImmersionBar; +import com.hjq.bar.TitleBar; +import com.hjq.http.listener.OnHttpListener; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; +import okhttp3.Call; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 项目中的 Activity 基类 + */ +public abstract class MyActivity extends BaseActivity + implements ToastAction, TitleBarAction, + SwipeAction, OnHttpListener { + + /** 标题栏对象 */ + private TitleBar mTitleBar; + /** 状态栏沉浸 */ + private ImmersionBar mImmersionBar; + + /** 加载对话框 */ + private BaseDialog mDialog; + /** 对话框数量 */ + private int mDialogTotal; + + /** + * 当前加载对话框是否在显示中 + */ + public boolean isShowDialog() { + return mDialog != null && mDialog.isShowing(); + } + + /** + * 显示加载对话框 + */ + public void showDialog() { + mDialogTotal++; + postDelayed(() -> { + if (mDialogTotal > 0 && !isFinishing()) { + if (mDialog == null) { + mDialog = new WaitDialog.Builder(this) + .setCancelable(false) + .create(); + } + if (!mDialog.isShowing()) { + mDialog.show(); + } + } + }, 300); + } + + /** + * 隐藏加载对话框 + */ + public void hideDialog() { + if (mDialogTotal > 0) { + mDialogTotal--; + } + + if (mDialogTotal == 0 && mDialog != null && mDialog.isShowing() && !isFinishing()) { + mDialog.dismiss(); + } + } + + @Override + protected void initLayout() { + super.initLayout(); + + if (getTitleBar() != null) { + getTitleBar().setOnTitleBarListener(this); + } + + // 初始化沉浸式状态栏 + if (isStatusBarEnabled()) { + getStatusBarConfig().init(); + + // 设置标题栏沉浸 + if (getTitleBar() != null) { + ImmersionBar.setTitleBar(this, getTitleBar()); + } + } + } + + /** + * 是否使用沉浸式状态栏 + */ + protected boolean isStatusBarEnabled() { + return true; + } + + /** + * 状态栏字体深色模式 + */ + protected boolean isStatusBarDarkFont() { + return true; + } + + /** + * 初始化沉浸式状态栏 + */ + @NonNull + protected ImmersionBar createStatusBarConfig() { + return ImmersionBar.with(this) + // 默认状态栏字体颜色为黑色 + .statusBarDarkFont(isStatusBarDarkFont()); + } + + /** + * 获取状态栏沉浸的配置对象 + */ + @NonNull + public ImmersionBar getStatusBarConfig() { + if (mImmersionBar == null) { + mImmersionBar = createStatusBarConfig(); + } + return mImmersionBar; + } + + /** + * 设置标题栏的标题 + */ + @Override + public void setTitle(@StringRes int id) { + setTitle(getString(id)); + } + + /** + * 设置标题栏的标题 + */ + @Override + public void setTitle(CharSequence title) { + super.setTitle(title); + if (getTitleBar() != null) { + getTitleBar().setTitle(title); + } + } + + @Override + @Nullable + public TitleBar getTitleBar() { + if (mTitleBar == null) { + mTitleBar = obtainTitleBar(getContentView()); + } + return mTitleBar; + } + + @Override + public void onLeftClick(View v) { + onBackPressed(); + } + + @Override + public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + super.startActivityForResult(intent, requestCode, options); + overridePendingTransition(R.anim.right_in_activity, R.anim.right_out_activity); + } + + @Override + public void finish() { + super.finish(); + overridePendingTransition(R.anim.left_in_activity, R.anim.left_out_activity); + } + + /** + * {@link OnHttpListener} + */ + + @Override + public void onStart(Call call) { + showDialog(); + } + + @Override + public void onSucceed(Object result) { + if (result instanceof HttpData) { + toast(((HttpData) result).getMessage()); + } + } + + @Override + public void onFail(Exception e) { + toast(e.getMessage()); + } + + @Override + public void onEnd(Call call) { + hideDialog(); + } + + @Override + protected void onDestroy() { + if (isShowDialog()) { + hideDialog(); + } + mDialog = null; + super.onDestroy(); + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/common/MyAdapter.java b/app/src/main/java/com/example/alcoholic/common/MyAdapter.java new file mode 100644 index 0000000..f57042c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/common/MyAdapter.java @@ -0,0 +1,204 @@ +package com.example.alcoholic.common; + +import android.content.Context; +import android.view.View; + + +import com.example.base.BaseAdapter; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.IntRange; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/12/19 + * desc : 项目中 RecyclerView 适配器基类 + */ +public abstract class MyAdapter extends BaseAdapter { + + /** 列表数据 */ + private List mDataSet; + /** 当前列表的页码,默认为第一页,用于分页加载功能 */ + private int mPageNumber = 1; + /** 是否是最后一页,默认为false,用于分页加载功能 */ + private boolean mLastPage; + /** 标记对象 */ + private Object mTag; + + public MyAdapter(@NonNull Context context) { + super(context); + } + + @Override + public int getItemCount() { + return mDataSet == null ? 0 : mDataSet.size(); + } + + /** + * 设置新的数据 + */ + public void setData(@Nullable List data) { + mDataSet = data; + notifyDataSetChanged(); + } + + /** + * 获取当前数据 + */ + @Nullable + public List getData() { + return mDataSet; + } + + /** + * 追加一些数据 + */ + public void addData(List data) { + if (data == null || data.size() == 0) { + return; + } + + if (mDataSet == null || mDataSet.size() == 0) { + setData(data); + } else { + mDataSet.addAll(data); + notifyItemRangeInserted(mDataSet.size() - data.size(), data.size()); + } + } + + /** + * 清空当前数据 + */ + public void clearData() { + if (mDataSet == null || mDataSet.size() == 0) { + return; + } + + mDataSet.clear(); + notifyDataSetChanged(); + } + + /** + * 获取某个位置上的数据 + */ + public T getItem(@IntRange(from = 0) int position) { + return mDataSet.get(position); + } + + /** + * 更新某个位置上的数据 + */ + public void setItem(@IntRange(from = 0) int position, @NonNull T item) { + if (mDataSet == null) { + mDataSet = new ArrayList<>(); + } + mDataSet.set(position, item); + notifyItemChanged(position); + } + + /** + * 添加单条数据 + */ + public void addItem(@NonNull T item) { + if (mDataSet == null) { + mDataSet = new ArrayList<>(); + } + + addItem(mDataSet.size(), item); + } + + public void addItem(@IntRange(from = 0) int position, @NonNull T item) { + if (mDataSet == null) { + mDataSet = new ArrayList<>(); + } + + if (position < mDataSet.size()) { + mDataSet.add(position, item); + } else { + mDataSet.add(item); + position = mDataSet.size() - 1; + } + notifyItemInserted(position); + } + + /** + * 删除单条数据 + */ + public void removeItem(@NonNull T item) { + int index = mDataSet.indexOf(item); + if (index != -1) { + removeItem(index); + } + } + + public void removeItem(@IntRange(from = 0) int position) { + // 如果是在for循环删除后要记得i-- + mDataSet.remove(position); + // 告诉适配器删除数据的位置,会有动画效果 + notifyItemRemoved(position); + } + + /** + * 获取当前的页码 + */ + public int getPageNumber() { + return mPageNumber; + } + + /** + * 设置当前的页码 + */ + public void setPageNumber(@IntRange(from = 0)int pageNumber) { + mPageNumber = pageNumber; + } + + /** + * 当前是否为最后一页 + */ + public boolean isLastPage() { + return mLastPage; + } + + /** + * 设置是否为最后一页 + */ + public void setLastPage(boolean flag) { + mLastPage = flag; + } + + /** + * 获取标记 + */ + @Nullable + public Object getTag() { + return mTag; + } + + /** + * 设置标记 + */ + public void setTag(@NonNull Object tag) { + mTag = tag; + } + + public final class SimpleHolder extends ViewHolder { + + public SimpleHolder(@LayoutRes int id) { + super(id); + } + + public SimpleHolder(View itemView) { + super(itemView); + } + + @Override + public void onBindView(int position) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/common/MyApplication.java b/app/src/main/java/com/example/alcoholic/common/MyApplication.java new file mode 100644 index 0000000..ee15ce5 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/common/MyApplication.java @@ -0,0 +1,179 @@ +package com.example.alcoholic.common; + +import android.app.Application; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.widget.Toast; + +import com.example.alcoholic.R; +import com.example.alcoholic.db.DbMsgManager; +import com.example.alcoholic.db.DbUserManager; +import com.example.alcoholic.helper.ActivityStackManager; +import com.example.alcoholic.http.model.RequestHandler; +import com.example.alcoholic.http.server.ReleaseServer; +import com.example.alcoholic.other.AppConfig; +import com.hjq.bar.TitleBar; +import com.hjq.bar.style.TitleBarLightStyle; +import com.hjq.http.EasyConfig; +import com.hjq.http.config.IRequestServer; +import com.hjq.toast.ToastInterceptor; +import com.hjq.toast.ToastUtils; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMOptions; +import com.orhanobut.logger.AndroidLogAdapter; +import com.orhanobut.logger.Logger; +import com.scwang.smart.refresh.footer.ClassicsFooter; +import com.scwang.smart.refresh.header.ClassicsHeader; +import com.scwang.smart.refresh.layout.SmartRefreshLayout; +import com.tencent.mmkv.MMKV; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import okhttp3.OkHttpClient; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 项目中的 Application 基类 + */ +public final class MyApplication extends Application implements LifecycleOwner { + + private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); + + private static Application mApplication; + + @Override + public void onCreate() { + super.onCreate(); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mApplication = this; + initSdk(this); + initDb(); + } + + public static Application getApplication() { + return mApplication; + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mLifecycle; + } + + /** + * 初始化一些第三方框架 + */ + public static void initSdk(Application application) { + // 吐司工具类 + ToastUtils.init(application); + + //logger + Logger.addLogAdapter(new AndroidLogAdapter()); + + //mmkv + MMKV.initialize(application); + + // 设置 Toast 拦截器 + ToastUtils.setToastInterceptor(new ToastInterceptor() { + @Override + public boolean intercept(Toast toast, CharSequence text) { + boolean intercept = super.intercept(toast, text); + if (intercept) { + Log.e("Toast", "空 Toast"); + } else { + Log.i("Toast", text.toString()); + } + return intercept; + } + }); + + // 初始化标题栏全局样式 + TitleBar.initStyle(new TitleBarLightStyle(application) { + + @Override + public Drawable getBackground() { + return new ColorDrawable(ContextCompat.getColor(application, R.color.colorPrimary)); + } + + @Override + public Drawable getBackIcon() { + return getDrawable(R.drawable.arrows_left_ic); + } + }); + + // 本地异常捕捉 +// CrashHandler.register(application); + + // 友盟统计、登录、分享 SDK +// UmengClient.init(application); + + // Bugly 异常捕捉 +// CrashReport.initCrashReport(application, AppConfig.getBuglyId(), AppConfig.isDebug()); + + // 设置全局的 Header 构建器 + SmartRefreshLayout.setDefaultRefreshHeaderCreator((context, layout) -> new ClassicsHeader(context).setEnableLastTime(false)); + // 设置全局的 Footer 构建器 + SmartRefreshLayout.setDefaultRefreshFooterCreator((context, layout) -> new ClassicsFooter(context).setDrawableSize(20)); + + // Activity 栈管理初始化 + ActivityStackManager.getInstance().init(application); + + // 网络请求框架初始化 + IRequestServer server; + server = new ReleaseServer(); + + EasyConfig.with(new OkHttpClient()) + // 是否打印日志 + .setLogEnabled(AppConfig.isDebug()) + // 设置服务器配置 + .setServer(server) + // 设置请求处理策略 + .setHandler(new RequestHandler(application)) + // 设置请求重试次数 + .setRetryCount(1) + // 添加全局请求参数 + //.addParam("token", "6666666") + // 添加全局请求头 + //.addHeader("time", "20191030") + // 启用配置 + .into(); + + // Activity 侧滑返回 +// SmartSwipeBack.activitySlidingBack(application, activity -> { +// if (activity instanceof SwipeAction) { +// return ((SwipeAction) activity).isSwipeEnable(); +// } +// return true; +// }); + + + //im + int pid = android.os.Process.myPid(); + String processAppName = AppConfig.getAppName(application,pid); + // 如果APP启用了远程的service,此application:onCreate会被调用2次 + // 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次 + // 默认的APP会在以包名为默认的process name下运行,如果查到的process name不是APP的process name就立即返回 + if (!(processAppName == null ||!processAppName.equalsIgnoreCase(application.getPackageName()))) { + EMOptions options = new EMOptions(); + // 默认添加好友时,是不需要验证的,改成需要验证 + options.setAcceptInvitationAlways(false); + //初始化 + EMClient.getInstance().init(application, options); + //在做打包混淆时,关闭debug模式,避免消耗不必要的资源 + EMClient.getInstance().setDebugMode(true); + } + + } + + + public static void initDb(){ + DbMsgManager.getInstance(); + DbUserManager.getInstance(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/common/MyFragment.java b/app/src/main/java/com/example/alcoholic/common/MyFragment.java new file mode 100644 index 0000000..a1cc6b2 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/common/MyFragment.java @@ -0,0 +1,163 @@ +package com.example.alcoholic.common; + +import android.view.ViewGroup; + + +import com.example.alcoholic.action.TitleBarAction; +import com.example.alcoholic.action.ToastAction; +import com.example.alcoholic.http.model.HttpData; +import com.example.base.BaseFragment; +import com.gyf.immersionbar.ImmersionBar; +import com.hjq.bar.TitleBar; +import com.hjq.http.listener.OnHttpListener; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import okhttp3.Call; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 项目中 Fragment 懒加载基类 + */ +public abstract class MyFragment extends BaseFragment + implements ToastAction, TitleBarAction, OnHttpListener { + + /** 标题栏对象 */ + private TitleBar mTitleBar; + /** 状态栏沉浸 */ + private ImmersionBar mImmersionBar; + + @Override + protected void initFragment() { + super.initFragment(); + if (getTitleBar() != null) { + getTitleBar().setOnTitleBarListener(this); + } + + // 初始化沉浸式状态栏 + if (isStatusBarEnabled()) { + getStatusBarConfig().init(); + + // 设置标题栏沉浸 + if (getTitleBar() != null) { + ImmersionBar.setTitleBar(this, getTitleBar()); + } + } + } + + /** + * 是否在 Fragment 使用沉浸式 + */ + public boolean isStatusBarEnabled() { + return false; + } + + /** + * 获取状态栏沉浸的配置对象 + */ + @NonNull + protected ImmersionBar getStatusBarConfig() { + if (mImmersionBar == null) { + mImmersionBar = createStatusBarConfig(); + } + return mImmersionBar; + } + + /** + * 初始化沉浸式 + */ + @NonNull + protected ImmersionBar createStatusBarConfig() { + return ImmersionBar.with(this) + // 默认状态栏字体颜色为黑色 + .statusBarDarkFont(statusBarDarkFont()) + // 解决软键盘与底部输入框冲突问题,默认为false,还有一个重载方法,可以指定软键盘mode + .keyboardEnable(true); + } + + /** + * 获取状态栏字体颜色 + */ + protected boolean statusBarDarkFont() { + // 返回真表示黑色字体 + return true; + } + + @Override + @Nullable + public TitleBar getTitleBar() { + if (mTitleBar == null) { + mTitleBar = obtainTitleBar((ViewGroup) getView()); + } + return mTitleBar; + } + + /** + * 当前加载对话框是否在显示中 + */ + public boolean isShowDialog() { + A activity = getAttachActivity(); + if (activity != null) { + return activity.isShowDialog(); + } else { + return false; + } + } + + /** + * 显示加载对话框 + */ + public void showDialog() { + A activity = getAttachActivity(); + if (activity != null) { + activity.showDialog(); + } + } + + /** + * 隐藏加载对话框 + */ + public void hideDialog() { + A activity = getAttachActivity(); + if (activity != null) { + activity.hideDialog(); + } + } + + /** + * {@link OnHttpListener} + */ + + @Override + public void onStart(Call call) { + showDialog(); + } + + @Override + public void onSucceed(Object result) { + if (result instanceof HttpData) { + toast(((HttpData) result).getMessage()); + } + } + + @Override + public void onFail(Exception e) { + toast(e.getMessage()); + } + + @Override + public void onEnd(Call call) { + hideDialog(); + } + + @Override + public void onResume() { + super.onResume(); + if (isStatusBarEnabled()) { + // 重新初始化状态栏 + getStatusBarConfig().init(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/constant/Constants.java b/app/src/main/java/com/example/alcoholic/constant/Constants.java new file mode 100644 index 0000000..0382bf7 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/constant/Constants.java @@ -0,0 +1,14 @@ +package com.example.alcoholic.constant; + +/** + * Created by + * Description:常量存储类 + * on 2020/11/18. + */ +public class Constants { + + + /* 每页加载数量 */ + public static final int LOAD_LIMIT = 30; + +} diff --git a/app/src/main/java/com/example/alcoholic/constant/JumpDataContants.java b/app/src/main/java/com/example/alcoholic/constant/JumpDataContants.java new file mode 100644 index 0000000..d592af7 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/constant/JumpDataContants.java @@ -0,0 +1,30 @@ +package com.example.alcoholic.constant; + +/** + * Created by + * Description: 跳转的数据的常量 + * on 2020/11/18. + */ +public class JumpDataContants { + + + public static final String JUMP_DATA_USER_ACCOUNT = "jumpDataUserAccount"; + public static final String JUMP_DATA_CHAT_TYPE = "jumpDataChatType"; + public static final String JUMP_DATA_IS_OWNER = "jumpDataIsOwner"; + + + /** 索引 */ + public static final String INDEX = "index"; + /** 数量 */ + public static final String AMOUNT = "amount"; + + + /** 图片 */ + public static final String IMAGE = "picture"; + /** 视频 */ + public static final String VIDEO = "video"; + /** 文件 */ + public static final String FILE = "file"; + + +} diff --git a/app/src/main/java/com/example/alcoholic/constant/PConstant.java b/app/src/main/java/com/example/alcoholic/constant/PConstant.java new file mode 100644 index 0000000..d0f9af4 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/constant/PConstant.java @@ -0,0 +1,26 @@ +package com.example.alcoholic.constant; + +/** + * Created by + * Description: + * on 2020/11/17. + */ +public class PConstant { + + public static String fartQuotes(){ + + String content = ""; + + String[] quotes = new String[]{ + "对一个人的国家的爱是美好的事情——但为什么爱要止于国界。", + "我认识到,爱国主义是不够的,我还必须不对任何人心怀仇恨与抱怨。", + "不要过分的醉心放任自由,一点也不加以限制的自由,它的害处与危险实在不少。", + "爱国如饥渴。", + "向北望星提剑立,生命长为国家忧。", + }; + + + return quotes[2]; + } + +} diff --git a/app/src/main/java/com/example/alcoholic/constant/RandomConstant.java b/app/src/main/java/com/example/alcoholic/constant/RandomConstant.java new file mode 100644 index 0000000..6ba9024 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/constant/RandomConstant.java @@ -0,0 +1,26 @@ +package com.example.alcoholic.constant; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Created by + * Description: 随机常量类 + * on 2020/11/17. + */ +public class RandomConstant { + + + /** + * 随机生成头像 + * @return + */ + public static String randomHeadImg(){ + String[] heads = new String[]{ + "https://i.ibb.co/7X1LDkv/de-head-1.jpg", + "https://i.ibb.co/VHHn11n/de-head-2.jpg" + }; + int index = ThreadLocalRandom.current().nextInt(0, (heads.length-1)); + return heads[index]; + } + +} diff --git a/app/src/main/java/com/example/alcoholic/db/DbContactsHelper.java b/app/src/main/java/com/example/alcoholic/db/DbContactsHelper.java new file mode 100644 index 0000000..e20184c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/db/DbContactsHelper.java @@ -0,0 +1,113 @@ +package com.example.alcoholic.db; + +import com.example.alcoholic.bean.ContactsBean; +import com.example.alcoholic.gen.ContactsBeanDao; +import com.example.alcoholic.utils.MMKVUserUtils; + +import java.util.List; + +/** + * Created by + * Description: + * on 2020/11/18. + */ +public class DbContactsHelper { + + /** + * 创建一个新的联系人 + */ + public static ContactsBean createContact(String account,boolean isfriend){ + ContactsBean contactsBean = new ContactsBean(); + contactsBean.setAttribute(MMKVUserUtils.getInstance().getUserAccount()); + contactsBean.setContactsAccount(account); + contactsBean.setCodeName(""); + contactsBean.setIsFriend(isfriend); + contactsBean.setIsShield(false); + contactsBean.setIsBlack(false); + contactsBean.setContactsTag(contactsBean.getAttribute()+"-"+contactsBean.getContactsAccount()); + + return contactsBean; + } + + /** + * 插入一条记录 + */ + public static void insetContacts(ContactsBean contactsBean){ + DbContactsManager.getInstance().getmDaoSession().insertOrReplace(contactsBean); + } + + + /** + * 查询所有联系人 + */ + public static List queryContactsAll(){ + return DbContactsManager.getInstance().getmDaoSession().queryBuilder(ContactsBean.class) + .where(ContactsBeanDao.Properties.Attribute.eq(MMKVUserUtils.getInstance().getUserAccount())).build().list(); + } + + /** + * 查询联系人 - 根据用户账户 + */ + public static ContactsBean queryContacts2Account(String account){ + + List contactsBeans = DbContactsManager.getInstance().getmDaoSession() + .queryBuilder(ContactsBean.class) + .where(ContactsBeanDao.Properties.Attribute.eq(MMKVUserUtils.getInstance().getUserAccount()) + ,ContactsBeanDao.Properties.ContactsAccount.eq(account)).build().list(); + + if (contactsBeans.size() > 0){ + return contactsBeans.get(0); + }else { + return null; + } + + } + + /** + * 查询好友备注 + */ + public static String queryCodeName2Account(String account){ + List contactsBeans = DbContactsManager.getInstance().getmDaoSession() + .queryBuilder(ContactsBean.class) + .where(ContactsBeanDao.Properties.Attribute.eq(MMKVUserUtils.getInstance().getUserAccount()) + ,ContactsBeanDao.Properties.ContactsAccount.eq(account)).build().list(); + + if (contactsBeans.size() > 0){ + return contactsBeans.get(0).getCodeName(); + }else { + return ""; + } + } + + /** + * 修改好友备注 + */ + public static void updateUserCodeName(String account,String codeName){ + ContactsBean contactsBean = queryContacts2Account(account); + if (contactsBean == null){ + contactsBean = createContact(account,false); + } + + contactsBean.setCodeName(codeName); + DbContactsManager.getInstance().getmDaoSession().update(contactsBean); + + } + + /** + * 根据用户id修改好友状态 + */ + public static void upDateAccount2Friend(String account,boolean isfriend){ + ContactsBean contactsBean = queryContacts2Account(account); + + if (contactsBean != null){ + contactsBean.setIsFriend(isfriend); + }else { + contactsBean = createContact(account,isfriend); + } + + DbContactsManager.getInstance().getmDaoSession().insertOrReplace(contactsBean); + + + } + +} diff --git a/app/src/main/java/com/example/alcoholic/db/DbContactsManager.java b/app/src/main/java/com/example/alcoholic/db/DbContactsManager.java new file mode 100644 index 0000000..0528f3c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/db/DbContactsManager.java @@ -0,0 +1,59 @@ +package com.example.alcoholic.db; + +import com.example.alcoholic.common.MyApplication; +import com.example.alcoholic.gen.DaoMaster; +import com.example.alcoholic.gen.DaoSession; + +/** + * Created by + * Description:联系人关系管理库 + * on 2020/11/18. + */ +public class DbContactsManager { + private static final String db_name = "Contacts_db"; + + private DaoMaster mDaoMaster; + private DaoSession mDaoSession; + + public DbContactsManager() { + init(); + } + + /** + * 静态内部类,实例化对象使用 + */ + private static class SingleInstanceHolder { + private static final DbContactsManager INSTANCE = new DbContactsManager(); + } + + /** + * 对外唯一实例的接口 + * + * @return + */ + public static DbContactsManager getInstance() { + return DbContactsManager.SingleInstanceHolder.INSTANCE; + } + + /** + * 初始化数据 + */ + private void init() { + DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(MyApplication.getApplication(), + db_name); + mDaoMaster = new DaoMaster(devOpenHelper.getWritableDatabase()); + mDaoSession = mDaoMaster.newSession(); + } + + public DaoMaster getmDaoMaster() { + return mDaoMaster; + } + public DaoSession getmDaoSession() { + return mDaoSession; + } + public DaoSession getNewSession() { + mDaoSession = mDaoMaster.newSession(); + return mDaoSession; + } + +} diff --git a/app/src/main/java/com/example/alcoholic/db/DbMsgHelper.java b/app/src/main/java/com/example/alcoholic/db/DbMsgHelper.java new file mode 100644 index 0000000..bd3037b --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/db/DbMsgHelper.java @@ -0,0 +1,87 @@ +package com.example.alcoholic.db; + +import android.database.Cursor; + +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.constant.Constants; +import com.example.alcoholic.gen.MessageBeanDao; +import com.example.alcoholic.im.ImBeanChangeHelper; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.orhanobut.logger.Logger; +import com.tencent.mmkv.MMKV; + +import java.util.Collections; +import java.util.List; + +/** + * Created by + * Description: 消息数据库帮助类 + * on 2020/11/17. + */ +public class DbMsgHelper { + + /** + * 插入一条消息 + */ + public static void insertMsg(MessageBean msgBean){ + DbMsgManager.getInstance().getmDaoSession().insertOrReplace(msgBean); + } + + + /** + * 查询消息 - 所有 + */ + public static List queryMsgAll(){ + return DbMsgManager.getInstance().getmDaoSession().loadAll(MessageBean.class); + } + + /** + * 查询消息 - 根据用户账户 + */ + public static List queryMsg2User(int pageNum,String userAccount){ + List messageBeans; + + messageBeans = DbMsgManager.getInstance() + .getmDaoSession() + .queryBuilder(MessageBean.class) +// .whereOr(MessageBeanDao.Properties.Attribute.eq(userAccount),MessageBeanDao.Properties.FromAccount.eq(userAccount)) + .where(MessageBeanDao.Properties.Attribute.eq(MMKVUserUtils.getInstance().getUserAccount()) + ,MessageBeanDao.Properties.OtherSide.eq(userAccount)) + .orderDesc(MessageBeanDao.Properties.MsgTime) + .limit(Constants.LOAD_LIMIT) + .offset(pageNum*Constants.LOAD_LIMIT) + .build() + .list(); + + + Collections.reverse(messageBeans); + return messageBeans; + } + + /** + * 查询会话列表 + */ + public static List queryChat2Me(){ + List messageBeans; + + Cursor cursor = DbMsgManager.getInstance().getmDaoSession().getDatabase().rawQuery("SELECT * FROM " + +MessageBeanDao.TABLENAME + " where (" + +MessageBeanDao.Properties.OtherSide.columnName + "," + +MessageBeanDao.Properties.MsgTime.columnName + ") in (SELECT " + +MessageBeanDao.Properties.OtherSide.columnName + ",max(" + +MessageBeanDao.Properties.MsgTime.columnName+") FROM " + +MessageBeanDao.TABLENAME + " WHERE " + +MessageBeanDao.Properties.Attribute.columnName +" ='" + +MMKVUserUtils.getInstance().getUserAccount() + "' group by " + +MessageBeanDao.Properties.OtherSide.columnName + ") group by " + +MessageBeanDao.Properties.OtherSide.columnName + " order by " + +MessageBeanDao.Properties.MsgTime.columnName + " desc",null); + + + messageBeans = ImBeanChangeHelper.cursorMsgToList(cursor,MessageBeanDao.Properties.class); + + + return messageBeans; + } + +} diff --git a/app/src/main/java/com/example/alcoholic/db/DbMsgManager.java b/app/src/main/java/com/example/alcoholic/db/DbMsgManager.java new file mode 100644 index 0000000..d622579 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/db/DbMsgManager.java @@ -0,0 +1,59 @@ +package com.example.alcoholic.db; + +import com.example.alcoholic.common.MyApplication; +import com.example.alcoholic.gen.DaoMaster; +import com.example.alcoholic.gen.DaoSession; + +/** + * Created by + * Description: 消息存库管理类 + * on 2020/11/17. + */ +public class DbMsgManager { + private static final String db_name = "message_db"; + + private DaoMaster mDaoMaster; + private DaoSession mDaoSession; + + public DbMsgManager() { + init(); + } + + /** + * 静态内部类,实例化对象使用 + */ + private static class SingleInstanceHolder { + private static final DbMsgManager INSTANCE = new DbMsgManager(); + } + + /** + * 对外唯一实例的接口 + * + * @return + */ + public static DbMsgManager getInstance() { + return SingleInstanceHolder.INSTANCE; + } + + /** + * 初始化数据 + */ + private void init() { + DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(MyApplication.getApplication(), + db_name); + mDaoMaster = new DaoMaster(devOpenHelper.getWritableDatabase()); + mDaoSession = mDaoMaster.newSession(); + } + + public DaoMaster getmDaoMaster() { + return mDaoMaster; + } + public DaoSession getmDaoSession() { + return mDaoSession; + } + public DaoSession getNewSession() { + mDaoSession = mDaoMaster.newSession(); + return mDaoSession; + } + +} diff --git a/app/src/main/java/com/example/alcoholic/db/DbUserHelper.java b/app/src/main/java/com/example/alcoholic/db/DbUserHelper.java new file mode 100644 index 0000000..d4b5044 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/db/DbUserHelper.java @@ -0,0 +1,233 @@ +package com.example.alcoholic.db; + +import android.database.sqlite.SQLiteException; +import android.service.autofill.UserData; + +import com.example.alcoholic.bean.ContactsBean; +import com.example.alcoholic.bean.GroupDescriptionBean; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.constant.RandomConstant; +import com.example.alcoholic.gen.UserInfoBeanDao; +import com.example.alcoholic.utils.AESUtils; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.google.gson.Gson; +import com.hyphenate.EMValueCallBack; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMGroup; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; +import com.tencent.mmkv.MMKV; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +/** + * Created by + * Description:用户信息库操作类 + * on 2020/11/18. + */ +public class DbUserHelper { + + /** + * 更新用户Tag + */ + public static void reUserTag(String account){ + UserInfoBean userInfoBean = DbUserManager.getInstance() + .getmDaoSession() + .queryBuilder(UserInfoBean.class) + .where(UserInfoBeanDao.Properties.UserAccount.eq(account)) + .build().list().get(0); + + userInfoBean.setUserTag(MMKVUserUtils.getInstance().getUserInfoTag()); + + DbUserManager.getInstance().getmDaoSession().update(userInfoBean); + + } + + /** + * 更新当前用户信息 + */ + public static void reUserInfo(){ + UserInfoBean userInfoBean = new UserInfoBean(); + userInfoBean.setUserTag(MMKVUserUtils.getInstance().getUserInfoTag()); + userInfoBean.setUserAccount(MMKVUserUtils.getInstance().getUserAccount()); + userInfoBean.setUserName(MMKVUserUtils.getInstance().getUserName()); + userInfoBean.setUserHead(MMKVUserUtils.getInstance().getUserHead()); + userInfoBean.setUserIntro(MMKVUserUtils.getInstance().getUserIntro()); + userInfoBean.setIsGroup(false); + DbUserManager.getInstance().getmDaoSession().insertOrReplace(userInfoBean); + + } + + /** + * 插入一条用户信息 + */ + public static void insertUser(UserInfoBean userInfoBean){ + DbUserManager.getInstance().getmDaoSession().insertOrReplace(userInfoBean); + } + + /** + * 用户登录时获取用户信息 + */ + public static UserInfoBean queryUserIsLogin(String account){ + //查找该用户信息,找到了就返回,找不到则生成新数据 + UserInfoBean userInfoBean = queryUser2Account(account); + if (userInfoBean == null){ + userInfoBean = new UserInfoBean(); + userInfoBean.setUserTag(""); + userInfoBean.setUserAccount(account); + userInfoBean.setUserName(account); + userInfoBean.setUserHead(RandomConstant.randomHeadImg()); + userInfoBean.setUserIntro(""); + insertUser(userInfoBean); + } + return userInfoBean; + } + + + + /** + * 根据用户账户,查询 + */ + public static UserInfoBean queryUser2Account(String account){ + + List userInfoBeans = DbUserManager.getInstance() + .getmDaoSession() + .queryBuilder(UserInfoBean.class) + .where(UserInfoBeanDao.Properties.UserAccount.eq(account)) + .build().list(); + + if (userInfoBeans.size() > 0){ + return userInfoBeans.get(0); + }else { + return null; + } + + } + + /** + * 根据用户账户,仅查询非群聊 + */ + public static UserInfoBean queryUserNoGroup2Account(String account){ + + List userInfoBeans = DbUserManager.getInstance() + .getmDaoSession() + .queryBuilder(UserInfoBean.class) + .where(UserInfoBeanDao.Properties.UserAccount.eq(account), UserInfoBeanDao.Properties.IsGroup.eq(false)) + .build().list(); + + if (userInfoBeans.size() > 0){ + return userInfoBeans.get(0); + }else { + return null; + } + + } + + /** + * 获取用户昵称 + */ + public static String getUserName(String account){ + UserInfoBean userInfoBean = queryUser2Account(account); + + if (userInfoBean != null){ + return userInfoBean.getUserName(); + }else { + return account; + } + } + + + /** + * 判断展示用户昵称还是备注 + */ + public static String getUserShowName(String account,String name){ + String codeName = DbContactsHelper.queryCodeName2Account(account); + + if (EmptyUtils.isNotEmpty(codeName)){ + return codeName; + }else { + return name; + } + + } + + + /** + * 根据群组id 获取群组数据 + */ + public static UserInfoBean queryGroupInfo2Id(String groupId){ + + List userInfoBeans = DbUserManager.getInstance() + .getmDaoSession() + .queryBuilder(UserInfoBean.class) + .where(UserInfoBeanDao.Properties.UserAccount.eq(groupId),UserInfoBeanDao.Properties.IsGroup.eq(true)) + .build().list(); + + UserInfoBean userInfoBean = null; + if (userInfoBeans.size() > 0 ){ + userInfoBean = userInfoBeans.get(0); + } + + if (userInfoBean == null || EmptyUtils.isEmpty(userInfoBean.getUserTag().trim())){ + //根据群组ID从服务器获取群组基本信息 + try { + EMGroup group = EMClient.getInstance().groupManager().getGroupFromServer(groupId); + + userInfoBean = insertGroup2Bean(group); + } catch (Exception e) { + e.printStackTrace(); + } + } + + return userInfoBean; + } + + + /** + * 插入群组信息 + */ + public static UserInfoBean insertGroup2Bean(EMGroup group){ + + UserInfoBean userInfoBean = new UserInfoBean(); + + + String description; + try { + description = AESUtils.decrypt(MMKVStytemUtils.getInstance().getAesKey(),group.getDescription().replaceAll(" ","+")); + + GroupDescriptionBean groupDescription = new Gson().fromJson(description, GroupDescriptionBean.class); + + userInfoBean.setUserTag(groupDescription.getInfoTag()); + userInfoBean.setUserHead(groupDescription.getHeadImg()); + userInfoBean.setUserIntro(groupDescription.getIntro()); + + } catch (Exception e) { + userInfoBean.setUserTag(""); + userInfoBean.setUserHead(""); + userInfoBean.setUserIntro(""); + e.printStackTrace(); + } + + userInfoBean.setUserAccount(group.getGroupId()); + userInfoBean.setUserName(group.getGroupName()); + userInfoBean.setIsGroup(true); + + + insertUser(userInfoBean); + + + return userInfoBean; + + } + +} diff --git a/app/src/main/java/com/example/alcoholic/db/DbUserManager.java b/app/src/main/java/com/example/alcoholic/db/DbUserManager.java new file mode 100644 index 0000000..cc190a8 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/db/DbUserManager.java @@ -0,0 +1,60 @@ +package com.example.alcoholic.db; + +import com.example.alcoholic.common.MyApplication; +import com.example.alcoholic.gen.DaoMaster; +import com.example.alcoholic.gen.DaoSession; + +/** + * Created by + * Description:各种用户信息存储管理类 + * on 2020/11/18. + */ +public class DbUserManager { + private static final String db_name = "user_all_db"; + + private DaoMaster mDaoMaster; + private DaoSession mDaoSession; + + public DbUserManager() { + init(); + } + + /** + * 静态内部类,实例化对象使用 + */ + private static class SingleInstanceHolder { + private static final DbUserManager INSTANCE = new DbUserManager(); + } + + /** + * 对外唯一实例的接口 + * + * @return + */ + public static DbUserManager getInstance() { + return DbUserManager.SingleInstanceHolder.INSTANCE; + } + + /** + * 初始化数据 + */ + private void init() { + DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(MyApplication.getApplication(), + db_name); + mDaoMaster = new DaoMaster(devOpenHelper.getWritableDatabase()); + mDaoSession = mDaoMaster.newSession(); + } + + public DaoMaster getmDaoMaster() { + return mDaoMaster; + } + public DaoSession getmDaoSession() { + return mDaoSession; + } + public DaoSession getNewSession() { + mDaoSession = mDaoMaster.newSession(); + return mDaoSession; + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/helper/ActivityStackManager.java b/app/src/main/java/com/example/alcoholic/helper/ActivityStackManager.java new file mode 100644 index 0000000..303490c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/helper/ActivityStackManager.java @@ -0,0 +1,135 @@ +package com.example.alcoholic.helper; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.ArrayMap; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/11/18 + * desc : Activity 栈管理 + */ +public final class ActivityStackManager implements Application.ActivityLifecycleCallbacks { + + private static volatile ActivityStackManager sInstance; + + private final ArrayMap mActivitySet = new ArrayMap<>(); + + /** 当前应用上下文对象 */ + private Application mApplication; + /** 当前 Activity 对象标记 */ + private String mCurrentTag; + + private ActivityStackManager() {} + + public static ActivityStackManager getInstance() { + // 加入双重校验锁 + if(sInstance == null) { + synchronized (ActivityStackManager.class) { + if(sInstance == null){ + sInstance = new ActivityStackManager(); + } + } + } + return sInstance; + } + + public void init(Application application) { + mApplication = application; + application.registerActivityLifecycleCallbacks(this); + } + + /** + * 获取 Application 对象 + */ + public Application getApplication() { + return mApplication; + } + + /** + * 获取栈顶的 Activity + */ + public Activity getTopActivity() { + return mActivitySet.get(mCurrentTag); + } + + /** + * 销毁所有的 Activity + */ + public void finishAllActivities() { + finishAllActivities((Class) null); + } + + /** + * 销毁所有的 Activity,除这些 Class 之外的 Activity + */ + @SafeVarargs + public final void finishAllActivities(Class... classArray) { + String[] keys = mActivitySet.keySet().toArray(new String[]{}); + for (String key : keys) { + Activity activity = mActivitySet.get(key); + if (activity != null && !activity.isFinishing()) { + boolean whiteClazz = false; + if (classArray != null) { + for (Class clazz : classArray) { + if (activity.getClass() == clazz) { + whiteClazz = true; + } + } + } + // 如果不是白名单上面的 Activity 就销毁掉 + if (!whiteClazz) { + activity.finish(); + mActivitySet.remove(key); + } + } + } + } + + /** + * 获取一个对象的独立无二的标记 + */ + private static String getObjectTag(Object object) { + // 对象所在的包名 + 对象的内存地址 + return object.getClass().getName() + Integer.toHexString(object.hashCode()); + } + + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + mCurrentTag = getObjectTag(activity); + mActivitySet.put(getObjectTag(activity), activity); + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + mCurrentTag = getObjectTag(activity); + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + mCurrentTag = getObjectTag(activity); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) {} + + @Override + public void onActivityStopped(@NonNull Activity activity) {} + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + mActivitySet.remove(getObjectTag(activity)); + if (getObjectTag(activity).equals(mCurrentTag)) { + // 清除当前标记 + mCurrentTag = null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/helper/CacheDataManager.java b/app/src/main/java/com/example/alcoholic/helper/CacheDataManager.java new file mode 100644 index 0000000..805c9dc --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/helper/CacheDataManager.java @@ -0,0 +1,114 @@ +package com.example.alcoholic.helper; + +import android.content.Context; +import android.os.Environment; + +import java.io.File; +import java.math.BigDecimal; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/03/01 + * desc : 应用缓存管理 + */ +public final class CacheDataManager { + + /** + * 获取缓存大小 + */ + public static String getTotalCacheSize(Context context) { + long cacheSize = getFolderSize(context.getCacheDir()); + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + cacheSize += getFolderSize(context.getExternalCacheDir()); + } + return getFormatSize(cacheSize); + } + + /** + * 清除缓存 + */ + public static void clearAllCache(Context context) { + deleteDir(context.getCacheDir()); + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + deleteDir(context.getExternalCacheDir()); + } + } + + /** + * 删除文件夹 + */ + private static boolean deleteDir(File dir) { + if (dir != null) { + if (dir.isDirectory()) { + String[] children = dir.list(); + if (children != null) { + for (String child : children) { + deleteDir(new File(dir, child)); + } + } + } else { + return dir.delete(); + } + } + return false; + } + + // 获取文件大小 + // Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/ 目录,一般放一些长时间保存的数据 + // Context.getExternalCacheDir() --> SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据 + private static long getFolderSize(File file) { + long size = 0; + try { + File[] list = file.listFiles(); + if (list != null) { + for (File temp : list) { + // 如果下面还有文件 + if (temp.isDirectory()) { + size = size + getFolderSize(temp); + } else { + size = size + temp.length(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return size; + } + + /** + * 格式化单位 + */ + public static String getFormatSize(double size) { + double kiloByte = size / 1024; + if (kiloByte < 1) { + // return size + "Byte"; + return "0K"; + } + + double megaByte = kiloByte / 1024; + if (megaByte < 1) { + BigDecimal result1 = new BigDecimal(Double.toString(kiloByte)); + return result1.setScale(2, BigDecimal.ROUND_HALF_UP) + .toPlainString() + "K"; + } + + double gigaByte = megaByte / 1024; + if (gigaByte < 1) { + BigDecimal result2 = new BigDecimal(Double.toString(megaByte)); + return result2.setScale(2, BigDecimal.ROUND_HALF_UP) + .toPlainString() + "M"; + } + + double teraBytes = gigaByte / 1024; + if (teraBytes < 1) { + BigDecimal result3 = new BigDecimal(Double.toString(gigaByte)); + return result3.setScale(2, BigDecimal.ROUND_HALF_UP) + .toPlainString() + "GB"; + } + BigDecimal result4 = new BigDecimal(teraBytes); + return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + + "TB"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/helper/DoubleClickHelper.java b/app/src/main/java/com/example/alcoholic/helper/DoubleClickHelper.java new file mode 100644 index 0000000..ce6d163 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/helper/DoubleClickHelper.java @@ -0,0 +1,32 @@ +package com.example.alcoholic.helper; + +import android.os.SystemClock; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 防双击判断工具类 + */ +public final class DoubleClickHelper { + + /** 数组的长度为2代表只记录双击操作 */ + private static final long[] TIME_ARRAY = new long[2]; + + /** + * 是否在短时间内进行了双击操作 + */ + public static boolean isOnDoubleClick() { + // 默认间隔时长 + return isOnDoubleClick(1500); + } + + /** + * 是否在短时间内进行了双击操作 + */ + public static boolean isOnDoubleClick(int time) { + System.arraycopy(TIME_ARRAY, 1, TIME_ARRAY, 0, TIME_ARRAY.length - 1); + TIME_ARRAY[TIME_ARRAY.length - 1] = SystemClock.uptimeMillis(); + return TIME_ARRAY[0] >= (SystemClock.uptimeMillis() - time); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/helper/InputTextHelper.java b/app/src/main/java/com/example/alcoholic/helper/InputTextHelper.java new file mode 100644 index 0000000..12a0dcf --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/helper/InputTextHelper.java @@ -0,0 +1,303 @@ +package com.example.alcoholic.helper; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 文本输入辅助类,通过管理多个 TextView 输入是否为空来启用或者禁用按钮的点击事件 + * blog : https://www.jianshu.com/p/fd3795e8a6b3 + */ +public final class InputTextHelper implements TextWatcher { + + /** 操作按钮的View */ + private View mView; + /** 是否禁用后设置半透明度 */ + private boolean isAlpha; + + /** TextView集合 */ + private List mViewSet; + + /** 输入监听器 */ + private OnInputTextListener mListener; + + /** + * 构造函数 + * + * @param view 跟随 TextView 输入为空来判断启动或者禁用这个 View + * @param alpha 是否需要设置透明度 + */ + private InputTextHelper(View view, boolean alpha) { + if (view == null) { + throw new IllegalArgumentException("are you ok?"); + } + mView = view; + isAlpha = alpha; + } + + /** + * 创建 Builder + */ + public static Builder with(Activity activity) { + return new Builder(activity); + } + + /** + * 添加 TextView + * + * @param views 传入单个或者多个 TextView + */ + public void addViews(List views) { + if (views == null) { + return; + } + + if (mViewSet == null) { + mViewSet = views; + } else { + mViewSet.addAll(views); + } + + for (TextView view : views) { + view.addTextChangedListener(this); + } + + // 触发一次监听 + notifyChanged(); + } + + /** + * 添加 TextView + * + * @param views 传入单个或者多个 TextView + */ + public void addViews(TextView... views) { + if (views == null) { + return; + } + + if (mViewSet == null) { + mViewSet = new ArrayList<>(views.length); + } + + for (TextView view : views) { + // 避免重复添加 + if (!mViewSet.contains(view)) { + view.addTextChangedListener(this); + mViewSet.add(view); + } + } + // 触发一次监听 + notifyChanged(); + } + + /** + * 移除 TextView 监听,避免内存泄露 + */ + public void removeViews(TextView... views) { + if (mViewSet != null && mViewSet.size() > 0) { + for (TextView view : views) { + view.removeTextChangedListener(this); + mViewSet.remove(view); + } + // 触发一次监听 + notifyChanged(); + } + } + + /** + * 移除所有 TextView 监听,避免内存泄露 + */ + public void removeAllViews() { + if (mViewSet == null) { + return; + } + + for (TextView view : mViewSet) { + view.removeTextChangedListener(this); + } + mViewSet.clear(); + mViewSet = null; + } + + /** + * 设置输入监听 + */ + public void setListener(OnInputTextListener listener) { + mListener = listener; + } + + /** + * {@link TextWatcher} + */ + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + notifyChanged(); + } + + /** + * 通知更新 + */ + public void notifyChanged() { + if (mViewSet == null) { + return; + } + + // 重新遍历所有的输入 + for (TextView view : mViewSet) { + if ("".equals(view.getText().toString())) { + setEnabled(false); + return; + } + } + + if (mListener != null) { + setEnabled(mListener.onInputChange(this)); + } else { + setEnabled(true); + } + } + + /** + * 设置 View 的事件 + * + * @param enabled 启用或者禁用 View 的事件 + */ + public void setEnabled(boolean enabled) { + if (enabled == mView.isEnabled()) { + return; + } + + if (enabled) { + //启用View的事件 + mView.setEnabled(true); + if (isAlpha) { + //设置不透明 + mView.setAlpha(1f); + } + } else { + //禁用View的事件 + mView.setEnabled(false); + if (isAlpha) { + //设置半透明 + mView.setAlpha(0.5f); + } + } + } + + public static final class Builder { + + /** 当前的 Activity */ + private final Activity mActivity; + /** 操作按钮的 View */ + private View mView; + /** 是否禁用后设置半透明度 */ + private boolean isAlpha; + /** TextView集合 */ + private final List mViewSet = new ArrayList<>(); + /** 文本 */ + private OnInputTextListener mListener; + + private Builder(Activity activity) { + mActivity = activity; + } + + public Builder addView(TextView view) { + mViewSet.add(view); + return this; + } + + public Builder setMain(View view) { + mView = view; + return this; + } + + public Builder setAlpha(boolean alpha) { + isAlpha = alpha; + return this; + } + + public Builder setListener(OnInputTextListener listener) { + mListener = listener; + return this; + } + + public InputTextHelper build(){ + InputTextHelper helper = new InputTextHelper(mView, isAlpha); + helper.addViews(mViewSet); + helper.setListener(mListener); + mActivity.getApplication().registerActivityLifecycleCallbacks(new TextInputLifecycle(mActivity, helper)); + return helper; + } + } + + private static class TextInputLifecycle implements Application.ActivityLifecycleCallbacks { + + private Activity mActivity; + private InputTextHelper mTextHelper; + + private TextInputLifecycle(Activity activity, InputTextHelper helper) { + this.mActivity = activity; + this.mTextHelper = helper; + } + + @Override + public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(@NonNull Activity activity) {} + + @Override + public void onActivityResumed(@NonNull Activity activity) {} + + @Override + public void onActivityPaused(@NonNull Activity activity) {} + + @Override + public void onActivityStopped(@NonNull Activity activity) {} + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + if (mActivity != null && mActivity == activity) { + mTextHelper.removeAllViews(); + mActivity.getApplication().registerActivityLifecycleCallbacks(this); + mTextHelper = null; + mActivity = null; + } + } + } + + /** + * 文本变化监听器 + */ + public interface OnInputTextListener { + + /** + * 输入发生了变化 + * @return 返回按钮的 Enabled 状态 + */ + boolean onInputChange(InputTextHelper helper); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/helper/KeyboardUtils.java b/app/src/main/java/com/example/alcoholic/helper/KeyboardUtils.java new file mode 100644 index 0000000..46f4229 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/helper/KeyboardUtils.java @@ -0,0 +1,62 @@ +package com.example.alcoholic.helper; + +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 软键盘工具类 + */ +public final class KeyboardUtils { + + /** + * 显示软键盘 + * + * @param view 依附的View + */ + public static void showKeyboard(View view) { + if (view == null) { + return; + } + InputMethodManager imm = (InputMethodManager) view.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(view, 0); + } + } + + /** + * 隐藏软键盘 + * + * @param view 依附的View + */ + public static void hideKeyboard(View view) { + if (view == null) { + return; + } + InputMethodManager imm = (InputMethodManager) view.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + /** + * 切换软键盘 + * + * @param view 依附的View + */ + public static void toggleSoftInput(View view) { + if (view == null) { + return; + } + InputMethodManager imm = (InputMethodManager) view.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.toggleSoftInput(0, 0); + } + } +} diff --git a/app/src/main/java/com/example/alcoholic/helper/RadioButtonGroupHelper.java b/app/src/main/java/com/example/alcoholic/helper/RadioButtonGroupHelper.java new file mode 100644 index 0000000..ca2c056 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/helper/RadioButtonGroupHelper.java @@ -0,0 +1,116 @@ +package com.example.alcoholic.helper; + +import android.view.View; +import android.widget.CompoundButton; +import android.widget.RadioButton; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.IdRes; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 多个 CompoundButton 选中处理辅助类(用于代替 RadioGroup) + */ +public final class RadioButtonGroupHelper implements CompoundButton.OnCheckedChangeListener { + + /** RadioButton集合 */ + private List mViewSet; + + /** 多个RadioButton监听对象 */ + private OnCheckedChangeListener mListener; + + public RadioButtonGroupHelper(RadioButton... groups) { + mViewSet = new ArrayList<>(groups.length - 1); + + for (RadioButton view : groups) { + // 如果这个RadioButton没有设置id的话 + if (view.getId() == View.NO_ID) { + throw new IllegalArgumentException("are you ok?"); + } + view.setOnCheckedChangeListener(this); + mViewSet.add(view); + } + } + + public RadioButtonGroupHelper(View rootView, @IdRes int... ids) { + mViewSet = new ArrayList<>(ids.length - 1); + for (@IdRes int id : ids) { + RadioButton view = rootView.findViewById(id); + view.setOnCheckedChangeListener(this); + mViewSet.add(view); + } + } + + /** 监听标记,避免重复回调 */ + private boolean mTag; + + /** + * {@link CompoundButton.OnCheckedChangeListener} + */ + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked && !mTag) { + mTag = true; + for (CompoundButton view : mViewSet) { + if (view != buttonView && view.isChecked()) { + // 这个 API 会触发监听事件 + view.setChecked(false); + } + } + if (mListener != null) { + mListener.onCheckedChanged((RadioButton) buttonView, buttonView.getId()); + } + mTag = false; + } + } + + /** + * 移除监听,避免内存泄露 + */ + public void removeViews() { + if (mViewSet == null) { + return; + } + + for (CompoundButton view : mViewSet) { + view.setOnCheckedChangeListener(null); + } + mViewSet.clear(); + mViewSet = null; + } + + /** + * 取消选中 + */ + public void clearCheck() { + for (CompoundButton view : mViewSet) { + if (view.isChecked()) { + view.setChecked(false); + } + } + } + + /** + * 设置多个RadioButton的监听 + */ + public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { + mListener = listener; + } + + /** + * 多个CompoundButton选中监听 + */ + public interface OnCheckedChangeListener { + /** + * 被选中的CompoundButton对象 + * + * @param radioButton 选中的RadioButton + * @param checkedId 选中的资源id + */ + void onCheckedChanged(RadioButton radioButton, @IdRes int checkedId); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/glide/GlideConfig.java b/app/src/main/java/com/example/alcoholic/http/glide/GlideConfig.java new file mode 100644 index 0000000..4852348 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/glide/GlideConfig.java @@ -0,0 +1,77 @@ +package com.example.alcoholic.http.glide; + +import android.content.Context; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; +import com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper; +import com.bumptech.glide.load.engine.cache.LruResourceCache; +import com.bumptech.glide.load.engine.cache.MemorySizeCalculator; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.module.AppGlideModule; +import com.bumptech.glide.request.RequestOptions; +import com.example.alcoholic.R; +import com.hjq.http.EasyConfig; + +import java.io.File; +import java.io.InputStream; + +import androidx.annotation.NonNull; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/15 + * desc : Glide 全局配置 + */ +@GlideModule +public final class GlideConfig extends AppGlideModule { + + /** 本地图片缓存文件最大值 */ + private static final int IMAGE_DISK_CACHE_MAX_SIZE = 500 * 1024 * 1024; + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Override + public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { + // 读写外部缓存目录不需要申请存储权限 + File diskCacheFile = new File(context.getCacheDir(), "glide"); + // 如果这个路径是一个文件 + if (diskCacheFile.exists() && diskCacheFile.isFile()) { + // 执行删除操作 + diskCacheFile.delete(); + } + // 如果这个路径不存在 + if (!diskCacheFile.exists()) { + // 创建多级目录 + diskCacheFile.mkdirs(); + } + builder.setDiskCache(() -> DiskLruCacheWrapper.create(diskCacheFile, IMAGE_DISK_CACHE_MAX_SIZE)); + + MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context).build(); + int defaultMemoryCacheSize = calculator.getMemoryCacheSize(); + int defaultBitmapPoolSize = calculator.getBitmapPoolSize(); + + int customMemoryCacheSize = (int) (1.2 * defaultMemoryCacheSize); + int customBitmapPoolSize = (int) (1.2 * defaultBitmapPoolSize); + + builder.setMemoryCache(new LruResourceCache(customMemoryCacheSize)); + builder.setBitmapPool(new LruBitmapPool(customBitmapPoolSize)); + + // 设置默认的加载占位图和加载出错图 + builder.setDefaultRequestOptions(new RequestOptions().placeholder(R.drawable.image_loading_bg).error(R.drawable.image_error_bg)); + } + + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + // Glide 默认采用的是 HttpURLConnection 来做网络请求,这里切换成更高效的 OkHttp + registry.replace(GlideUrl.class, InputStream.class, new OkHttpLoader.Factory(EasyConfig.getInstance().getClient())); + } + + @Override + public boolean isManifestParsingEnabled() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/glide/OkHttpFetcher.java b/app/src/main/java/com/example/alcoholic/http/glide/OkHttpFetcher.java new file mode 100644 index 0000000..6d4bd5a --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/glide/OkHttpFetcher.java @@ -0,0 +1,106 @@ +package com.example.alcoholic.http.glide; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.HttpException; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.util.ContentLengthInputStream; +import com.bumptech.glide.util.Preconditions; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import androidx.annotation.NonNull; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/15 + * desc : OkHttp 加载器 + */ +public final class OkHttpFetcher implements DataFetcher, Callback { + + private final Call.Factory mCallFactory; + private final GlideUrl mGlideUrl; + private InputStream mInputStream; + private ResponseBody mResponseBody; + private DataCallback mDataCallback; + private volatile Call mCall; + + OkHttpFetcher(Call.Factory factory, GlideUrl url) { + mCallFactory = factory; + mGlideUrl = url; + } + + @Override + public void loadData(@NonNull Priority priority, + @NonNull final DataFetcher.DataCallback callback) { + Request.Builder requestBuilder = new Request.Builder().url(mGlideUrl.toStringUrl()); + for (Map.Entry headerEntry : mGlideUrl.getHeaders().entrySet()) { + String key = headerEntry.getKey(); + requestBuilder.addHeader(key, headerEntry.getValue()); + } + Request request = requestBuilder.build(); + mDataCallback = callback; + + mCall = mCallFactory.newCall(request); + mCall.enqueue(this); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + mDataCallback.onLoadFailed(e); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + mResponseBody = response.body(); + if (response.isSuccessful()) { + long contentLength = Preconditions.checkNotNull(mResponseBody).contentLength(); + mInputStream = ContentLengthInputStream.obtain(mResponseBody.byteStream(), contentLength); + mDataCallback.onDataReady(mInputStream); + } else { + mDataCallback.onLoadFailed(new HttpException(response.message(), response.code())); + } + } + + @Override + public void cleanup() { + try { + if (mInputStream != null) { + mInputStream.close(); + } + } catch (IOException ignored) {} + + if (mResponseBody != null) { + mResponseBody.close(); + } + mDataCallback = null; + } + + @Override + public void cancel() { + if (mCall != null) { + mCall.cancel(); + } + } + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.REMOTE; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/glide/OkHttpLoader.java b/app/src/main/java/com/example/alcoholic/http/glide/OkHttpLoader.java new file mode 100644 index 0000000..053dbd7 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/glide/OkHttpLoader.java @@ -0,0 +1,56 @@ +package com.example.alcoholic.http.glide; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; + +import java.io.InputStream; + +import androidx.annotation.NonNull; +import okhttp3.Call; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/15 + * desc : OkHttp 加载模型 + */ +public final class OkHttpLoader implements ModelLoader { + + private final Call.Factory mFactory; + + private OkHttpLoader(@NonNull Call.Factory factory) { + mFactory = factory; + } + + @Override + public boolean handles(@NonNull GlideUrl url) { + return true; + } + + @Override + public LoadData buildLoadData(@NonNull GlideUrl model, int width, int height, + @NonNull Options options) { + return new LoadData<>(model, new OkHttpFetcher(mFactory, model)); + } + + public static class Factory implements ModelLoaderFactory { + + private final Call.Factory mFactory; + + Factory(@NonNull Call.Factory factory) { + mFactory = factory; + } + + @NonNull + @Override + public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { + return new OkHttpLoader(mFactory); + } + + @Override + public void teardown() {} + } +} diff --git a/app/src/main/java/com/example/alcoholic/http/json/BooleanTypeAdapter.java b/app/src/main/java/com/example/alcoholic/http/json/BooleanTypeAdapter.java new file mode 100644 index 0000000..f2595cf --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/json/BooleanTypeAdapter.java @@ -0,0 +1,40 @@ +package com.example.alcoholic.http.json; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/05/05 + * desc : boolean / Boolean 类型解析适配器,参考:{@link com.google.gson.internal.bind.TypeAdapters#BOOLEAN} + */ +public class BooleanTypeAdapter extends TypeAdapter { + + @Override + public Boolean read(JsonReader in) throws IOException { + switch (in.peek()) { + case BOOLEAN: + return in.nextBoolean(); + case STRING: + // 如果后台返回 "true" 或者 "TRUE",则处理为 true,否则为 false + return Boolean.parseBoolean(in.nextString()); + case NUMBER: + // 如果这个后台返回是 1 则处理为 true,否则为 false + return in.nextInt() == 1; + case NULL: + in.nextNull(); + return null; + default: + in.skipValue(); + return null; + } + } + @Override + public void write(JsonWriter out, Boolean value) throws IOException { + out.value(value); + } +} diff --git a/app/src/main/java/com/example/alcoholic/http/json/DoubleTypeAdapter.java b/app/src/main/java/com/example/alcoholic/http/json/DoubleTypeAdapter.java new file mode 100644 index 0000000..981b065 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/json/DoubleTypeAdapter.java @@ -0,0 +1,41 @@ +package com.example.alcoholic.http.json; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/05/05 + * desc : double / Double 类型解析适配器,参考:{@link com.google.gson.internal.bind.TypeAdapters#DOUBLE} + */ +public class DoubleTypeAdapter extends TypeAdapter { + + @Override + public Number read(JsonReader in) throws IOException { + switch (in.peek()) { + case NUMBER: + return in.nextDouble(); + case STRING: + try { + return Double.parseDouble(in.nextString()); + } catch (NumberFormatException e) { + // 如果是空字符串则会抛出这个异常 + return 0; + } + case NULL: + in.nextNull(); + return null; + default: + in.skipValue(); + return 0; + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/json/FloatTypeAdapter.java b/app/src/main/java/com/example/alcoholic/http/json/FloatTypeAdapter.java new file mode 100644 index 0000000..7a9f3a4 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/json/FloatTypeAdapter.java @@ -0,0 +1,23 @@ +package com.example.alcoholic.http.json; + +import com.google.gson.stream.JsonReader; + +import java.io.IOException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/05/05 + * desc : float / Float 类型解析适配器,参考:{@link com.google.gson.internal.bind.TypeAdapters#FLOAT} + */ +public class FloatTypeAdapter extends DoubleTypeAdapter { + + @Override + public Number read(JsonReader in) throws IOException { + Number number = super.read(in); + if (number != null) { + return number.floatValue(); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/json/IntegerTypeAdapter.java b/app/src/main/java/com/example/alcoholic/http/json/IntegerTypeAdapter.java new file mode 100644 index 0000000..72cdd50 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/json/IntegerTypeAdapter.java @@ -0,0 +1,23 @@ +package com.example.alcoholic.http.json; + +import com.google.gson.stream.JsonReader; + +import java.io.IOException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/05/05 + * desc : int / Integer 类型解析适配器,参考:{@link com.google.gson.internal.bind.TypeAdapters#INTEGER} + */ +public class IntegerTypeAdapter extends DoubleTypeAdapter { + + @Override + public Number read(JsonReader in) throws IOException { + Number number = super.read(in); + if (number != null) { + return number.intValue(); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/json/ListTypeAdapter.java b/app/src/main/java/com/example/alcoholic/http/json/ListTypeAdapter.java new file mode 100644 index 0000000..0d58045 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/json/ListTypeAdapter.java @@ -0,0 +1,43 @@ +package com.example.alcoholic.http.json; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/05/05 + * desc : List 类型解析适配器 + */ +public class ListTypeAdapter implements JsonDeserializer { + + @SuppressWarnings("unchecked") + @Override + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + // 如果这是一个数组 + if (json.isJsonArray()) { + JsonArray array = json.getAsJsonArray(); + // 获取 List 上的泛型 + Type itemType = ((ParameterizedType) typeOfT).getActualTypeArguments()[0]; + List list = new ArrayList(); + for (int i = 0; i < array.size(); i++) { + JsonElement element = array.get(i); + // 解析 List 中的条目对象 + Object item = context.deserialize(element, itemType); + list.add(item); + } + return list; + } else { + // 类型不符,直接返回 null + return null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/json/LongTypeAdapter.java b/app/src/main/java/com/example/alcoholic/http/json/LongTypeAdapter.java new file mode 100644 index 0000000..cd5c97c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/json/LongTypeAdapter.java @@ -0,0 +1,23 @@ +package com.example.alcoholic.http.json; + +import com.google.gson.stream.JsonReader; + +import java.io.IOException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/05/05 + * desc : long / Long 类型解析适配器,参考:{@link com.google.gson.internal.bind.TypeAdapters#LONG} + */ +public class LongTypeAdapter extends DoubleTypeAdapter { + + @Override + public Number read(JsonReader in) throws IOException { + Number number = super.read(in); + if (number != null) { + return number.longValue(); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/json/StringTypeAdapter.java b/app/src/main/java/com/example/alcoholic/http/json/StringTypeAdapter.java new file mode 100644 index 0000000..c4aac5c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/json/StringTypeAdapter.java @@ -0,0 +1,39 @@ +package com.example.alcoholic.http.json; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/EasyHttp + * time : 2020/05/05 + * desc : String 类型解析适配器,参考:{@link com.google.gson.internal.bind.TypeAdapters#STRING} + */ +public class StringTypeAdapter extends TypeAdapter { + + @Override + public String read(JsonReader in) throws IOException { + switch (in.peek()) { + case STRING: + case NUMBER: + return in.nextString(); + case BOOLEAN: + // 对于布尔类型比较特殊,需要做针对性处理 + return Boolean.toString(in.nextBoolean()); + case NULL: + in.nextNull(); + return null; + default: + in.skipValue(); + return null; + } + } + + @Override + public void write(JsonWriter out, String value) throws IOException { + out.value(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/model/HttpData.java b/app/src/main/java/com/example/alcoholic/http/model/HttpData.java new file mode 100644 index 0000000..9b7cf75 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/model/HttpData.java @@ -0,0 +1,29 @@ +package com.example.alcoholic.http.model; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 统一接口数据结构 + */ +public class HttpData { + + /** 返回码 */ + private int code; + /** 提示语 */ + private String msg; + /** 数据 */ + private T data; + + public int getCode() { + return code; + } + + public String getMessage() { + return msg; + } + + public T getData() { + return data; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/model/RequestHandler.java b/app/src/main/java/com/example/alcoholic/http/model/RequestHandler.java new file mode 100644 index 0000000..cef048f --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/model/RequestHandler.java @@ -0,0 +1,193 @@ +package com.example.alcoholic.http.model; + +import android.app.Application; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.example.alcoholic.R; +import com.example.alcoholic.http.json.BooleanTypeAdapter; +import com.example.alcoholic.http.json.DoubleTypeAdapter; +import com.example.alcoholic.http.json.FloatTypeAdapter; +import com.example.alcoholic.http.json.IntegerTypeAdapter; +import com.example.alcoholic.http.json.ListTypeAdapter; +import com.example.alcoholic.http.json.LongTypeAdapter; +import com.example.alcoholic.http.json.StringTypeAdapter; +import com.example.alcoholic.http.response.ImgUploadBean; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.internal.bind.TypeAdapters; +import com.hjq.http.EasyLog; +import com.hjq.http.config.IRequestHandler; +import com.hjq.http.exception.DataException; +import com.hjq.http.exception.ResponseException; +import com.hjq.http.exception.ResultException; +import com.hjq.http.exception.TokenException; +import com.hjq.http.exception.UnknownException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; + +import androidx.lifecycle.LifecycleOwner; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/07 + * desc : 请求处理类 + */ +public final class RequestHandler implements IRequestHandler { + + private final Application mApplication; + + private Gson mGson; + + public RequestHandler(Application application) { + mApplication = application; + } + + @Override + public Object requestSucceed(LifecycleOwner lifecycle, Response response, Type type) throws Exception { + + if (Response.class.equals(type)) { + return response; + } + + if (!response.isSuccessful()) { + // 返回响应异常 + throw new ResponseException(mApplication.getString(R.string.http_response_error) + ",responseCode:" + response.code() + ",message:" + response.message(), response); + } + + ResponseBody body = response.body(); + if (body == null) { + return null; + } + + if (Bitmap.class.equals(type)) { + // 如果这是一个 Bitmap 对象 + return BitmapFactory.decodeStream(body.byteStream()); + } + + String text; + try { + text = body.string(); + } catch (IOException e) { + // 返回结果读取异常 + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + + // 打印这个 Json + EasyLog.json(text); + + final Object result; + if (String.class.equals(type)) { + // 如果这是一个 String 对象 + result = text; + } else if (JSONObject.class.equals(type)) { + try { + // 如果这是一个 JSONObject 对象 + result = new JSONObject(text); + } catch (JSONException e) { + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + } else if (JSONArray.class.equals(type)) { + try { + // 如果这是一个 JSONArray 对象 + result = new JSONArray(text); + }catch (JSONException e) { + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + } else { + + try { + if (mGson == null) { + // Json 容错处理 + mGson = new GsonBuilder() + .registerTypeAdapterFactory(TypeAdapters.newFactory(String.class, new StringTypeAdapter())) + .registerTypeAdapterFactory(TypeAdapters.newFactory(boolean.class, Boolean.class, new BooleanTypeAdapter())) + .registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, new IntegerTypeAdapter())) + .registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, new LongTypeAdapter())) + .registerTypeAdapterFactory(TypeAdapters.newFactory(float.class, Float.class, new FloatTypeAdapter())) + .registerTypeAdapterFactory(TypeAdapters.newFactory(double.class, Double.class, new DoubleTypeAdapter())) + .registerTypeHierarchyAdapter(List.class, new ListTypeAdapter()) + .create(); + } + result = mGson.fromJson(text, type); + } catch (JsonSyntaxException e) { + // 返回结果读取异常 + throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); + } + + if (result instanceof HttpData) { + HttpData model = (HttpData) result; + if (model.getCode() == 0) { + // 代表执行成功 + return result; + } else if (model.getCode() == 1001) { + // 代表登录失效,需要重新登录 + throw new TokenException(mApplication.getString(R.string.http_account_error)); + } else { + // 代表执行失败 + throw new ResultException(model.getMessage(), model); + } + } + + if (result instanceof ImgUploadBean){ + ImgUploadBean imgUploadBean = (ImgUploadBean) result; + + if (imgUploadBean.getStatus() == 200){ + return result; + }else { + // 代表执行失败 + throw new UnknownException("图片上传失败"); + } + + } + } + return result; + } + + @Override + public Exception requestFail(LifecycleOwner lifecycle, Exception e) { + // 判断这个异常是不是自己抛的 + // TODO: 2020/11/13 +// if (e instanceof HttpException) { +// if (e instanceof TokenException) { +// // 登录信息失效,跳转到登录页 +// Application application = ActivityStackManager.getInstance().getApplication(); +// Intent intent = new Intent(application, LoginActivity.class); +// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +// application.startActivity(intent); +// // 销毁除了登录页之外的界面 +// ActivityStackManager.getInstance().finishAllActivities(LoginActivity.class); +// } +// } else { +// if (e instanceof SocketTimeoutException) { +// e = new TimeoutException(mApplication.getString(R.string.http_server_out_time), e); +// } else if (e instanceof UnknownHostException) { +// NetworkInfo info = ((ConnectivityManager) mApplication.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); +// // 判断网络是否连接 +// if (info != null && info.isConnected()) { +// // 有连接就是服务器的问题 +// e = new ServerException(mApplication.getString(R.string.http_server_error), e); +// } else { +// // 没有连接就是网络异常 +// e = new NetworkException(mApplication.getString(R.string.http_network_error), e); +// } +// } else if (e instanceof IOException) { +// //e = new CancelException(context.getString(R.string.http_request_cancel), e); +// e = new CancelException("", e); +// } else { +// e = new HttpException(e.getMessage(), e); +// } +// } + return e; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/http/request/ImgUpLoadApi.java b/app/src/main/java/com/example/alcoholic/http/request/ImgUpLoadApi.java new file mode 100644 index 0000000..e6717d6 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/request/ImgUpLoadApi.java @@ -0,0 +1,45 @@ +package com.example.alcoholic.http.request; + +import com.hjq.http.annotation.HttpIgnore; +import com.hjq.http.config.IRequestApi; + +import java.io.File; + +/** + * Created by + * Description: 图片上传接口 + * on 2020/11/23. + */ +public class ImgUpLoadApi implements IRequestApi { + @Override + public String getApi() { + return "1/upload"; + } + + + private String key = "ce2334330e0e82e325cb0f2aecc2e66a"; + private File image; + private String name; + private long expiration; + + + public ImgUpLoadApi setKey(String key) { + this.key = key; + return this; + } + + public ImgUpLoadApi setImage(File image) { + this.image = image; + return this; + } + + public ImgUpLoadApi setName(String name) { + this.name = name; + return this; + } + + public ImgUpLoadApi setExpiration(long expiration) { + this.expiration = expiration; + return this; + } +} diff --git a/app/src/main/java/com/example/alcoholic/http/response/ImgUploadBean.java b/app/src/main/java/com/example/alcoholic/http/response/ImgUploadBean.java new file mode 100644 index 0000000..74ca75b --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/response/ImgUploadBean.java @@ -0,0 +1,338 @@ +package com.example.alcoholic.http.response; + +/** + * Created by + * Description:图片上传返回实体类 + * on 2020/11/23. + */ +public class ImgUploadBean { + + + /** + * data : {"id":"gP1ZDr7","title":"img-7833e84d85da4ef19fa268d78e5e1908","url_viewer":"https://ibb.co/gP1ZDr7","url":"https://i.ibb.co/9VXHqpv/img-7833e84d85da4ef19fa268d78e5e1908.jpg","display_url":"https://i.ibb.co/txfpbHh/img-7833e84d85da4ef19fa268d78e5e1908.jpg","size":35907,"time":"1606110231","expiration":"0","image":{"filename":"img-7833e84d85da4ef19fa268d78e5e1908.jpg","name":"img-7833e84d85da4ef19fa268d78e5e1908","mime":"image/jpeg","extension":"jpg","url":"https://i.ibb.co/9VXHqpv/img-7833e84d85da4ef19fa268d78e5e1908.jpg"},"thumb":{"filename":"img-7833e84d85da4ef19fa268d78e5e1908.jpg","name":"img-7833e84d85da4ef19fa268d78e5e1908","mime":"image/jpeg","extension":"jpg","url":"https://i.ibb.co/gP1ZDr7/img-7833e84d85da4ef19fa268d78e5e1908.jpg"},"medium":{"filename":"img-7833e84d85da4ef19fa268d78e5e1908.jpg","name":"img-7833e84d85da4ef19fa268d78e5e1908","mime":"image/jpeg","extension":"jpg","url":"https://i.ibb.co/txfpbHh/img-7833e84d85da4ef19fa268d78e5e1908.jpg"},"delete_url":"https://ibb.co/gP1ZDr7/96cda5476745b08f66baaa8b26bd6e0a"} + * success : true + * status : 200 + */ + + private DataBean data; + private boolean success; + private int status; + + public DataBean getData() { + return data; + } + + public void setData(DataBean data) { + this.data = data; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public static class DataBean { + /** + * id : gP1ZDr7 + * title : img-7833e84d85da4ef19fa268d78e5e1908 + * url_viewer : https://ibb.co/gP1ZDr7 + * url : https://i.ibb.co/9VXHqpv/img-7833e84d85da4ef19fa268d78e5e1908.jpg + * display_url : https://i.ibb.co/txfpbHh/img-7833e84d85da4ef19fa268d78e5e1908.jpg + * size : 35907 + * time : 1606110231 + * expiration : 0 + * image : {"filename":"img-7833e84d85da4ef19fa268d78e5e1908.jpg","name":"img-7833e84d85da4ef19fa268d78e5e1908","mime":"image/jpeg","extension":"jpg","url":"https://i.ibb.co/9VXHqpv/img-7833e84d85da4ef19fa268d78e5e1908.jpg"} + * thumb : {"filename":"img-7833e84d85da4ef19fa268d78e5e1908.jpg","name":"img-7833e84d85da4ef19fa268d78e5e1908","mime":"image/jpeg","extension":"jpg","url":"https://i.ibb.co/gP1ZDr7/img-7833e84d85da4ef19fa268d78e5e1908.jpg"} + * medium : {"filename":"img-7833e84d85da4ef19fa268d78e5e1908.jpg","name":"img-7833e84d85da4ef19fa268d78e5e1908","mime":"image/jpeg","extension":"jpg","url":"https://i.ibb.co/txfpbHh/img-7833e84d85da4ef19fa268d78e5e1908.jpg"} + * delete_url : https://ibb.co/gP1ZDr7/96cda5476745b08f66baaa8b26bd6e0a + */ + + private String id; + private String title; + private String url_viewer; + private String url; + private String display_url; + private int size; + private String time; + private String expiration; + private ImageBean image; + private ThumbBean thumb; + private MediumBean medium; + private String delete_url; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getUrl_viewer() { + return url_viewer; + } + + public void setUrl_viewer(String url_viewer) { + this.url_viewer = url_viewer; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDisplay_url() { + return display_url; + } + + public void setDisplay_url(String display_url) { + this.display_url = display_url; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } + + public String getExpiration() { + return expiration; + } + + public void setExpiration(String expiration) { + this.expiration = expiration; + } + + public ImageBean getImage() { + return image; + } + + public void setImage(ImageBean image) { + this.image = image; + } + + public ThumbBean getThumb() { + return thumb; + } + + public void setThumb(ThumbBean thumb) { + this.thumb = thumb; + } + + public MediumBean getMedium() { + return medium; + } + + public void setMedium(MediumBean medium) { + this.medium = medium; + } + + public String getDelete_url() { + return delete_url; + } + + public void setDelete_url(String delete_url) { + this.delete_url = delete_url; + } + + public static class ImageBean { + /** + * filename : img-7833e84d85da4ef19fa268d78e5e1908.jpg + * name : img-7833e84d85da4ef19fa268d78e5e1908 + * mime : image/jpeg + * extension : jpg + * url : https://i.ibb.co/9VXHqpv/img-7833e84d85da4ef19fa268d78e5e1908.jpg + */ + + private String filename; + private String name; + private String mime; + private String extension; + private String url; + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMime() { + return mime; + } + + public void setMime(String mime) { + this.mime = mime; + } + + public String getExtension() { + return extension; + } + + public void setExtension(String extension) { + this.extension = extension; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } + + public static class ThumbBean { + /** + * filename : img-7833e84d85da4ef19fa268d78e5e1908.jpg + * name : img-7833e84d85da4ef19fa268d78e5e1908 + * mime : image/jpeg + * extension : jpg + * url : https://i.ibb.co/gP1ZDr7/img-7833e84d85da4ef19fa268d78e5e1908.jpg + */ + + private String filename; + private String name; + private String mime; + private String extension; + private String url; + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMime() { + return mime; + } + + public void setMime(String mime) { + this.mime = mime; + } + + public String getExtension() { + return extension; + } + + public void setExtension(String extension) { + this.extension = extension; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } + + public static class MediumBean { + /** + * filename : img-7833e84d85da4ef19fa268d78e5e1908.jpg + * name : img-7833e84d85da4ef19fa268d78e5e1908 + * mime : image/jpeg + * extension : jpg + * url : https://i.ibb.co/txfpbHh/img-7833e84d85da4ef19fa268d78e5e1908.jpg + */ + + private String filename; + private String name; + private String mime; + private String extension; + private String url; + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMime() { + return mime; + } + + public void setMime(String mime) { + this.mime = mime; + } + + public String getExtension() { + return extension; + } + + public void setExtension(String extension) { + this.extension = extension; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } + } +} diff --git a/app/src/main/java/com/example/alcoholic/http/server/ImgBBServer.java b/app/src/main/java/com/example/alcoholic/http/server/ImgBBServer.java new file mode 100644 index 0000000..e4904c7 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/server/ImgBBServer.java @@ -0,0 +1,15 @@ +package com.example.alcoholic.http.server; + +import com.hjq.http.config.IRequestServer; + +/** + * Created by + * Description: imgbb fuwu + * on 2020/11/23. + */ +public class ImgBBServer implements IRequestServer { + @Override + public String getHost() { + return "https://api.imgbb.com/"; + } +} diff --git a/app/src/main/java/com/example/alcoholic/http/server/ReleaseServer.java b/app/src/main/java/com/example/alcoholic/http/server/ReleaseServer.java new file mode 100644 index 0000000..5d3f685 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/http/server/ReleaseServer.java @@ -0,0 +1,20 @@ +package com.example.alcoholic.http.server; + +import com.hjq.http.config.IRequestServer; + +/** + * Created by + * Description: + * on 2020/11/13. + */ +public class ReleaseServer implements IRequestServer { + @Override + public String getHost() { + return "https://www.baidu.com/"; + } + + @Override + public String getPath() { + return "api/"; + } +} diff --git a/app/src/main/java/com/example/alcoholic/im/ContactListener.java b/app/src/main/java/com/example/alcoholic/im/ContactListener.java new file mode 100644 index 0000000..b647e68 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/im/ContactListener.java @@ -0,0 +1,126 @@ +package com.example.alcoholic.im; + +import android.content.Intent; + +import com.example.alcoholic.bean.ContactListenerBean; +import com.example.alcoholic.bean.ContactsBean; +import com.example.alcoholic.bean.MessageEncryptBean; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.constant.PConstant; +import com.example.alcoholic.constant.RandomConstant; +import com.example.alcoholic.db.DbContactsHelper; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.utils.AESUtils; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.example.base.BaseActivity; +import com.google.gson.Gson; +import com.hyphenate.EMCallBack; +import com.hyphenate.EMContactListener; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMTextMessageBody; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; + +import org.greenrobot.eventbus.EventBus; + +import androidx.annotation.Nullable; + +/** + * Created by + * Description: 联系人监听 + * on 2020/11/18. + */ +public class ContactListener implements EMContactListener { + @Override + public void onContactAdded(String username) { + //增加了联系人时回调此方法 +// Logger.d("增加了联系人"); + DbContactsHelper.upDateAccount2Friend(username,true); + + EventBus.getDefault().post(new ContactListenerBean(username,true)); + SendMsg.sendText("你好!很高兴和你成为朋友。",username); + } + + @Override + public void onContactDeleted(String username) { + //被删除时回调此方法 +// Logger.d("被删除时"); + DbContactsHelper.upDateAccount2Friend(username,false); + EventBus.getDefault().post(new ContactListenerBean(username,false)); + } + + @Override + public void onContactInvited(String username, String reason) { + //收到好友邀请,直接同意 +// Logger.d("收到好友请求"); + EMClient.getInstance().contactManager().asyncAcceptInvitation(username, new EMCallBack() { + @Override + public void onSuccess() { + contactAdd(username,reason); + } + + @Override + public void onError(int code, String error) { + + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + } + + @Override + public void onFriendRequestAccepted(String username) { + //好友请求被同意 +// Logger.d("好友请求被同意"); + DbContactsHelper.upDateAccount2Friend(username,true); + EventBus.getDefault().post(new ContactListenerBean(username,true)); + } + + @Override + public void onFriendRequestDeclined(String username) { + //好友请求被拒绝 +// Logger.d("好友请求被拒绝"); + DbContactsHelper.upDateAccount2Friend(username,false); + EventBus.getDefault().post(new ContactListenerBean(username,false)); + } + + + private void contactAdd(String username, String reason){ + MessageEncryptBean messageEncryptBean = new MessageEncryptBean(); + + try { + String msgJson = AESUtils.decrypt(MMKVStytemUtils.getInstance().getAesKey(),reason); + messageEncryptBean = new Gson().fromJson(msgJson, MessageEncryptBean.class); + } catch (Exception e) { + + messageEncryptBean.setUserTag(""); + messageEncryptBean.setUserName(username+" - 信息丢失"); + messageEncryptBean.setUserHead(RandomConstant.randomHeadImg()); + messageEncryptBean.setUserIntro("该用户消息丢失在酒杯里~"); + messageEncryptBean.setTextMsgContent(PConstant.fartQuotes()); + + e.printStackTrace(); + } + + //存储用户信息 + UserInfoBean userInfoBean= new UserInfoBean(); + userInfoBean.setUserTag(messageEncryptBean.getUserTag()); + userInfoBean.setUserAccount(username); + userInfoBean.setUserName(messageEncryptBean.getUserName()); + userInfoBean.setUserHead(messageEncryptBean.getUserHead()); + userInfoBean.setUserIntro(messageEncryptBean.getUserIntro()); + userInfoBean.setIsGroup(false); + DbUserHelper.insertUser(userInfoBean); + + //存储联系人信息 + DbContactsHelper.insetContacts(DbContactsHelper.createContact(username,true)); + } + + + + +} diff --git a/app/src/main/java/com/example/alcoholic/im/IMConstant.java b/app/src/main/java/com/example/alcoholic/im/IMConstant.java new file mode 100644 index 0000000..d3c5efb --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/im/IMConstant.java @@ -0,0 +1,112 @@ +package com.example.alcoholic.im; + +import com.hyphenate.chat.EMMessage; + +/** + * Created by + * Description: im的相关常量类 + * on 2020/11/16. + */ +public class IMConstant { + + //聊天类型 + public static final int CHAT_TYPE_CHAT = 1 ; //单聊 + public static final int CHAT_TYPE_GROUPCHAT = 2 ; //群聊 + public static final int CHAT_TYPE_CHATROOM = 3 ; //聊天室 + + //消息类型 + public static final int MESSAGE_TYPE_TXT = 1; + public static final int MESSAGE_TYPE_IMAGE = 2; + public static final int MESSAGE_TYPE_VIDEO = 3; + public static final int MESSAGE_TYPE_LOCATION = 4; + public static final int MESSAGE_TYPE_VOICE = 5; + public static final int MESSAGE_TYPE_FILE = 6; + public static final int MESSAGE_TYPE_CMD = 7; + public static final int MESSAGE_TYPE_CUSTOM = 8; + + //消息方向 + public static final int MESSAGE_DIRECT_SEND = 1; + public static final int MESSAGE_DIRECT_RECEIVE = 2; + + //消息状态 + public static final int MESSAGE_STATUS_SUCCESS = 1;//成功 + public static final int MESSAGE_STATUS_FAIL = 2;//失败 + public static final int MESSAGE_STATUS_INPROGRESS = 3;//发送/接收过程中 + public static final int MESSAGE_STATUS_CREATE = 4;//创建成功待发送 + + + /** + * 获取聊天类型 + */ + public static int makeChatType(EMMessage.ChatType chatType){ + switch (chatType){ + case Chat: + return CHAT_TYPE_CHAT; + case GroupChat: + return CHAT_TYPE_GROUPCHAT; + case ChatRoom: + return CHAT_TYPE_CHATROOM; + default: + return CHAT_TYPE_CHAT; + } + } + + /** + * 获取消息类型 + */ + public static int makeMessageType(EMMessage.Type type){ + switch (type){ + case TXT: + return MESSAGE_TYPE_TXT; + case IMAGE: + return MESSAGE_TYPE_IMAGE; + case VIDEO: + return MESSAGE_TYPE_VIDEO; + case LOCATION: + return MESSAGE_TYPE_LOCATION; + case VOICE: + return MESSAGE_TYPE_VOICE; + case FILE: + return MESSAGE_TYPE_FILE; + case CMD: + return MESSAGE_TYPE_CMD; + case CUSTOM: + return MESSAGE_TYPE_CUSTOM; + default: + return MESSAGE_TYPE_TXT; + } + } + + /** + * 获取消息的方向类型 + */ + public static int makeMsgDirect(EMMessage.Direct direct){ + switch (direct){ + case SEND: + return MESSAGE_DIRECT_SEND; + case RECEIVE: + return MESSAGE_DIRECT_RECEIVE; + default: + return MESSAGE_DIRECT_SEND; + } + } + + /** + * 获取消息状态 + */ + public static int makeMsgStatus(EMMessage.Status status){ + switch (status){ + case SUCCESS: + return MESSAGE_STATUS_SUCCESS; + case FAIL: + return MESSAGE_STATUS_FAIL; + case INPROGRESS: + return MESSAGE_STATUS_INPROGRESS; + case CREATE: + return MESSAGE_STATUS_CREATE; + default: + return MESSAGE_STATUS_SUCCESS; + } + } + +} diff --git a/app/src/main/java/com/example/alcoholic/im/ImBeanChangeHelper.java b/app/src/main/java/com/example/alcoholic/im/ImBeanChangeHelper.java new file mode 100644 index 0000000..1bade38 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/im/ImBeanChangeHelper.java @@ -0,0 +1,226 @@ +package com.example.alcoholic.im; + +import android.database.Cursor; + +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.bean.MessageEncryptBean; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.constant.PConstant; +import com.example.alcoholic.constant.RandomConstant; +import com.example.alcoholic.db.DbMsgHelper; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.gen.MessageBeanDao; +import com.example.alcoholic.utils.AESUtils; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.google.gson.Gson; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMGroup; +import com.hyphenate.chat.EMMessage; +import com.hyphenate.chat.EMTextMessageBody; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; +import com.tencent.mmkv.MMKV; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +/** + * Created by + * Description:im 实体类转换工具类 + * on 2020/11/17. + */ +public class ImBeanChangeHelper { + + /** + * 收到消息时创建实体嘞 + */ + public static MessageBean msgBeanChange(EMMessage emBean){ + MessageBean messageBean = new MessageBean(); + + MessageEncryptBean messageEncryptBean = new MessageEncryptBean(); + try { + String msgJson = AESUtils.decrypt(MMKVStytemUtils.getInstance().getAesKey(),((EMTextMessageBody)emBean.getBody()).getMessage()); + messageEncryptBean = new Gson().fromJson(msgJson, MessageEncryptBean.class); + } catch (Exception e) { + + + messageEncryptBean.setUserTag(""); + messageEncryptBean.setUserHead(RandomConstant.randomHeadImg()); + messageEncryptBean.setTextMsgContent(PConstant.fartQuotes()); + if (emBean.getChatType() == EMMessage.ChatType.Chat){ + //单聊 + messageEncryptBean.setUserName(emBean.getFrom()+" - 信息丢失"); + messageEncryptBean.setUserIntro("该用户消息丢失在酒杯里~"); + }else { + //群组 + messageEncryptBean.setUserName(emBean.getTo()+" - 群组信息丢失"); + messageEncryptBean.setUserIntro("该群组消息丢失在酒杯里~"); + } + + e.printStackTrace(); + } + + messageBean.setMsgId(emBean.getMsgId()); + messageBean.setAttribute(MMKVUserUtils.getInstance().getUserAccount()); + if (emBean.getChatType() == EMMessage.ChatType.Chat){ + //会话 + messageBean.setOtherSide(emBean.getFrom()); + messageBean.setToUserTag(MMKVUserUtils.getInstance().getUserInfoTag()); + messageBean.setToAccount(emBean.getTo()); + messageBean.setToName(MMKVUserUtils.getInstance().getUserName()); + messageBean.setToHead(MMKVUserUtils.getInstance().getUserHead()); + }else { + //群聊 + UserInfoBean groupInfo = DbUserHelper.queryGroupInfo2Id(emBean.getTo()); + messageBean.setOtherSide(emBean.getTo()); + messageBean.setToAccount(emBean.getTo()); + + //如果存得有该群聊的信息,则直接使用该群聊信息, + if (EmptyUtils.isNotEmpty(groupInfo)) { + messageBean.setToUserTag(groupInfo.getUserTag()); + messageBean.setToName(groupInfo.getUserName()); + messageBean.setToHead(groupInfo.getUserHead()); + }else { + //没有该群聊信息 + //根据群组ID从服务器获取群组基本信息 + try { + EMGroup group = EMClient.getInstance().groupManager().getGroupFromServer(emBean.getTo()); + + //更新群组数据 + UserInfoBean newGroupInfo = DbUserHelper.insertGroup2Bean(group); + messageBean.setToUserTag(newGroupInfo.getUserTag()); + messageBean.setToName(newGroupInfo.getUserName()); + messageBean.setToHead(newGroupInfo.getUserHead()); + } catch (HyphenateException e) { + e.printStackTrace(); + } + } + + } + + messageBean.setFromAccount(emBean.getUserName()); + messageBean.setFromUserTag(messageEncryptBean.getUserTag()); + messageBean.setFromName(messageEncryptBean.getUserName()); + messageBean.setFromHead(messageEncryptBean.getUserHead()); + messageBean.setMsgTextContent(messageEncryptBean.getTextMsgContent()); + messageBean.setMsgImgContent(messageEncryptBean.getImgMsgContent()); + messageBean.setMsgTime(emBean.getMsgTime()); + messageBean.setGroupAckCount(emBean.groupAckCount()); + messageBean.setChatType(IMConstant.makeChatType(emBean.getChatType())); + messageBean.setMsgType(IMConstant.makeMessageType(emBean.getType())); + messageBean.setStatus(IMConstant.makeMsgStatus(emBean.status())); + messageBean.setDirection(IMConstant.makeMsgDirect(emBean.direct())); + + //顺便存储用户信息 + UserInfoBean userInfoBean = new UserInfoBean(); + userInfoBean.setUserTag(messageEncryptBean.getUserTag()); + userInfoBean.setUserAccount(emBean.getUserName()); + userInfoBean.setUserName(messageEncryptBean.getUserName()); + userInfoBean.setUserHead(messageEncryptBean.getUserHead()); + userInfoBean.setUserIntro(messageEncryptBean.getUserIntro()); + userInfoBean.setIsGroup(false); + DbUserHelper.insertUser(userInfoBean); + + return messageBean; + } + + /** + * 发送消息时创建实体嘞 + */ + public static MessageBean sendMsgCreate(EMMessage message, MessageEncryptBean messageEncryptBean){ + UserInfoBean userInfoBean = DbUserHelper.queryUser2Account(message.getTo()); + + MessageBean messageBean = new MessageBean(); + + messageBean.setMsgId(message.getMsgId()); + messageBean.setAttribute(MMKVUserUtils.getInstance().getUserAccount()); + messageBean.setOtherSide(message.getTo()); + messageBean.setFromUserTag(MMKVUserUtils.getInstance().getUserInfoTag()); + messageBean.setFromAccount(MMKVUserUtils.getInstance().getUserAccount()); + messageBean.setFromName(MMKVUserUtils.getInstance().getUserName()); + messageBean.setFromHead(MMKVUserUtils.getInstance().getUserHead()); + messageBean.setToAccount(message.getTo()); + if (userInfoBean!= null){ + messageBean.setToUserTag(userInfoBean.getUserTag()); + messageBean.setToName(userInfoBean.getUserName()); + messageBean.setToHead(userInfoBean.getUserHead()); + } + messageBean.setMsgTextContent(messageEncryptBean.getTextMsgContent()); +// messageBean.setMsgImgContent(); + messageBean.setMsgTime(message.getMsgTime()); + messageBean.setGroupAckCount(0); + messageBean.setChatType(IMConstant.makeChatType(message.getChatType())); + messageBean.setMsgType(IMConstant.makeMessageType(message.getType())); + messageBean.setStatus(IMConstant.makeMsgStatus(message.status())); + messageBean.setDirection(IMConstant.makeMsgDirect(message.direct())); + + DbMsgHelper.insertMsg(messageBean); + return messageBean; + } + + /** + * 游标切换 + */ + public static ArrayList cursorMsgToList(Cursor cursor, Class objClass){ + + ArrayList objList = new ArrayList(); + + int idIndex = cursor.getColumnIndex(MessageBeanDao.Properties.Id.columnName); + int msgIdIndex = cursor.getColumnIndex(MessageBeanDao.Properties.MsgId.columnName); + int attributeIndex = cursor.getColumnIndex(MessageBeanDao.Properties.Attribute.columnName); + int otherSideIndex = cursor.getColumnIndex(MessageBeanDao.Properties.OtherSide.columnName); + int fromUserTagIndex = cursor.getColumnIndex(MessageBeanDao.Properties.FromUserTag.columnName); + int fromAccountIndex = cursor.getColumnIndex(MessageBeanDao.Properties.FromAccount.columnName); + int fromNameIndex = cursor.getColumnIndex(MessageBeanDao.Properties.FromName.columnName); + int fromHeadIndex = cursor.getColumnIndex(MessageBeanDao.Properties.FromHead.columnName); + int toUserTagIndex = cursor.getColumnIndex(MessageBeanDao.Properties.ToUserTag.columnName); + int toAccountIndex = cursor.getColumnIndex(MessageBeanDao.Properties.ToAccount.columnName); + int toNameIndex = cursor.getColumnIndex(MessageBeanDao.Properties.ToName.columnName); + int toHeadIndex = cursor.getColumnIndex(MessageBeanDao.Properties.ToHead.columnName); + int msgTextContentIndex = cursor.getColumnIndex(MessageBeanDao.Properties.MsgTextContent.columnName); + int msgImgContentIndex = cursor.getColumnIndex(MessageBeanDao.Properties.MsgImgContent.columnName); + int msgTimeIndex = cursor.getColumnIndex(MessageBeanDao.Properties.MsgTime.columnName); + int groupAckCountIndex = cursor.getColumnIndex(MessageBeanDao.Properties.GroupAckCount.columnName); + int chatTypeIndex = cursor.getColumnIndex(MessageBeanDao.Properties.ChatType.columnName); + int msgTypeIndex = cursor.getColumnIndex(MessageBeanDao.Properties.MsgType.columnName); + int statusIndex = cursor.getColumnIndex(MessageBeanDao.Properties.Status.columnName); + int directionIndex = cursor.getColumnIndex(MessageBeanDao.Properties.Direction.columnName); + + while (cursor.moveToNext()){ + MessageBean messageBean = new MessageBean(); + + messageBean.setId(cursor.getLong(idIndex)); + messageBean.setMsgId(cursor.getString(msgIdIndex)); + messageBean.setAttribute(cursor.getString(attributeIndex)); + messageBean.setOtherSide(cursor.getString(otherSideIndex)); + messageBean.setFromUserTag(cursor.getString(fromUserTagIndex)); + messageBean.setFromAccount(cursor.getString(fromAccountIndex)); + messageBean.setFromName(cursor.getString(fromNameIndex)); + messageBean.setFromHead(cursor.getString(fromHeadIndex)); + messageBean.setToUserTag(cursor.getString(toUserTagIndex)); + messageBean.setToAccount(cursor.getString(toAccountIndex)); + messageBean.setToName(cursor.getString(toNameIndex)); + messageBean.setToHead(cursor.getString(toHeadIndex)); + messageBean.setMsgTextContent(cursor.getString(msgTextContentIndex)); + messageBean.setMsgImgContent(cursor.getString(msgImgContentIndex)); + messageBean.setMsgTime(cursor.getLong(msgTimeIndex)); + messageBean.setGroupAckCount(cursor.getInt(groupAckCountIndex)); + messageBean.setChatType(cursor.getInt(chatTypeIndex)); + messageBean.setMsgType(cursor.getInt(msgTypeIndex)); + messageBean.setStatus(cursor.getInt(statusIndex)); + messageBean.setDirection(cursor.getInt(directionIndex)); + + objList.add(messageBean); + } + return objList; + } +} diff --git a/app/src/main/java/com/example/alcoholic/im/MsgGetListener.java b/app/src/main/java/com/example/alcoholic/im/MsgGetListener.java new file mode 100644 index 0000000..3a97be6 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/im/MsgGetListener.java @@ -0,0 +1,72 @@ +package com.example.alcoholic.im; + +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.db.DbMsgHelper; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.google.gson.Gson; +import com.hyphenate.EMMessageListener; +import com.hyphenate.chat.EMMessage; +import com.hyphenate.chat.EMMessageBody; +import com.orhanobut.logger.Logger; + +import org.greenrobot.eventbus.EventBus; + +import java.security.MessageDigest; +import java.util.List; + +/** + * Created by + * Description: + * on 2020/11/16. + */ +public class MsgGetListener implements EMMessageListener { + @Override + public void onMessageReceived(List messages) { + //收到消息 + + new Thread(){ + @Override + public void run() { + super.run(); + MessageBean messageBean = ImBeanChangeHelper.msgBeanChange(messages.get(0)); + + DbMsgHelper.insertMsg(messageBean); + + EventBus.getDefault().post(messageBean); + } + }.start(); + + } + + @Override + public void onCmdMessageReceived(List messages) { + //收到透传消息 + + } + + @Override + public void onMessageRead(List messages) { + //收到已读回执 + + } + + @Override + public void onMessageDelivered(List messages) { + //收到已送达回执 + + } + + @Override + public void onMessageRecalled(List messages) { + //消息被撤回 + + } + + @Override + public void onMessageChanged(EMMessage message, Object change) { + //消息状态变动 + + } + +} + diff --git a/app/src/main/java/com/example/alcoholic/im/MyConnectionListener.java b/app/src/main/java/com/example/alcoholic/im/MyConnectionListener.java new file mode 100644 index 0000000..f48f155 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/im/MyConnectionListener.java @@ -0,0 +1,32 @@ +package com.example.alcoholic.im; + +import android.content.Intent; + +import com.example.alcoholic.ui.acitivity.MainActivity; +import com.hjq.toast.ToastUtils; +import com.hyphenate.EMConnectionListener; +import com.hyphenate.EMError; +import com.hyphenate.util.EMLog; +import com.orhanobut.logger.Logger; + +/** + * Created by + * Description:链接状态监听 + * on 2020/11/23. + */ +public class MyConnectionListener implements EMConnectionListener { + + + @Override + public void onConnected() { + + } + + @Override + public void onDisconnected(int errorCode) { + ToastUtils.show("链接已断开,将导致无法正常使用,请检查网络配置后重新启动 App"); + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/im/SendMsg.java b/app/src/main/java/com/example/alcoholic/im/SendMsg.java new file mode 100644 index 0000000..8b33e8c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/im/SendMsg.java @@ -0,0 +1,122 @@ +package com.example.alcoholic.im; + +import android.content.Context; + +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.bean.MessageEncryptBean; +import com.example.alcoholic.constant.PConstant; +import com.example.alcoholic.db.DbMsgHelper; +import com.example.alcoholic.utils.AESUtils; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.google.gson.Gson; +import com.hyphenate.EMCallBack; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMMessage; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +/** + * Created by + * Description: 发送消息 + * on 2020/11/13. + */ +public class SendMsg { + + /** + * 发送消息 + */ + public static MessageBean sendText(String content,String toChatUsername){ + return sendText(content,toChatUsername,IMConstant.CHAT_TYPE_CHAT); + } + + /** + * 发送消息 + */ + public static MessageBean sendText(String content,String toChatUsername,int chatType){ + MessageEncryptBean messageEncryptBean = new MessageEncryptBean(); + messageEncryptBean.setUserTag(MMKVUserUtils.getInstance().getUserInfoTag()); + messageEncryptBean.setUserName(MMKVUserUtils.getInstance().getUserName()); + messageEncryptBean.setUserHead(MMKVUserUtils.getInstance().getUserHead()); + messageEncryptBean.setUserIntro(MMKVUserUtils.getInstance().getUserIntro()); + messageEncryptBean.setTextMsgContent(content); + + String enContent; + try { + enContent = AESUtils.encrypt(MMKVStytemUtils.getInstance().getAesKey(),new Gson().toJson(messageEncryptBean)); + } catch (Exception e) { + enContent = PConstant.fartQuotes(); + e.printStackTrace(); + } + + //创建一条文本消息,content为消息文字内容,toChatUsername为对方用户或者群聊的id,后文皆是如此 + EMMessage message = EMMessage.createTxtSendMessage(enContent, toChatUsername); + //如果是群聊,设置chattype,默认是单聊 + switch (chatType){ + case IMConstant.CHAT_TYPE_CHAT: + message.setChatType(EMMessage.ChatType.Chat); + break; + case IMConstant.CHAT_TYPE_GROUPCHAT: + message.setChatType(EMMessage.ChatType.GroupChat); + break; + case IMConstant.CHAT_TYPE_CHATROOM: + message.setChatType(EMMessage.ChatType.ChatRoom); + break; + default: + message.setChatType(EMMessage.ChatType.Chat); + break; + } + //发送消息 + EMClient.getInstance().chatManager().sendMessage(message); + + return ImBeanChangeHelper.sendMsgCreate(message,messageEncryptBean); + } + + + /** + * 发送好友请求 + */ + public static void sendFriendAdd(String toAddUsername,String why,final EMCallBack callback){ + String reason; + + MessageEncryptBean messageEncryptBean = new MessageEncryptBean(); + messageEncryptBean.setUserTag(MMKVUserUtils.getInstance().getUserInfoTag()); + messageEncryptBean.setUserName(MMKVUserUtils.getInstance().getUserName()); + messageEncryptBean.setUserHead(MMKVUserUtils.getInstance().getUserHead()); + messageEncryptBean.setUserIntro(MMKVUserUtils.getInstance().getUserIntro()); + messageEncryptBean.setTextMsgContent(why); + + try { + reason = AESUtils.encrypt(MMKVStytemUtils.getInstance().getAesKey(),new Gson().toJson(messageEncryptBean)); + } catch (Exception e) { + reason = PConstant.fartQuotes(); + e.printStackTrace(); + } + + //参数为要添加的好友的username和添加理由 + EMClient.getInstance().contactManager().aysncAddContact(toAddUsername, + reason, + callback); + } + + + /** + * 删除好友 + */ + public static void deleteFriend(String username,final EMCallBack callback){ + + + EMClient.getInstance().contactManager().aysncDeleteContact(username,callback); + + } + +} diff --git a/app/src/main/java/com/example/alcoholic/other/AppConfig.java b/app/src/main/java/com/example/alcoholic/other/AppConfig.java new file mode 100644 index 0000000..f81c76d --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/other/AppConfig.java @@ -0,0 +1,74 @@ +package com.example.alcoholic.other; + + +import android.app.ActivityManager; +import android.app.Application; +import android.content.Context; +import android.content.pm.PackageManager; + +import com.example.alcoholic.BuildConfig; + +import java.util.Iterator; +import java.util.List; + +import static android.content.Context.ACTIVITY_SERVICE; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/02 + * desc : App 配置管理类 + */ +public final class AppConfig { + + /** + * 当前是否为 Debug 模式 + */ + public static boolean isDebug() { + return BuildConfig.DEBUG; + } + + /** + * 获取当前应用的包名 + */ + public static String getPackageName() { + return BuildConfig.APPLICATION_ID; + } + + /** + * 获取当前应用的版本名 + */ + public static String getVersionName() { + return BuildConfig.VERSION_NAME; + } + + /** + * 获取当前应用的版本码 + */ + public static int getVersionCode() { + return BuildConfig.VERSION_CODE; + } + + /** + * 获取appname + */ + public static String getAppName(Context context, int pID) { + String processName = null; + ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); + List l = am.getRunningAppProcesses(); + Iterator i = l.iterator(); + PackageManager pm = context.getPackageManager(); + while (i.hasNext()) { + ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next()); + try { + if (info.pid == pID) { + processName = info.processName; + return processName; + } + } catch (Exception e) { + // Log.d("Process", "Error>> :"+ e.toString()); + } + } + return processName; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/other/GridSpaceDecoration.java b/app/src/main/java/com/example/alcoholic/other/GridSpaceDecoration.java new file mode 100644 index 0000000..226f40a --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/other/GridSpaceDecoration.java @@ -0,0 +1,50 @@ +package com.example.alcoholic.other; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/07/25 + * desc : 图片选择列表分割线 + */ +public final class GridSpaceDecoration extends RecyclerView.ItemDecoration { + + private final int mSpace; + + public GridSpaceDecoration(int space) { + mSpace = space; + } + + @Override + public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.State state) {} + + @SuppressWarnings("all") + @Override + public void getItemOffsets(@NonNull Rect rect, @NonNull View view, RecyclerView recyclerView, @NonNull RecyclerView.State state) { + int position = recyclerView.getChildAdapterPosition(view); + int spanCount = ((GridLayoutManager) recyclerView.getLayoutManager()).getSpanCount(); + + // 每一行的最后一个才留出右边间隙 + if ((position + 1) % spanCount == 0) { + rect.right = mSpace; + } + + // 只有第一行才留出顶部间隙 + if (position < spanCount) { + rect.top = mSpace; + } + + rect.bottom = mSpace; + rect.left = mSpace; + } + + @Override + public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.State state) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/other/KeyboardWatcher.java b/app/src/main/java/com/example/alcoholic/other/KeyboardWatcher.java new file mode 100644 index 0000000..6f3a750 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/other/KeyboardWatcher.java @@ -0,0 +1,137 @@ +package com.example.alcoholic.other; + +import android.app.Activity; +import android.app.Application; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/07/04 + * desc : 软键盘监听类 + */ +public final class KeyboardWatcher implements + ViewTreeObserver.OnGlobalLayoutListener, + Application.ActivityLifecycleCallbacks { + + private Activity mActivity; + private View mContentView; + private SoftKeyboardStateListener mListeners; + private boolean isSoftKeyboardOpened; + private int mStatusBarHeight; + + public static KeyboardWatcher with(Activity activity) { + return new KeyboardWatcher(activity); + } + + private KeyboardWatcher(Activity activity) { + mActivity = activity; + mContentView = activity.findViewById(Window.ID_ANDROID_CONTENT); + + mActivity.getApplication().registerActivityLifecycleCallbacks(this); + mContentView.getViewTreeObserver().addOnGlobalLayoutListener(this); + + // 获取 status_bar_height 资源的 ID + int resourceId = mActivity.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + //根据资源 ID 获取响应的尺寸值 + mStatusBarHeight = mActivity.getResources().getDimensionPixelSize(resourceId); + } + } + + /** + * {@link ViewTreeObserver.OnGlobalLayoutListener} + */ + + @Override + public void onGlobalLayout() { + final Rect r = new Rect(); + //r will be populated with the coordinates of your view that area still visible. + mContentView.getWindowVisibleDisplayFrame(r); + + final int heightDiff = mContentView.getRootView().getHeight() - (r.bottom - r.top); + if (!isSoftKeyboardOpened && heightDiff > mContentView.getRootView().getHeight() / 4) { + isSoftKeyboardOpened = true; + if ((mActivity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != WindowManager.LayoutParams.FLAG_FULLSCREEN) { + if (mListeners != null) { + mListeners.onSoftKeyboardOpened(heightDiff - mStatusBarHeight); + } + } else { + if (mListeners != null) { + mListeners.onSoftKeyboardOpened(heightDiff); + } + } + + } else if (isSoftKeyboardOpened && heightDiff < mContentView.getRootView().getHeight() / 4) { + isSoftKeyboardOpened = false; + if (mListeners != null) { + mListeners.onSoftKeyboardClosed(); + } + } + } + + /** + * 设置软键盘弹出监听 + */ + public void setListener(SoftKeyboardStateListener listener) { + mListeners = listener; + } + + /** + * {@link Application.ActivityLifecycleCallbacks} + */ + + @Override + public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(@NonNull Activity activity) {} + + @Override + public void onActivityResumed(@NonNull Activity activity) {} + + @Override + public void onActivityPaused(@NonNull Activity activity) {} + + @Override + public void onActivityStopped(@NonNull Activity activity) {} + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + if (mActivity == activity) { + mActivity.getApplication().unregisterActivityLifecycleCallbacks(this); + mContentView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + mActivity = null; + mContentView = null; + mListeners = null; + } + } + + /** + * 软键盘状态监听器 + */ + public interface SoftKeyboardStateListener { + + /** + * 软键盘弹出了 + * @param keyboardHeight 软键盘高度 + */ + void onSoftKeyboardOpened(int keyboardHeight); + + /** + * 软键盘收起了 + */ + void onSoftKeyboardClosed(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/ChatActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/ChatActivity.java new file mode 100644 index 0000000..ce5e929 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/ChatActivity.java @@ -0,0 +1,262 @@ +package com.example.alcoholic.ui.acitivity; + +import android.content.Intent; +import android.os.Bundle; +import android.renderscript.RenderScript; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.listener.OnUpFetchListener; +import com.chad.library.adapter.base.listener.UpFetchListenerImp; +import com.chad.library.adapter.base.module.BaseUpFetchModule; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.common.ImActivity; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.Constants; +import com.example.alcoholic.db.DbMsgHelper; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.helper.InputTextHelper; +import com.example.alcoholic.im.IMConstant; +import com.example.alcoholic.im.SendMsg; +import com.example.alcoholic.ui.acitivity.user.GroupHomeActivity; +import com.example.alcoholic.ui.acitivity.user.UserHomeActivity; +import com.example.alcoholic.ui.adapter.MsgListAdapter; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.orhanobut.logger.Logger; + +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_CHAT_TYPE; +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description: + * on 2020/11/13. + */ +public class ChatActivity extends ImActivity { + + private RecyclerView mRvMsg; + private EditText mEtInput; + private TextView mBtnSend; + + private MsgListAdapter mMsgAdapter; + private LinearLayoutManager mLayoutManager; + + private int pageNum = 0; + private String mToUserAccount; + /* 会话类型 */ + private int mChatType = IMConstant.CHAT_TYPE_CHAT; + + @Override + protected int getLayoutId() { + return R.layout.activity_chat; + } + + @Override + protected void initView() { + getStatusBarConfig().keyboardEnable(true).init(); + + mRvMsg = findViewById(R.id.chat_rv_msgList); + mEtInput = findViewById(R.id.chat_et_input); + mBtnSend = findViewById(R.id.chat_btn_send); + + setOnClickListener(mBtnSend); + + + initList(); + + //输入框按钮监听 + InputTextHelper.with(this).setMain(mBtnSend).addView(mEtInput).build(); + + } + + @Override + protected void initData() { + if (getIntent() != null && getIntent().getExtras() != null){ + mToUserAccount = getIntent().getExtras().getString(JUMP_DATA_USER_ACCOUNT,""); + mChatType = getIntent().getExtras().getInt(JUMP_DATA_CHAT_TYPE,IMConstant.CHAT_TYPE_CHAT); + mMsgAdapter.setChatType(mChatType); + } + if (EmptyUtils.isEmpty(mToUserAccount)){ + toast("会话异常"); + finish(); + } + + + //标题栏 + setTitle(DbUserHelper.getUserShowName(mToUserAccount,DbUserHelper.getUserName(mToUserAccount))); + + + mMsgAdapter.addData(DbMsgHelper.queryMsg2User(pageNum,mToUserAccount)); + scrollBottom(true,false); + } + + /** + * 接收到新消息 + */ + @Override + protected void onReNewMsg(MessageBean newMsgBean) { + if (newMsgBean.getOtherSide().equals(mToUserAccount)){ + mMsgAdapter.addData(newMsgBean); + scrollBottom(); + } + } + + private void initList(){ + mMsgAdapter = new MsgListAdapter(); + mLayoutManager = new LinearLayoutManager(this); + mRvMsg.setLayoutManager(mLayoutManager); + mRvMsg.setAdapter(mMsgAdapter); + + + //软键盘弹出列表下滑 + mRvMsg.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (bottom < oldBottom) { + scrollBottom(false,false);//滑动到底部 + } + } + }); + + //加载监听 + mMsgAdapter.getUpFetchModule().setUpFetchEnable(true); + mMsgAdapter.getUpFetchModule().setStartUpFetchPosition((int) (Constants.LOAD_LIMIT * 0.9)); + mMsgAdapter.getUpFetchModule().setOnUpFetchListener(new OnUpFetchListener() { + @Override + public void onUpFetch() { + //当数据加载完之后再加载 + if (mMsgAdapter.getItemCount() >= Constants.LOAD_LIMIT) { + loadMoreData(); + } + + } + }); + } + + @Override + public void onRightClick(View v) { + Intent intent; + if (mChatType == IMConstant.CHAT_TYPE_CHAT){ + intent = new Intent(this,UserHomeActivity.class); + }else { + intent = new Intent(this, GroupHomeActivity.class); + } + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,mToUserAccount); + intent.putExtras(bundle); + startActivity(intent); + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.chat_btn_send: + //发送消息 + if (EmptyUtils.isEmpty(getEtInput())){ + toast("请不要发送空消息"); + } + sendMsg(); + break; + } + } + + /** + * 发送消息 + */ + private void sendMsg(){ + EventBus.getDefault().post(SendMsg.sendText(getEtInput(),mToUserAccount,mChatType)); + + mEtInput.setText(""); + scrollBottom(); + } + + /** + * 页面加载 + */ + private void loadMoreData(){ + pageNum++; + //开启加载动画 + mMsgAdapter.getUpFetchModule().setUpFetching(true); + mRvMsg.postDelayed(new Runnable() { + @Override + public void run() { + + List moreMsg = DbMsgHelper.queryMsg2User(pageNum,mToUserAccount); + + mMsgAdapter.addData(0,moreMsg); + + //暂停加载动画 + mMsgAdapter.getUpFetchModule().setUpFetching(false); + + + if (moreMsg.size() < Constants.LOAD_LIMIT){ + mMsgAdapter.getUpFetchModule().setUpFetchEnable(false); + } + } + },300); + + + } + + + + /** + * 获取输入的消息内容 + */ + private String getEtInput() { + return mEtInput.getText().toString().trim(); + } + + + /** + * 列表滚动到底部 + * + */ + private void scrollBottom(){ + scrollBottom(false,true); + } + + private void scrollBottom(boolean isGoDown){ + scrollBottom(isGoDown,true); + } + + /** + * 列表滚动到底部 + * @param isGoDown 是否直接到底部 + */ + private void scrollBottom(boolean isGoDown,boolean isAnim){ + if (mRvMsg == null) return; + + if ((mLayoutManager.findLastVisibleItemPosition() < (mMsgAdapter.getItemCount() - 5)) && !isGoDown) { + return; + } + + mRvMsg.post(new Runnable() { + @Override + public void run() { + if (mMsgAdapter.getItemCount() > 0) { + //是否平滑移动 + if (isAnim){ + mRvMsg.smoothScrollToPosition(mMsgAdapter.getItemCount() - 1); + }else { + mRvMsg.scrollToPosition(mMsgAdapter.getItemCount() - 1); + } + } + } + }); + + } +} + diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/HomeActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/HomeActivity.java new file mode 100644 index 0000000..c2b7f0c --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/HomeActivity.java @@ -0,0 +1,171 @@ +package com.example.alcoholic.ui.acitivity; + +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.View; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.common.MyFragment; +import com.example.alcoholic.helper.ActivityStackManager; +import com.example.alcoholic.helper.DoubleClickHelper; +import com.example.alcoholic.im.ContactListener; +import com.example.alcoholic.im.MsgGetListener; +import com.example.alcoholic.im.MyConnectionListener; +import com.example.alcoholic.other.KeyboardWatcher; +import com.example.alcoholic.ui.fragment.FindFragment; +import com.example.alcoholic.ui.fragment.HomeFragment; +import com.example.alcoholic.ui.fragment.MeFragment; +import com.example.alcoholic.ui.fragment.MessageFragment; +import com.example.base.BaseFragmentAdapter; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.hyphenate.chat.EMClient; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.ViewPager; + +/** + * Created by + * Description: 主界面 + * on 2020/11/13. + */ +public class HomeActivity extends MyActivity + implements BottomNavigationView.OnNavigationItemSelectedListener, + KeyboardWatcher.SoftKeyboardStateListener { + + private ViewPager mViewPager; + private BottomNavigationView mBottomNavigationView; + + private BaseFragmentAdapter mPagerAdapter; + + /* 链接监听 */ + private MyConnectionListener mMyConnectionListener; + /* 消息接收监听 */ + private MsgGetListener mMsgGetListener; + /* 好友操作监听 */ + private ContactListener mContactListener; + + @Override + protected int getLayoutId() { + return R.layout.activity_home; + } + + @Override + protected void initView() { + mViewPager = findViewById(R.id.home_pager); + mBottomNavigationView = findViewById(R.id.home_navigation); + + // 不使用图标默认变色 + mBottomNavigationView.setItemIconTintList(null); + mBottomNavigationView.setOnNavigationItemSelectedListener(this); + + KeyboardWatcher.with(this) + .setListener(this); + + initIm();//初始化im + } + + + private void initIm(){ + mMyConnectionListener = new MyConnectionListener(); + EMClient.getInstance().addConnectionListener(mMyConnectionListener); + mMsgGetListener = new MsgGetListener(); + EMClient.getInstance().chatManager().addMessageListener(mMsgGetListener); + mContactListener = new ContactListener(); + EMClient.getInstance().contactManager().setContactListener(mContactListener); + + + //加载群组和会话消息 + EMClient.getInstance().groupManager().loadAllGroups(); + EMClient.getInstance().chatManager().loadAllConversations(); + } + + + @Override + protected void initData() { + mPagerAdapter = new BaseFragmentAdapter<>(this); + mPagerAdapter.addFragment(HomeFragment.newInstance()); + mPagerAdapter.addFragment(FindFragment.newInstance()); + mPagerAdapter.addFragment(MessageFragment.newInstance()); + mPagerAdapter.addFragment(MeFragment.newInstance()); + + // 设置成懒加载模式 + mPagerAdapter.setLazyMode(true); + mViewPager.setAdapter(mPagerAdapter); + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_home: + mViewPager.setCurrentItem(0); + return true; + case R.id.home_found: + mViewPager.setCurrentItem(1); + return true; + case R.id.home_message: + mViewPager.setCurrentItem(2); + return true; + case R.id.home_me: + mViewPager.setCurrentItem(3); + return true; + default: + break; + } + return false; + } + + @Override + public void onSoftKeyboardOpened(int keyboardHeight) { + mBottomNavigationView.setVisibility(View.GONE); + } + + @Override + public void onSoftKeyboardClosed() { + mBottomNavigationView.setVisibility(View.VISIBLE); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // 回调当前 Fragment 的 onKeyDown 方法 + if (mPagerAdapter.getShowFragment().onKeyDown(keyCode, event)) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public void onBackPressed() { + if (DoubleClickHelper.isOnDoubleClick()) { + // 移动到上一个任务栈,避免侧滑引起的不良反应 + moveTaskToBack(false); + postDelayed(() -> { + + // 进行内存优化,销毁掉所有的界面 + ActivityStackManager.getInstance().finishAllActivities(); + // 销毁进程(注意:调用此 API 可能导致当前 Activity onDestroy 方法无法正常回调) + // System.exit(0); + + }, 300); + } else { + toast(R.string.home_exit_hint); + } + } + + @Override + protected void onDestroy() { + mViewPager.setAdapter(null); + mBottomNavigationView.setOnNavigationItemSelectedListener(null); + // 记得在不需要的时候移除listener,如在activity的onDestroy()时 + EMClient.getInstance().chatManager().removeMessageListener(mMsgGetListener); + super.onDestroy(); + } + + @Override + public boolean isSwipeEnable() { + return false; + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/MainActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/MainActivity.java new file mode 100644 index 0000000..0b3feab --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/MainActivity.java @@ -0,0 +1,21 @@ +package com.example.alcoholic.ui.acitivity; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import com.example.alcoholic.R; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/SplashActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/SplashActivity.java new file mode 100644 index 0000000..ae77a3d --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/SplashActivity.java @@ -0,0 +1,57 @@ +package com.example.alcoholic.ui.acitivity; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.ui.acitivity.user.LoginActivity; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.gyf.immersionbar.BarHide; +import com.gyf.immersionbar.ImmersionBar; +import com.hyphenate.chat.EMClient; + +import androidx.annotation.NonNull; + +/** + * Created by + * Description:闪屏页面 + * on 2020/11/16. + */ +public class SplashActivity extends MyActivity { + @Override + protected int getLayoutId() { + return R.layout.activity_splash; + } + + @Override + protected void initView() { + + + + if (MMKVUserUtils.getInstance().isLogin()){ + //已登录 + startActivity(HomeActivity.class); + finish(); + }else { + //没有登录 + startActivity(LoginActivity.class); + finish(); + } + + + } + + @Override + protected void initData() { + + } + + + @NonNull + @Override + protected ImmersionBar createStatusBarConfig() { + return super.createStatusBarConfig() + // 隐藏状态栏和导航栏 + .hideBar(BarHide.FLAG_HIDE_BAR); + } + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/image/CameraActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/image/CameraActivity.java new file mode 100644 index 0000000..d9bc012 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/image/CameraActivity.java @@ -0,0 +1,173 @@ +package com.example.alcoholic.ui.acitivity.image; + +import android.content.Intent; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; + +import com.example.alcoholic.R; +import com.example.alcoholic.aop.Permissions; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.JumpDataContants; +import com.example.alcoholic.other.AppConfig; +import com.example.base.BaseActivity; +import com.hjq.permissions.Permission; +import com.hjq.permissions.XXPermissions; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/18 + * desc : 拍照选择(拍摄图片、视频) + */ +public final class CameraActivity extends MyActivity { + + private static final int CAMERA_REQUEST_CODE = 1024; + + public static void start(BaseActivity activity, OnCameraListener listener) { + start(activity, false, listener); + } + + @Permissions({Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE, Permission.CAMERA}) + public static void start(BaseActivity activity, boolean video, OnCameraListener listener) { + File file = createCameraFile(video); + Intent intent = new Intent(activity, CameraActivity.class); + intent.putExtra(JumpDataContants.FILE, file); + intent.putExtra(JumpDataContants.VIDEO, video); + activity.startActivityForResult(intent, (resultCode, data) -> { + + if (listener == null) { + return; + } + + if (resultCode == RESULT_OK) { + listener.onSelected(file); + } else { + listener.onCancel(); + } + }); + } + + private File mFile; + + @Override + protected int getLayoutId() { + return 0; + } + + @Override + protected void initView() { + + } + + @Override + protected void initData() { + Intent intent; + // 启动系统相机 + if (getBoolean(JumpDataContants.VIDEO)) { + // 录制视频 + intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + } else { + // 拍摄照片 + intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + } + if (XXPermissions.hasPermission(this, Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE, Permission.CAMERA) + && intent.resolveActivity(getPackageManager()) != null) { + mFile = getSerializable(JumpDataContants.FILE); + if (mFile != null && mFile.exists()) { + + Uri imageUri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // 通过 FileProvider 创建一个 Content 类型的 Uri 文件 + imageUri = FileProvider.getUriForFile(this, AppConfig.getPackageName() + ".provider", mFile); + } else { + imageUri = Uri.fromFile(mFile); + } + // 对目标应用临时授权该 Uri 所代表的文件 + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + // 将拍取的照片保存到指定 Uri + intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); + startActivityForResult(intent, CAMERA_REQUEST_CODE); + } else { + toast(R.string.camera_image_error); + finish(); + } + } else { + toast(R.string.camera_launch_fail); + finish(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CAMERA_REQUEST_CODE) { + switch (resultCode) { + case RESULT_OK: + // 重新扫描多媒体(否则可能扫描不到) + MediaScannerConnection.scanFile(getApplicationContext(), new String[]{mFile.getPath()}, null,null); + break; + case RESULT_CANCELED: + // 删除这个文件 + mFile.delete(); + break; + default: + break; + } + setResult(resultCode); + finish(); + } + } + + /** + * 创建一个拍照图片文件对象 + */ + @SuppressWarnings("ResultOfMethodCallIgnored") + private static File createCameraFile(boolean video) { + File folder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera"); + if (!folder.exists() || !folder.isDirectory()) { + if (!folder.mkdirs()) { + folder = Environment.getExternalStorageDirectory(); + } + } + + try { + File file = new File(folder, (video ? "IMG_" : "VID") + new SimpleDateFormat("_yyyyMMdd_HHmmss.", Locale.getDefault()).format(new Date()) + (video ? "mp4" : "jpg")); + file.createNewFile(); + return file; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 拍照选择监听 + */ + public interface OnCameraListener { + + /** + * 选择回调 + * + * @param file 文件 + */ + void onSelected(File file); + + /** + * 取消回调 + */ + default void onCancel() {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/image/ImagePreviewActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/image/ImagePreviewActivity.java new file mode 100644 index 0000000..a96492b --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/image/ImagePreviewActivity.java @@ -0,0 +1,92 @@ +package com.example.alcoholic.ui.acitivity.image; + +import android.content.Context; +import android.content.Intent; + +import com.example.alcoholic.R; +import com.example.alcoholic.aop.CheckNet; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.JumpDataContants; +import com.example.alcoholic.ui.page.ImagePagerAdapter; +import com.gyf.immersionbar.BarHide; +import com.gyf.immersionbar.ImmersionBar; +import com.rd.PageIndicatorView; + +import java.util.ArrayList; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.ViewPager; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/03/05 + * desc : 查看大图 + */ +public final class ImagePreviewActivity extends MyActivity { + + public static void start(Context context, String url) { + ArrayList images = new ArrayList<>(1); + images.add(url); + start(context, images); + } + + public static void start(Context context, ArrayList urls) { + start(context, urls, 0); + } + + @CheckNet + public static void start(Context context, ArrayList urls, int index) { + Intent intent = new Intent(context, ImagePreviewActivity.class); + intent.putExtra(JumpDataContants.IMAGE, urls); + intent.putExtra(JumpDataContants.INDEX, index); + context.startActivity(intent); + } + + private ViewPager mViewPager; + private PageIndicatorView mIndicatorView; + + @Override + protected int getLayoutId() { + return R.layout.activity_image_preview; + } + + @Override + protected void initView() { + mViewPager = findViewById(R.id.vp_image_preview_pager); + mIndicatorView = findViewById(R.id.pv_image_preview_indicator); + mIndicatorView.setViewPager(mViewPager); + } + + @Override + protected void initData() { + ArrayList images = getStringArrayList(JumpDataContants.IMAGE); + int index = getInt(JumpDataContants.INDEX); + if (images != null && images.size() > 0) { + mViewPager.setAdapter(new ImagePagerAdapter(this, images)); + if (index != 0 && index <= images.size()) { + mViewPager.setCurrentItem(index); + } + } else { + finish(); + } + } + + @NonNull + @Override + protected ImmersionBar createStatusBarConfig() { + return super.createStatusBarConfig() + // 隐藏状态栏和导航栏 + .hideBar(BarHide.FLAG_HIDE_BAR); + } + + @Override + public boolean isStatusBarDarkFont() { + return false; + } + + @Override + public boolean isSwipeEnable() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/image/ImageSelectActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/image/ImageSelectActivity.java new file mode 100644 index 0000000..eb37cd5 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/image/ImageSelectActivity.java @@ -0,0 +1,459 @@ +package com.example.alcoholic.ui.acitivity.image; + +import android.content.ContentResolver; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.View; +import android.view.animation.AnimationUtils; + +import com.example.alcoholic.R; +import com.example.alcoholic.action.StatusAction; +import com.example.alcoholic.aop.Permissions; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.JumpDataContants; +import com.example.alcoholic.other.GridSpaceDecoration; +import com.example.alcoholic.ui.acitivity.MainActivity; +import com.example.alcoholic.ui.adapter.ImageSelectAdapter; +import com.example.alcoholic.ui.dialog.AlbumDialog; +import com.example.alcoholic.widget.HintLayout; +import com.example.base.BaseActivity; +import com.example.base.BaseAdapter; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.hjq.permissions.Permission; +import com.hjq.permissions.XXPermissions; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; +import top.zibin.luban.Luban; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/07/24 + * desc : 选择图片 + */ +public final class ImageSelectActivity extends MyActivity + implements StatusAction, Runnable, + BaseAdapter.OnItemClickListener, + BaseAdapter.OnItemLongClickListener, + BaseAdapter.OnChildClickListener { + + public static void start(BaseActivity activity, OnPhotoSelectListener listener) { + start(activity, 1, listener); + } + + @Permissions({Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE}) + public static void start(BaseActivity activity, int maxSelect, OnPhotoSelectListener listener) { + if (maxSelect < 1) { + // 最少要选择一个图片 + throw new IllegalArgumentException("are you ok?"); + } + Intent intent = new Intent(activity, ImageSelectActivity.class); + intent.putExtra(JumpDataContants.AMOUNT, maxSelect); + activity.startActivityForResult(intent, (resultCode, data) -> { + + if (listener == null || data == null) { + return; + } + + if (resultCode == RESULT_OK) { + lubanStudio(activity,listener,data.getStringArrayListExtra(JumpDataContants.IMAGE)); + } else { + listener.onCancel(); + } + }); + } + + private HintLayout mHintLayout; + private RecyclerView mRecyclerView; + private FloatingActionButton mFloatingView; + + private ImageSelectAdapter mAdapter; + + /** 最大选中 */ + private int mMaxSelect = 1; + /** 选中列表 */ + private final ArrayList mSelectImage = new ArrayList<>(); + + /** 全部图片 */ + private final ArrayList mAllImage = new ArrayList<>(); + /** 图片专辑 */ + private final HashMap> mAllAlbum = new HashMap<>(); + + @Override + protected int getLayoutId() { + return R.layout.activity_image_select; + } + + @Override + protected void initView() { + mHintLayout = findViewById(R.id.hl_image_select_hint); + mRecyclerView = findViewById(R.id.rv_image_select_list); + mFloatingView = findViewById(R.id.fab_image_select_floating); + setOnClickListener(mFloatingView); + + mAdapter = new ImageSelectAdapter(this, mSelectImage); + mAdapter.setOnChildClickListener(R.id.fl_image_select_check, this); + mAdapter.setOnItemClickListener(this); + mAdapter.setOnItemLongClickListener(this); + mRecyclerView.setAdapter(mAdapter); + // 禁用动画效果 + mRecyclerView.setItemAnimator(null); + // 添加分割线 + mRecyclerView.addItemDecoration(new GridSpaceDecoration((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()))); + } + + @SuppressWarnings("all") + @Override + protected void initData() { + // 获取最大的选择数 + mMaxSelect = getInt(JumpDataContants.AMOUNT, mMaxSelect); + + // 显示加载进度条 + showLoading(); + // 加载图片列表 + new Thread(ImageSelectActivity.this).start(); + } + + @Override + public HintLayout getHintLayout() { + return mHintLayout; + } + + @Override + public void onRightClick(View v) { + if (mAllImage.isEmpty()) { + return; + } + + ArrayList data = new ArrayList<>(mAllAlbum.size() + 1); + data.add(new AlbumDialog.AlbumInfo(mAllImage.get(0), getString(R.string.image_select_all), String.format(getString(R.string.image_select_total), mAllAlbum.size()), mAdapter.getData() == mAllImage)); + Set keys = mAllAlbum.keySet(); + for (String key : keys) { + List temp = mAllAlbum.get(key); + if (temp != null && !temp.isEmpty()) { + data.add(new AlbumDialog.AlbumInfo(temp.get(0), key, String.format(getString(R.string.image_select_total), temp.size()), mAdapter.getData() == temp)); + } + } + + new AlbumDialog.Builder(this) + .setData(data) + .setListener((dialog, position, bean) -> { + + setRightTitle(bean.getName()); + // 滚动回第一个位置 + mRecyclerView.scrollToPosition(0); + if (position == 0) { + mAdapter.setData(mAllImage); + } else { + mAdapter.setData(mAllAlbum.get(bean.getName())); + } + // 执行列表动画 + mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.from_right_layout)); + mRecyclerView.scheduleLayoutAnimation(); + }) + .show(); + } + + @Override + protected void onRestart() { + super.onRestart(); + // 遍历判断选择了的图片是否被删除了 + for (String path : mSelectImage) { + File file = new File(path); + if (!file.isFile()) { + + mSelectImage.remove(path); + mAllImage.remove(path); + + File parentFile = file.getParentFile(); + if (parentFile != null) { + List data = mAllAlbum.get(parentFile.getName()); + if (data != null) { + data.remove(path); + } + mAdapter.notifyDataSetChanged(); + + if (mSelectImage.isEmpty()) { + mFloatingView.setImageResource(R.drawable.camera_ic); + } else { + mFloatingView.setImageResource(R.drawable.succeed_ic); + } + } + } + } + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.fab_image_select_floating) { + if (mSelectImage.isEmpty()) { + // 点击拍照 + CameraActivity.start(this, file -> { + + // 当前选中图片的数量必须小于最大选中数 + if (mSelectImage.size() < mMaxSelect) { + mSelectImage.add(file.getPath()); + } + + // 这里需要延迟刷新,否则可能会找不到拍照的图片 + postDelayed(() -> { + // 重新加载图片列表 + new Thread(ImageSelectActivity.this).start(); + }, 1000); + }); + } else { + // 完成选择 + setResult(RESULT_OK, new Intent().putStringArrayListExtra(JumpDataContants.IMAGE, mSelectImage)); + finish(); + } + } + } + + /** + * {@link BaseAdapter.OnItemClickListener} + * @param recyclerView RecyclerView对象 + * @param itemView 被点击的条目对象 + * @param position 被点击的条目位置 + */ + @Override + public void onItemClick(RecyclerView recyclerView, View itemView, int position) { + if (mSelectImage.contains(mAdapter.getItem(position))) { + ImagePreviewActivity.start(getActivity(), mSelectImage, mSelectImage.indexOf(mAdapter.getItem(position))); + } else { + ImagePreviewActivity.start(getActivity(), mAdapter.getItem(position)); + } + } + + /** + * {@link BaseAdapter.OnItemLongClickListener} + * @param recyclerView RecyclerView对象 + * @param itemView 被点击的条目对象 + * @param position 被点击的条目位置 + */ + @Override + public boolean onItemLongClick(RecyclerView recyclerView, View itemView, int position) { + if (mSelectImage.size() < mMaxSelect) { + // 长按的时候模拟选中 + return itemView.findViewById(R.id.fl_image_select_check).performClick(); + } else { + return false; + } + } + + /** + * {@link BaseAdapter.OnChildClickListener} + * @param recyclerView RecyclerView对象 + * @param childView 被点击的条目子 View Id + * @param position 被点击的条目位置 + */ + @Override + public void onChildClick(RecyclerView recyclerView, View childView, int position) { + if (childView.getId() == R.id.fl_image_select_check) { + if (mSelectImage.contains(mAdapter.getItem(position))) { + mSelectImage.remove(mAdapter.getItem(position)); + + if (mSelectImage.isEmpty()) { + mFloatingView.hide(); + postDelayed(() -> { + mFloatingView.setImageResource(R.drawable.camera_ic); + mFloatingView.show(); + }, 200); + } + + } else { + + if (mMaxSelect == 1 && mSelectImage.size() == 1) { + + List data = mAdapter.getData(); + if (data != null) { + int index = data.indexOf(mSelectImage.get(0)); + if (index != -1) { + mSelectImage.remove(0); + mAdapter.notifyItemChanged(index); + } + } + mSelectImage.add(mAdapter.getItem(position)); + + } else if (mSelectImage.size() < mMaxSelect) { + + mSelectImage.add(mAdapter.getItem(position)); + + if (mSelectImage.size() == 1) { + mFloatingView.hide(); + postDelayed(() -> { + mFloatingView.setImageResource(R.drawable.succeed_ic); + mFloatingView.show(); + }, 200); + } + } else { + toast(String.format(getString(R.string.image_select_max_hint), mMaxSelect)); + } + } + mAdapter.notifyItemChanged(position); + } + } + + @Override + public void run() { + mAllAlbum.clear(); + mAllImage.clear(); + + final Uri contentUri = MediaStore.Files.getContentUri("external"); + final String sortOrder = MediaStore.Files.FileColumns.DATE_MODIFIED + " DESC"; + final String selection = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; + + ContentResolver contentResolver = getContentResolver(); + String[] projections = new String[]{MediaStore.Files.FileColumns._ID, MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.WIDTH, + MediaStore.MediaColumns.HEIGHT, MediaStore.MediaColumns.SIZE}; + + Cursor cursor = null; + if (XXPermissions.hasPermission(this, Permission.READ_EXTERNAL_STORAGE)) { + cursor = contentResolver.query(contentUri, projections, selection, new String[]{String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE)}, sortOrder); + } + if (cursor != null && cursor.moveToFirst()) { + + int pathIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA); + int mimeTypeIndex = cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE); + int sizeIndex = cursor.getColumnIndex(MediaStore.MediaColumns.SIZE); + + do { + long size = cursor.getLong(sizeIndex); + // 图片大小不得小于 10 KB + if (size < 1024 * 10) { + continue; + } + + String type = cursor.getString(mimeTypeIndex); + String path = cursor.getString(pathIndex); + if (TextUtils.isEmpty(path) || TextUtils.isEmpty(type)) { + continue; + } + + File file = new File(path); + if (!file.exists() || !file.isFile()) { + continue; + } + + File parentFile = file.getParentFile(); + if (parentFile != null) { + // 获取目录名作为专辑名称 + String albumName = parentFile.getName(); + List data = mAllAlbum.get(albumName); + if (data == null) { + data = new ArrayList<>(); + mAllAlbum.put(albumName, data); + } + data.add(path); + mAllImage.add(path); + } + + } while (cursor.moveToNext()); + + cursor.close(); + } + + postDelayed(() -> { + // 滚动回第一个位置 + mRecyclerView.scrollToPosition(0); + // 设置新的列表数据 + mAdapter.setData(mAllImage); + + if (mSelectImage.isEmpty()) { + mFloatingView.setImageResource(R.drawable.camera_ic); + } else { + mFloatingView.setImageResource(R.drawable.succeed_ic); + } + + // 执行列表动画 + mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), R.anim.fall_down_layout)); + mRecyclerView.scheduleLayoutAnimation(); + + // 设置右标提 + setRightTitle(R.string.image_select_all); + + if (mAllImage.isEmpty()) { + // 显示空布局 + showEmpty(); + } else { + // 显示加载完成 + showComplete(); + } + }, 500); + } + + /** + * 图片选择监听 + */ + public interface OnPhotoSelectListener { + + /** + * 选择回调 + * + * @param files 图片列表 + */ + void onSelected(List files); + + /** + * 取消回调 + */ + default void onCancel() {} + } + + + /** + * 鲁班压缩一下 + */ + private static void lubanStudio(BaseActivity activity,OnPhotoSelectListener listener, List data) { + + + + Flowable.just(data) + .observeOn(Schedulers.io()) + .map(new Function, List>() { + @Override + public List apply(List list) throws Throwable { + return Luban.with(activity).load(list).get(); + } + }) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(new Consumer() { + @Override + public void accept(Throwable throwable) throws Throwable { + listener.onCancel(); + } + }) + .subscribe(new Consumer>() { + @Override + public void accept(List files) throws Throwable { + + listener.onSelected(files); + } + }); + + + + + + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/ContactsActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/ContactsActivity.java new file mode 100644 index 0000000..1892a9f --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/ContactsActivity.java @@ -0,0 +1,129 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.LinearLayout; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.listener.OnItemClickListener; +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.db.DbContactsHelper; +import com.example.alcoholic.ui.adapter.ContactsAdapter; +import com.example.alcoholic.ui.dialog.MessageDialog; +import com.example.alcoholic.ui.dialog.SyncContactsDialog; +import com.example.base.BaseDialog; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description: 联系人页面 + * on 2020/11/18. + */ +public class ContactsActivity extends MyActivity { + + private LinearLayout mLayoutAddFriend; + private LinearLayout mLayoutGroup; + private LinearLayout mLayoutBlack; + private RecyclerView mRvContacts; + + private ContactsAdapter mContactsAdapter; + + @Override + protected int getLayoutId() { + return R.layout.activity_contacts; + } + + @Override + protected void initView() { + mLayoutAddFriend = findViewById(R.id.contacts_layout_addFriend); + mLayoutGroup = findViewById(R.id.contacts_layout_group); + mLayoutBlack = findViewById(R.id.contacts_layout_black); + mRvContacts = findViewById(R.id.contacts_rv_contacts); + + + setOnClickListener(mLayoutAddFriend,mLayoutBlack,mLayoutGroup); + + initList(); + } + + @Override + protected void initData() { + + mContactsAdapter.setNewData(DbContactsHelper.queryContactsAll()); + + } + + private void initList(){ + mContactsAdapter = new ContactsAdapter(); + mRvContacts.setLayoutManager(new LinearLayoutManager(this)); + mRvContacts.setAdapter(mContactsAdapter); + mContactsAdapter.setEmptyView(R.layout.layout_empty); + + mContactsAdapter.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(@NonNull BaseQuickAdapter adapter, @NonNull View view, int position) { + Intent intent = new Intent(getActivity(),UserHomeActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,mContactsAdapter.getItem(position).getContactsAccount()); + intent.putExtras(bundle); + startActivity(intent); + } + }); + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.contacts_layout_addFriend: + //添加好友 + addFriend(); + break; + case R.id.contacts_layout_group: + //群组 + startActivity(GroupManageActivity.class); + break; + case R.id.contacts_layout_black: + //黑名单 + break; + } + } + + @Override + public void onRightClick(View v) { + + new MessageDialog.Builder(this).setMessage("是否要同步联系人信息?") + .setListener(new MessageDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog) { + new SyncContactsDialog.Builder(getActivity()).show(); + } + }) + .show(); + + } + + /** + * 添加好友 + */ + private void addFriend(){ + + startActivityForResult(FriendAddActivity.class, new OnActivityCallback() { + @Override + public void onActivityResult(int resultCode, @Nullable Intent data) { + initData(); + } + }); + + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/FriendAddActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/FriendAddActivity.java new file mode 100644 index 0000000..21a4e7f --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/FriendAddActivity.java @@ -0,0 +1,117 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.helper.InputTextHelper; +import com.example.alcoholic.im.SendMsg; +import com.hyphenate.EMCallBack; +import com.hyphenate.chat.EMClient; + +/** + * Created by + * Description:添加好友页面 + * on 2020/11/18. + */ +public class FriendAddActivity extends MyActivity { + + private EditText mEtAccount; + private EditText mEtWhy; + private TextView mBtnAdd; + + @Override + protected int getLayoutId() { + return R.layout.activity_friend_add; + } + + @Override + protected void initView() { + mEtAccount = findViewById(R.id.friendAdd_et_account); + mEtWhy = findViewById(R.id.friendAdd_et_why); + mBtnAdd = findViewById(R.id.friendAdd_btn_add); + + + setOnClickListener(mBtnAdd); + + + InputTextHelper.with(this) + .setMain(mBtnAdd) + .addView(mEtAccount) + .setListener(new InputTextHelper.OnInputTextListener() { + @Override + public boolean onInputChange(InputTextHelper helper) { + return getInputAccount().length() >= 6; + } + }) + .build(); + + } + + @Override + protected void initData() { + + } + + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.friendAdd_btn_add: + addFriend(); + + break; + } + } + + + private void addFriend(){ + showDialog(); + + SendMsg.sendFriendAdd(getInputAccount(), getInputWhy(), new EMCallBack() { + @Override + public void onSuccess() { + hideDialog(); + toast("申请好友成功"); + runOnUiThread(new Runnable() { + @Override + public void run() { + + mEtAccount.setText(""); + mEtWhy.setText(""); + } + }); + } + + @Override + public void onError(int code, String error) { + hideDialog(); + toast("申请好友失败:"+code+" - "+error); + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + + + } + + /** + * 获取输入的账户信息 + */ + private String getInputAccount(){ + return mEtAccount.getText().toString().trim().toLowerCase(); + } + + /** + * 获取输入的理由 + */ + private String getInputWhy(){ + return mEtWhy.getText().toString().trim(); + } + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupAddActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupAddActivity.java new file mode 100644 index 0000000..dfad204 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupAddActivity.java @@ -0,0 +1,104 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.app.Activity; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.helper.InputTextHelper; +import com.hyphenate.EMCallBack; +import com.hyphenate.chat.EMClient; + +/** + * Created by + * Description:加入群组 + * on 2020/11/24. + */ +public class GroupAddActivity extends MyActivity { + + private EditText mEtInput; + private TextView mBtnAdd; + + @Override + protected int getLayoutId() { + return R.layout.activity_group_add; + } + + @Override + protected void initView() { + mEtInput = findViewById(R.id.groupAdd_et_groupId); + mBtnAdd = findViewById(R.id.groupAdd_btn_add); + + setOnClickListener(mBtnAdd); + + InputTextHelper.with(this) + .setMain(mBtnAdd) + .addView(mEtInput) + .setListener(new InputTextHelper.OnInputTextListener() { + @Override + public boolean onInputChange(InputTextHelper helper) { + return getEtInput().length() >= 6; + } + }) + .build(); + + } + + @Override + protected void initData() { + + + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.groupAdd_btn_add: + addGroup(); + break; + } + } + + /** + * 添加群组 + */ + private void addGroup(){ + showDialog(); + + //如果群开群是自由加入的,即group.isMembersOnly()为false,直接join + EMClient.getInstance().groupManager().asyncJoinGroup(getEtInput(), new EMCallBack() { + @Override + public void onSuccess() { + hideDialog(); + toast("申请群组成功"); + setResult(Activity.RESULT_OK); + runOnUiThread(new Runnable() { + @Override + public void run() { + + mEtInput.setText(""); + } + }); + } + + @Override + public void onError(int code, String error) { + hideDialog(); + toast("申请群组失败:"+code+" - "+error); + } + + @Override + public void onProgress(int progress, String status) { + + } + });//需异步处理 + + } + + public String getEtInput() { + return mEtInput.getText().toString(); + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupAddMemberActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupAddMemberActivity.java new file mode 100644 index 0000000..0d70a6e --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupAddMemberActivity.java @@ -0,0 +1,204 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Canvas; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.listener.OnItemClickListener; +import com.chad.library.adapter.base.listener.OnItemSwipeListener; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.ContactsBean; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.JumpDataContants; +import com.example.alcoholic.db.DbContactsHelper; +import com.example.alcoholic.ui.adapter.ContactsAdapter; +import com.example.alcoholic.utils.EmptyUtils; +import com.hyphenate.EMCallBack; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMCursorResult; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description:群组添加新成员 + * on 2020/11/20. + */ +public class GroupAddMemberActivity extends MyActivity { + + private RecyclerView mRvUser; + + + private ContactsAdapter mContactsAdapter; + + private String mGroupId; + /* 暂存数据 */ + private List mContactsBeanList = new ArrayList<>(); + + @Override + protected int getLayoutId() { + return R.layout.activity_group_add_member; + } + + @Override + protected void initView() { + mRvUser = findViewById(R.id.groupAddMember_rv_user); + + + initList(); + } + + + private void initList(){ + + + // 侧滑监听 + OnItemSwipeListener onItemSwipeListener = new OnItemSwipeListener() { + @Override + public void onItemSwipeStart(RecyclerView.ViewHolder viewHolder, int pos) { + } + + @Override + public void clearView(RecyclerView.ViewHolder viewHolder, int pos) { + } + + @Override + public void onItemSwiped(RecyclerView.ViewHolder viewHolder, int pos) { + inviteUser(pos); + Logger.d(mContactsBeanList.get(pos).getContactsAccount()); + } + + @Override + public void onItemSwipeMoving(Canvas canvas, RecyclerView.ViewHolder viewHolder, float dX, float dY, boolean isCurrentlyActive) { + canvas.drawColor(ContextCompat.getColor(GroupAddMemberActivity.this, R.color.colorAccent)); + } + }; + + + mContactsAdapter = new ContactsAdapter(); + mContactsAdapter.getDraggableModule().setSwipeEnabled(true); + mContactsAdapter.getDraggableModule().setOnItemSwipeListener(onItemSwipeListener); + mContactsAdapter.getDraggableModule().getItemTouchHelperCallback().setSwipeMoveFlags(ItemTouchHelper.START | ItemTouchHelper.END); + mRvUser.setLayoutManager(new LinearLayoutManager(this)); + mRvUser.setAdapter(mContactsAdapter); + mContactsAdapter.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(@NonNull BaseQuickAdapter adapter, @NonNull View view, int position) { + + + } + }); + + } + + @Override + protected void initData() { + if (getIntent() != null && getIntent().getExtras()!= null){ + mGroupId = getIntent().getExtras().getString(JumpDataContants.JUMP_DATA_USER_ACCOUNT,mGroupId); + } + if (EmptyUtils.isEmpty(mGroupId)){ + toast("找不到该群组"); + finish(); + } + + showAddGroupMember(); + } + + + + /** + * 显示可以加入群组的人 + */ + private void showAddGroupMember(){ + + mContactsBeanList.addAll(DbContactsHelper.queryContactsAll()); + + + //如果群成员较多,需要多次从服务器获取完成 + new Thread(new Runnable() { + @Override + public void run() { + + List memberList = new ArrayList<>(); + EMCursorResult result = null; + final int pageSize = 20; + do { + try { + result = EMClient.getInstance().groupManager().fetchGroupMembers(mGroupId, + result != null ? result.getCursor() : "", pageSize); + memberList.addAll(result.getData()); + } catch (HyphenateException e) { + e.printStackTrace(); + } + } while (!TextUtils.isEmpty(result.getCursor()) && result.getData().size() == pageSize); + + + runOnUiThread(new Runnable() { + @Override + public void run() { + int size = mContactsBeanList.size(); + for (int i = size-1; i >= 0 ; i--) { + if (memberList.contains(mContactsBeanList.get(i).getContactsAccount())){ + mContactsBeanList.remove(i); + } + + } + + + mContactsAdapter.addData(mContactsBeanList); + } + }); + } + }).start(); + + } + + + /** + * 邀请进群 + */ + private void inviteUser(int pos){ + + //私有群里,如果开放了群成员邀请,群成员邀请调用下面方法 + String[] newmembers = new String[]{mContactsBeanList.get(pos).getContactsAccount()}; + EMClient.getInstance().groupManager().asyncInviteUser(mGroupId, newmembers, null, new EMCallBack() { + @Override + public void onSuccess() { + toast("邀请成功"); + mContactsBeanList.remove(pos); + setResult(Activity.RESULT_OK); + } + + @Override + public void onError(int code, String error) { + + toast("邀请失败:"+code+"-"+error); + mContactsAdapter.addData(pos,mContactsBeanList.get(pos)); + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupCreateActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupCreateActivity.java new file mode 100644 index 0000000..eb134ed --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupCreateActivity.java @@ -0,0 +1,9 @@ +package com.example.alcoholic.ui.acitivity.user; + +/** + * Created by + * Description:创建群组 + * on 2020/11/25. + */ +class GroupCreateActivity { +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupHomeActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupHomeActivity.java new file mode 100644 index 0000000..7cd9c18 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupHomeActivity.java @@ -0,0 +1,362 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.bean.AnnouncementBean; +import com.example.alcoholic.bean.GroupDescriptionBean; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.JumpDataContants; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.im.IMConstant; +import com.example.alcoholic.ui.acitivity.ChatActivity; +import com.example.alcoholic.ui.adapter.GroupMembersSimpleAdapter; +import com.example.alcoholic.ui.dialog.InputDialog; +import com.example.alcoholic.utils.AESUtils; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.example.base.BaseDialog; +import com.example.widget.layout.SettingBar; +import com.google.gson.Gson; +import com.hyphenate.EMCallBack; +import com.hyphenate.EMValueCallBack; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMCursorResult; +import com.hyphenate.chat.EMGroup; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; +import com.tencent.mmkv.MMKV; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_CHAT_TYPE; +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description:群租首页 + * on 2020/11/19. + */ +public class GroupHomeActivity extends MyActivity { + + private ImageView mIvHead; + private TextView mTvName; + private TextView mTvId; + private TextView mTvIntro; + private TextView mTvMemberSize; + private TextView mTvAnnouncement; + private SettingBar mBarInfo; + private SettingBar mBarSync; + private SettingBar mBarAnnouncement; + private SettingBar mBarOpenChat; + private RecyclerView mRvMembers; + private LinearLayout mLayoutAddMember; + + private GroupMembersSimpleAdapter mMembersAdapter; + + private String mGroupId; + /* 是否是群主 */ + private boolean isOwner = false; + + @Override + protected int getLayoutId() { + return R.layout.activity_group_home; + } + + @Override + protected void initView() { + mIvHead = findViewById(R.id.groupHome_iv_headImg); + mTvName = findViewById(R.id.groupHome_tv_name); + mTvId = findViewById(R.id.groupHome_tv_id); + mTvIntro = findViewById(R.id.groupHome_tv_intro); + mTvMemberSize = findViewById(R.id.groupHome_layout_memberSize); + mBarInfo = findViewById(R.id.groupHome_bar_info); + mBarSync = findViewById(R.id.groupHome_bar_sync); + mBarAnnouncement = findViewById(R.id.groupHome_bar_announcement); + mBarOpenChat = findViewById(R.id.groupHome_bar_openChat); + mRvMembers = findViewById(R.id.groupHome_rv_members); + mLayoutAddMember = findViewById(R.id.groupHome_layout_addMember); + mTvAnnouncement = findViewById(R.id.groupHome_tv_announcement); + + //群成员列表 + initMembersList(); + + setOnClickListener(mBarInfo,mBarSync,mBarAnnouncement,mBarOpenChat,mLayoutAddMember); + } + + @Override + protected void initData() { + if (getIntent() != null && getIntent().getExtras()!= null){ + mGroupId = getIntent().getExtras().getString(JumpDataContants.JUMP_DATA_USER_ACCOUNT,mGroupId); + } + if (EmptyUtils.isEmpty(mGroupId)){ + toast("找不到该群组"); + finish(); + } + + loadGroupInfo(); + + + } + + private void initMembersList(){ + mMembersAdapter = new GroupMembersSimpleAdapter(); + + LinearLayoutManager layoutManager= new LinearLayoutManager(this); + layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + mRvMembers.setLayoutManager(layoutManager); + mRvMembers.setAdapter(mMembersAdapter); + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.groupHome_bar_info: + //群组信息 + enterGroupInfo(); + break; + case R.id.groupHome_bar_sync: + //同步 + loadGroupInfo(); + break; + case R.id.groupHome_bar_announcement: + //群公告 + if (isOwner){ + //群主可以修改 + updateGroupAnnouncement(); + } + break; + case R.id.groupHome_layout_addMember: + //添加成员 + enterAddGroupMember(); + break; + case R.id.groupHome_bar_openChat: + Intent intent = new Intent(getContext(), ChatActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,mGroupId); + bundle.putInt(JUMP_DATA_CHAT_TYPE, IMConstant.CHAT_TYPE_GROUPCHAT); + intent.putExtras(bundle); + startActivity(intent); + break; + } + } + + /** + * 加载群组信息 + */ + private void loadGroupInfo(){ + showDialog(); + + EMClient.getInstance().groupManager().asyncGetGroupFromServer(mGroupId, new EMValueCallBack() { + @Override + public void onSuccess(EMGroup value) { + runOnUiThread(new Runnable() { + @Override + public void run() { + showGroupInfo(value); + } + }); + } + + @Override + public void onError(int error, String errorMsg) { + + } + }); + } + + /** + * 展示群组信息 + */ + private void showGroupInfo(EMGroup emGroup){ + //更新群组数据 + DbUserHelper.insertGroup2Bean(emGroup); + + mTvName.setText(emGroup.getGroupName()); + mTvId.setText(String.format("id:%s",mGroupId)); + + //aes解密部分 + try { + String introGson = AESUtils.decrypt(MMKVStytemUtils.getInstance().getAesKey(),emGroup.getDescription().replaceAll(" ","+")); + GroupDescriptionBean groupDescriptionBean = new Gson().fromJson(introGson,GroupDescriptionBean.class); + + GlideApp.with(this) + .load(groupDescriptionBean.getHeadImg()) + .circleCrop() + .into(mIvHead); + mTvIntro.setText(groupDescriptionBean.getIntro()); + + + //群公告 + if (EmptyUtils.isNotEmpty(emGroup.getAnnouncement())){ + String announcementJson = AESUtils.decrypt(MMKVStytemUtils.getInstance().getAesKey(),emGroup.getAnnouncement().replaceAll(" ","+")); + AnnouncementBean announcementBean = new Gson().fromJson(announcementJson,AnnouncementBean.class); + mTvAnnouncement.setText(announcementBean.getAnnouncement()); + }else { + mTvAnnouncement.setText("暂无"); + } + } catch (Exception e) { + e.printStackTrace(); + } + + //群成员 + mTvMemberSize.setText("群成员("+emGroup.getMemberCount()+"/"+emGroup.getMaxUserCount()+")"); + new Thread(new Runnable() { + @Override + public void run() { + List memberList = new ArrayList<>(); + EMCursorResult result = null; + final int pageSize = 20; + + //先插入群主 + memberList.add(emGroup.getOwner()); + + //保存是否是群主 + isOwner = emGroup.getOwner().equals(MMKVUserUtils.getInstance().getUserAccount()); + + do { + try { + result = EMClient.getInstance().groupManager().fetchGroupMembers(mGroupId, + result != null ? result.getCursor() : "", pageSize); + memberList.addAll(result.getData()); + } catch (HyphenateException e) { + e.printStackTrace(); + } + } while (!TextUtils.isEmpty(result.getCursor()) && result.getData().size() == pageSize); + + runOnUiThread(new Runnable() { + @Override + public void run() { + mMembersAdapter.setNewData(memberList); + + //控制项 + if (!isOwner){ + //非群主则不可以修改信息 + mBarAnnouncement.setRightIcon(null); + } + + + if (isShowDialog()){ + hideDialog(); + } + } + }); + + } + }).start(); + + + + + + + + + } + + + /** + * 进入群组信息 + */ + private void enterGroupInfo(){ + + Intent intent = new Intent(this,GroupInfoActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JumpDataContants.JUMP_DATA_USER_ACCOUNT,mGroupId); + bundle.putBoolean(JumpDataContants.JUMP_DATA_IS_OWNER,isOwner); + intent.putExtras(bundle); + startActivityForResult(intent, new OnActivityCallback() { + @Override + public void onActivityResult(int resultCode, @Nullable Intent data) { + if (resultCode == Activity.RESULT_OK) loadGroupInfo(); + } + }); + } + + /** + * 进入添加成员 + */ + private void enterAddGroupMember(){ + Intent intent = new Intent(this,GroupAddMemberActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JumpDataContants.JUMP_DATA_USER_ACCOUNT,mGroupId); + bundle.putBoolean(JumpDataContants.JUMP_DATA_IS_OWNER,isOwner); + intent.putExtras(bundle); + startActivityForResult(intent, new OnActivityCallback() { + @Override + public void onActivityResult(int resultCode, @Nullable Intent data) { + if (resultCode == Activity.RESULT_OK) loadGroupInfo(); + } + }); + } + + /** + * 修改群公告 + */ + private void updateGroupAnnouncement(){ + new InputDialog.Builder(this) + .setTitle("发布群公告") + .setHint("请输入将要发布的群公告") + .setAutoDismiss(false) + .setListener(new InputDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog, String content) { + if (content.trim().length() <=0 || content.trim().length() > 100){ + toast("群公告字数限制 0-100 字"); + return; + } + + AnnouncementBean announcementBean = new AnnouncementBean(); + announcementBean.setAnnouncement(content); + + try { + String announcement = AESUtils.encrypt(MMKVStytemUtils.getInstance().getAesKey(),new Gson().toJson(announcementBean)); + + EMClient.getInstance().groupManager().asyncUpdateGroupAnnouncement(mGroupId, announcement, new EMCallBack() { + @Override + public void onSuccess() { + loadGroupInfo(); + dialog.dismiss(); + } + + @Override + public void onError(int code, String error) { + dialog.dismiss(); + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + + + + } + @Override + public void onCancel(BaseDialog dialog) { + dialog.dismiss(); + } + }) + .show(); + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupInfoActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupInfoActivity.java new file mode 100644 index 0000000..a050445 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupInfoActivity.java @@ -0,0 +1,355 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.app.Activity; +import android.view.View; +import android.widget.ImageView; + +import com.example.alcoholic.R; +import com.example.alcoholic.bean.GroupDescriptionBean; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.JumpDataContants; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.http.request.ImgUpLoadApi; +import com.example.alcoholic.http.response.ImgUploadBean; +import com.example.alcoholic.http.server.ImgBBServer; +import com.example.alcoholic.ui.acitivity.image.ImageSelectActivity; +import com.example.alcoholic.ui.dialog.InputDialog; +import com.example.alcoholic.utils.AESUtils; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.example.alcoholic.utils.TimeUtils; +import com.example.base.BaseActivity; +import com.example.base.BaseDialog; +import com.example.widget.layout.SettingBar; +import com.google.gson.Gson; +import com.hjq.http.EasyHttp; +import com.hjq.http.listener.OnHttpListener; +import com.hyphenate.EMCallBack; +import com.hyphenate.chat.EMClient; +import com.orhanobut.logger.Logger; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +/** + * Created by + * Description:群组信息页面 + * on 2020/11/20. + */ +public class GroupInfoActivity extends MyActivity { + + + private ImageView mIvHead; + private SettingBar mBarHead; + private SettingBar mBarId; + private SettingBar mBarName; + private SettingBar mBarIntro; + + private String mGroupId; + /* 是否是群主 */ + private boolean isOwner = false; + /* 群组信息 - 本地 */ + private UserInfoBean mGroupInfo; + + @Override + protected int getLayoutId() { + return R.layout.activity_group_info; + } + + @Override + protected void initView() { + mIvHead = findViewById(R.id.groupInfo_iv_head); + mBarHead = findViewById(R.id.groupInfo_bar_head); + mBarId = findViewById(R.id.groupInfo_bar_id); + mBarName = findViewById(R.id.groupInfo_bar_name); + mBarIntro = findViewById(R.id.groupInfo_bar_intro); + + setOnClickListener(mBarHead,mBarName,mBarIntro); + + + } + + @Override + protected void initData() { + if (getIntent() != null && getIntent().getExtras()!= null){ + mGroupId = getIntent().getExtras().getString(JumpDataContants.JUMP_DATA_USER_ACCOUNT,""); + isOwner = getIntent().getExtras().getBoolean(JumpDataContants.JUMP_DATA_IS_OWNER,false); + } + if (EmptyUtils.isEmpty(mGroupId)){ + toast("找不到该群组"); + finish(); + } + + + if (!isOwner){ + //非群主不能修改 + mBarName.setRightIcon(null); + mBarIntro.setRightIcon(null); + } + + initGroupInfo(); + } + + private void initGroupInfo(){ + + mGroupInfo = DbUserHelper.queryUser2Account(mGroupId); + + + + + if (mGroupInfo != null && EmptyUtils.isNotEmpty(mGroupInfo.getUserHead())){ + GlideApp.with(this) + .load(mGroupInfo.getUserHead()) + .circleCrop() + .into(mIvHead); + mBarIntro.setRightText(mGroupInfo.getUserIntro()); + }else { + GlideApp.with(this) + .load(R.drawable.icon) + .circleCrop() + .into(mIvHead); + mBarIntro.setRightText(""); + } + + mBarId.setRightText(mGroupId); + mBarName.setRightText(mGroupInfo.getUserName()); + + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.groupInfo_bar_head: + //头像 + if (isOwner){ + //群主才能修改 + upGroupHead(); + } + break; + case R.id.groupInfo_bar_name: + //群名 + if (isOwner){ + //群主才能修改 + upGroupName(); + } + break; + case R.id.groupInfo_bar_intro: + //简介 + if (isOwner){ + //群主才能修改 + upGroupIntro(); + } + break; + } + } + + /** + * 修改群头像 + */ + public void upGroupHead(){ + ImageSelectActivity.start((BaseActivity) getActivity(), new ImageSelectActivity.OnPhotoSelectListener() { + + @Override + public void onSelected(List files) { + showDialog(); + EasyHttp.post((BaseActivity) getActivity()) + .server(new ImgBBServer()) + .api(new ImgUpLoadApi() + .setImage(files.get(0)) + .setName(MMKVUserUtils.getInstance().getUserAccount()+"_header_"+ TimeUtils.getNowMills())) + .request(new OnHttpListener() { + @Override + public void onSucceed(ImgUploadBean result) { + + + GroupDescriptionBean groupDescriptionBean = new GroupDescriptionBean(); + groupDescriptionBean.setInfoTag(MMKVUserUtils.getInstance().createTag()); + groupDescriptionBean.setHeadImg(result.getData().getUrl()); + groupDescriptionBean.setIntro(mGroupInfo.getUserIntro()); + + + try { + String description = AESUtils.encrypt(MMKVStytemUtils.getInstance().getAesKey(),new Gson().toJson(groupDescriptionBean)); + + //修改群描述 + EMClient.getInstance().groupManager().asyncChangeGroupDescription(mGroupId, description, new EMCallBack() { + @Override + public void onSuccess() { + + mGroupInfo.setUserHead(result.getData().getUrl()); + DbUserHelper.insertUser(mGroupInfo); + toast("修改群组头像成功"); + setResult(Activity.RESULT_OK); + runOnUiThread(new Runnable() { + @Override + public void run() { + hideDialog(); + initGroupInfo(); + } + }); + } + + @Override + public void onError(int code, String error) { + toast("修改失败:"+code+" - "+error); + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + + } catch (Exception e) { + hideDialog(); + e.printStackTrace(); + } + + } + + @Override + public void onFail(Exception e) { + hideDialog(); + toast("图片上传失败,请重试"); + } + }); + + } + + @Override + public void onCancel() { + toast("取消了"); + } + }); + } + + /** + * 修改群名 + */ + public void upGroupName(){ + new InputDialog.Builder(this) + .setTitle("修改群组名称") + .setHint("请输入您将要设置的群组名称吧") + .setAutoDismiss(false) + .setListener(new InputDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog, String content) { + if (content.trim().length()<=0 || content.trim().length() > 16){ + toast("群组名称长度限制 0-16 位"); + return; + } + //修改群名称 + EMClient.getInstance().groupManager().asyncChangeGroupName(mGroupId, content.trim(), new EMCallBack() { + @Override + public void onSuccess() { + mGroupInfo.setUserName(content.trim()); + DbUserHelper.insertUser(mGroupInfo); + toast("修改成功"); + setResult(Activity.RESULT_OK); + runOnUiThread(new Runnable() { + @Override + public void run() { + initGroupInfo(); + } + }); + dialog.dismiss(); + } + + @Override + public void onError(int code, String error) { + toast("修改失败:"+code+" - "+error); + } + + @Override + public void onProgress(int progress, String status) { + + } + });//需异步处理 + + } + @Override + public void onCancel(BaseDialog dialog) { + dialog.dismiss(); + } + }) + .show(); + } + + /** + * 修改群组简介 + */ + private void upGroupIntro(){ + new InputDialog.Builder(this) + .setTitle("修改群组简介") + .setHint("请输入您将要设置的群组简介吧") + .setAutoDismiss(false) + .setListener(new InputDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog, String content) { + if (content.trim().length()<=0 || content.trim().length() > 16){ + toast("群组简介长度限制 0-50 位"); + return; + } + GroupDescriptionBean groupDescriptionBean = new GroupDescriptionBean(); + groupDescriptionBean.setInfoTag(MMKVUserUtils.getInstance().createTag()); + groupDescriptionBean.setHeadImg(mGroupInfo.getUserHead()); + groupDescriptionBean.setIntro(content.trim()); + + try { + String description = AESUtils.encrypt(MMKVStytemUtils.getInstance().getAesKey(),new Gson().toJson(groupDescriptionBean)); + + //修改群描述 + EMClient.getInstance().groupManager().asyncChangeGroupDescription(mGroupId, description, new EMCallBack() { + @Override + public void onSuccess() { + mGroupInfo.setUserIntro(content.trim()); + DbUserHelper.insertUser(mGroupInfo); + toast("修改成功"); + setResult(Activity.RESULT_OK); + runOnUiThread(new Runnable() { + @Override + public void run() { + initGroupInfo(); + } + }); + dialog.dismiss(); + } + + @Override + public void onError(int code, String error) { + toast("修改失败:"+code+" - "+error); + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + + } catch (Exception e) { + e.printStackTrace(); + } + + + + } + @Override + public void onCancel(BaseDialog dialog) { + dialog.dismiss(); + } + }) + .show(); + } + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupManageActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupManageActivity.java new file mode 100644 index 0000000..2d835d4 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/GroupManageActivity.java @@ -0,0 +1,208 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.listener.OnItemClickListener; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.GroupDescriptionBean; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.RandomConstant; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.im.IMConstant; +import com.example.alcoholic.utils.AESUtils; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.google.gson.Gson; +import com.hyphenate.EMCallBack; +import com.hyphenate.EMValueCallBack; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMGroup; +import com.hyphenate.chat.EMMessage; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; + +import org.jetbrains.annotations.NotNull; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description:群组管理页面 + * on 2020/11/19. + */ +public class GroupManageActivity extends MyActivity { + + private LinearLayout mLayoutCreate; + private LinearLayout mLayoutAdd; + private RecyclerView mRvGroup; + + private BaseQuickAdapter mAdapter; + + @Override + protected int getLayoutId() { + return R.layout.activity_group_manage; + + } + + @Override + protected void initView() { + mLayoutCreate = findViewById(R.id.groupManage_layout_createGroup); + mLayoutAdd = findViewById(R.id.groupManage_layout_addGroup); + mRvGroup = findViewById(R.id.groupManage_rv_group); + + setOnClickListener(mLayoutCreate,mLayoutAdd); + + initList(); + } + + @Override + protected void initData() { + + //从服务器获取自己加入的和创建的群组列表,此api获取的群组sdk会自动保存到内存和db。 + EMClient.getInstance().groupManager().asyncGetJoinedGroupsFromServer(new EMValueCallBack>() { + @Override + public void onSuccess(List value) { + runOnUiThread(new Runnable() { + @Override + public void run() { + + //有数据 + mAdapter.setNewData(value); + + for (EMGroup emGroup:value) { + DbUserHelper.insertGroup2Bean(emGroup); + } + + + } + }); + } + + @Override + public void onError(int error, String errorMsg) { + Logger.e(error+"-"+errorMsg); + } + }); + + } + + + private void initList(){ + mAdapter = new BaseQuickAdapter(R.layout.item_contacts) { + @Override + protected void convert(@NotNull BaseViewHolder holder, EMGroup emGroup) { + ImageView ivHead = holder.getView(R.id.item_contacts_iv_head); + //头像 + try { + String description = description = AESUtils.decrypt(MMKVStytemUtils.getInstance().getAesKey(),emGroup.getDescription().replaceAll(" ","+")); + GroupDescriptionBean groupDescription = new Gson().fromJson(description, GroupDescriptionBean.class); + + GlideApp.with(getActivity()) + .load(groupDescription.getHeadImg()) + .circleCrop() + .into(ivHead); + + } catch (Exception e) { + e.printStackTrace(); + } + + + holder.setText(R.id.item_contacts_tv_name,emGroup.getGroupName()+"("+emGroup.getMemberCount()+" 人)"); + + } + }; + mRvGroup.setLayoutManager(new LinearLayoutManager(this)); + mRvGroup.setAdapter(mAdapter); + mAdapter.setEmptyView(R.layout.layout_empty); + + mAdapter.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(@NonNull BaseQuickAdapter adapter, @NonNull View view, int position) { + Intent intent = new Intent(GroupManageActivity.this, GroupHomeActivity.class); + + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,mAdapter.getItem(position).getGroupId()); + intent.putExtras(bundle); + startActivity(intent); + } + }); + + } + + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.groupManage_layout_createGroup: + //创建群组 + GroupDescriptionBean groupDescriptionBean = new GroupDescriptionBean(); + groupDescriptionBean.setHeadImg(RandomConstant.randomHeadImg()); + groupDescriptionBean.setInfoTag(MMKVUserUtils.getInstance().createTag()); + groupDescriptionBean.setIntro("这是一条新的简介"); + + try { + String aa = AESUtils.encrypt(MMKVStytemUtils.getInstance().getAesKey(),new Gson().toJson(groupDescriptionBean)); + + Logger.d(aa); + EMClient.getInstance().groupManager().asyncChangeGroupDescription("132497543331841", aa, new EMCallBack() { + @Override + public void onSuccess() { + Logger.d("修改成功"); + } + @Override + public void onError(int code, String error) { + + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + + break; + case R.id.groupManage_layout_addGroup: + + startActivityForResult(GroupAddActivity.class, new OnActivityCallback() { + @Override + public void onActivityResult(int resultCode, @Nullable Intent data) { + //如果添加,则重新刷新列表 + if (resultCode == Activity.RESULT_OK) initData(); + } + }); + break; + } + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/HelpActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/HelpActivity.java new file mode 100644 index 0000000..25056a2 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/HelpActivity.java @@ -0,0 +1,26 @@ +package com.example.alcoholic.ui.acitivity.user; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; + +/** + * Created by + * Description:使用帮助 + * on 2020/11/19. + */ +public class HelpActivity extends MyActivity { + @Override + protected int getLayoutId() { + return R.layout.activity_help; + } + + @Override + protected void initView() { + + } + + @Override + protected void initData() { + + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/LoginActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/LoginActivity.java new file mode 100644 index 0000000..558df64 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/LoginActivity.java @@ -0,0 +1,131 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.util.Log; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.db.DbUserManager; +import com.example.alcoholic.helper.InputTextHelper; +import com.example.alcoholic.ui.acitivity.HomeActivity; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.hyphenate.EMCallBack; +import com.hyphenate.chat.EMClient; + +/** + * Created by + * Description:登录页面 + * on 2020/11/13. + */ +public class LoginActivity extends MyActivity { + + private EditText mEtAccount; + private EditText mEtPsd; + private TextView mBtnRegister; + private TextView mBtnLogin; + + @Override + protected int getLayoutId() { + return R.layout.activity_user_login; + } + + @Override + protected void initView() { + mEtAccount = findViewById(R.id.userLogin_et_account); + mEtPsd = findViewById(R.id.userLogin_et_psd); + mBtnRegister = findViewById(R.id.userLogin_btn_register); + mBtnLogin = findViewById(R.id.userLogin_btn_login); + + setOnClickListener(mBtnRegister, mBtnLogin); + + + InputTextHelper.with(this) + .setMain(mBtnLogin) + .addView(mEtAccount) + .addView(mEtPsd) + .setListener(new InputTextHelper.OnInputTextListener() { + @Override + public boolean onInputChange(InputTextHelper helper) { + return getEtAccount().length() >= 6 + && getEtPsd().length() >= 6; + } + }) + .build(); + } + + @Override + protected void initData() { + + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.userLogin_btn_register: + startActivity(RegisterActivity.class); + break; + case R.id.userLogin_btn_login: + //登录 + login(); + break; + } + } + + public String getEtAccount() { + return mEtAccount.getText().toString().toLowerCase(); + } + + public String getEtPsd() { + return mEtPsd.getText().toString().toLowerCase(); + } + + /** + * 登录方法 + */ + public void login(){ + EMClient.getInstance().login(getEtAccount(),getEtPsd(),new EMCallBack() {//回调 + @Override + public void onSuccess() { + EMClient.getInstance().groupManager().loadAllGroups(); + EMClient.getInstance().chatManager().loadAllConversations(); + + Log.d("main", "登录聊天服务器成功!"); + + toast("登陆成功"); + + //保存用户信息 + UserInfoBean userInfoBean = DbUserHelper.queryUserIsLogin(getEtAccount()); + + MMKVUserUtils.getInstance().saveUserAccount(userInfoBean.getUserAccount()); + MMKVUserUtils.getInstance().saveUserPsd(getEtPsd()); + MMKVUserUtils.getInstance().saveUserName(userInfoBean.getUserName()); + MMKVUserUtils.getInstance().saveUserHead(userInfoBean.getUserHead()); + MMKVUserUtils.getInstance().saveUserIntro(userInfoBean.getUserIntro()); + + DbUserHelper.reUserTag(userInfoBean.getUserAccount()); + + startActivity(HomeActivity.class); + finish(); + } + + @Override + public void onProgress(int progress, String status) { + + } + + @Override + public void onError(int code, String message) { + toast("登录失败:"+message); + Log.d("main", "登录聊天服务器失败!"); + } + }); + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/RegisterActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/RegisterActivity.java new file mode 100644 index 0000000..b3b3ae6 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/RegisterActivity.java @@ -0,0 +1,121 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.helper.InputTextHelper; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.hyphenate.chat.EMClient; +import com.hyphenate.exceptions.HyphenateException; + +/** + * Created by + * Description: 注册页面 + * 注册条件:账号 6-16位,密码6-16位,英文数字 + * on 2020/11/16. + */ +public class RegisterActivity extends MyActivity { + + private EditText mEtAccount; + private EditText mEtPsd; + private EditText mEtPsd2; + private TextView mBtnRegister; + + @Override + protected int getLayoutId() { + return R.layout.activity_user_register; + } + + @Override + protected void initView() { + mEtAccount = findViewById(R.id.userRegister_et_account); + mEtPsd = findViewById(R.id.userRegister_et_psd); + mEtPsd2 = findViewById(R.id.userRegister_et_psd2); + mBtnRegister = findViewById(R.id.userRegister_btn_register); + + setOnClickListener(mBtnRegister); + + InputTextHelper.with(this) + .setMain(mBtnRegister) + .addView(mEtAccount) + .addView(mEtPsd) + .addView(mEtPsd2) + .setListener(new InputTextHelper.OnInputTextListener() { + @Override + public boolean onInputChange(InputTextHelper helper) { + return getEtAccount().length() >= 6 + && getEtPsd().length() >= 6 + && getEtPsd2().length() >= 6; + } + }) + .build(); + + + } + + @Override + protected void initData() { + + } + + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.userRegister_btn_register: + //点击了注册 + if (!getEtPsd().equals(getEtPsd2())){ + toast("两次输入密码不同!"); + break; + } + register(); + break; + } + } + + + public String getEtAccount() { + return mEtAccount.getText().toString().toLowerCase(); + } + + public String getEtPsd() { + return mEtPsd.getText().toString().toLowerCase(); + } + + public String getEtPsd2() { + return mEtPsd2.getText().toString().toLowerCase(); + } + + + /** + * 注册 + */ + private void register(){ + + new Thread(){ + @Override + public void run() { + super.run(); + + try { + EMClient.getInstance().createAccount(getEtAccount(), getEtPsd()); + + toast("注册成功"); + finish(); + } catch (HyphenateException e) { + e.printStackTrace(); + toast("注册失败"); + } + + } + }.start(); + + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/SettingActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/SettingActivity.java new file mode 100644 index 0000000..97d1617 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/SettingActivity.java @@ -0,0 +1,161 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.view.View; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.bean.UpVersionBean; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.helper.ActivityStackManager; +import com.example.alcoholic.helper.CacheDataManager; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.ui.dialog.InputDialog; +import com.example.alcoholic.ui.dialog.MessageDialog; +import com.example.alcoholic.ui.dialog.SyncContactsDialog; +import com.example.alcoholic.utils.MMKVStytemUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.example.base.BaseDialog; +import com.example.widget.layout.SettingBar; +import com.google.gson.Gson; +import com.hyphenate.chat.EMClient; +import com.orhanobut.logger.Logger; +import com.tencent.mmkv.MMKV; + +/** + * Created by + * Description:设置页面 + * on 2020/11/17. + */ +public class SettingActivity extends MyActivity { + + private TextView mBtnExitLogin; + + private SettingBar mBarAccount; + private SettingBar mBarContactsSync; + private SettingBar mBarSetKey; + private SettingBar mBarHelp; + private SettingBar mBarCheckUp; + + + @Override + protected int getLayoutId() { + return R.layout.activity_setting; + } + + @Override + protected void initView() { + mBarAccount = findViewById(R.id.setting_bar_account); + mBarContactsSync = findViewById(R.id.setting_bar_contactsSync); + mBarSetKey = findViewById(R.id.setting_bar_setKey); + mBarHelp = findViewById(R.id.setting_bar_help); + mBarCheckUp = findViewById(R.id.setting_bar_checkUp); + mBtnExitLogin = findViewById(R.id.setting_btn_exitLogin); + + + mBarAccount.setEnabled(false); + setOnClickListener(mBarContactsSync,mBarSetKey,mBarHelp,mBarCheckUp,mBtnExitLogin); + + + } + + @Override + protected void initData() { + initInfo(); + + } + + + private void initInfo(){ + mBarAccount.setRightText(MMKVUserUtils.getInstance().getUserAccount()); + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.setting_bar_contactsSync: + new MessageDialog.Builder(this).setMessage("是否要同步联系人信息?") + .setListener(new MessageDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog) { + new SyncContactsDialog.Builder(getActivity()).show(); + } + }) + .show(); + + break; + case R.id.setting_bar_setKey: + //设置密钥 + setBarSetKey(); + break; + case R.id.setting_bar_help: + //帮助 + startActivity(HelpActivity.class); + break; + case R.id.setting_bar_checkUp: + //检查更新 + checkUp(); + break; + case R.id.setting_btn_exitLogin: + //退出登录 + exitLogin(); + break; + } + } + + /** + * 设置密钥 + */ + private void setBarSetKey(){ + new InputDialog.Builder(this) + .setTitle("设置密钥") + .setHint("请输入解密密钥") + .setListener(new InputDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog, String content) { + int size = 4; + if (content.length() == size){ + MMKVStytemUtils.getInstance().saveAesKey(content+content+content+content); + toast("修改成功"); + }else { + toast("修改失败,密钥需要是 "+size+" 位"); + } + + } + }) + .show(); + + } + + /** + * 检查更新 + */ + private void checkUp(){ + UpVersionBean upVersionBean = new UpVersionBean(); + + upVersionBean.setvName("1.0"); + upVersionBean.setvCode(1); + upVersionBean.setMinMustUpCode(1); + upVersionBean.setvUpContent("更心新的版本"); + upVersionBean.setvAPKDownUrl("www.www"); + + Logger.d(new Gson().toJson(upVersionBean)); + + } + + /** + * 退出登录 + */ + private void exitLogin(){ + MMKVUserUtils.getInstance().clearAll(); + + EMClient.getInstance().logout(true); + + + startActivity(LoginActivity.class); + // 进行内存优化,销毁除登录页之外的所有界面 + ActivityStackManager.getInstance().finishAllActivities(LoginActivity.class); + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/UserHomeActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/UserHomeActivity.java new file mode 100644 index 0000000..d6b4c5f --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/UserHomeActivity.java @@ -0,0 +1,335 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.bean.ContactListenerBean; +import com.example.alcoholic.bean.ContactsBean; +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.constant.JumpDataContants; +import com.example.alcoholic.db.DbContactsHelper; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.im.IMConstant; +import com.example.alcoholic.im.SendMsg; +import com.example.alcoholic.ui.acitivity.ChatActivity; +import com.example.alcoholic.ui.dialog.InputDialog; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.base.BaseDialog; +import com.example.widget.layout.SettingBar; +import com.google.gson.Gson; +import com.hyphenate.EMCallBack; +import com.orhanobut.logger.Logger; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import okhttp3.Call; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_CHAT_TYPE; +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description:用户主页 + * on 2020/11/18. + */ +public class UserHomeActivity extends MyActivity { + + + private ImageView mIvHeadImg; + private TextView mTvName; + private TextView mTvCodeName; + private TextView mTvAccount; + private TextView mTvIntro; + + private SettingBar mBarSetCodeName; + private SettingBar mBarOpenChat; + private SettingBar mBarAddFriend; + private SettingBar mBarDeleteFriend; + private SettingBar mBarAddBlack; + private SettingBar mBarDeleteBlack; + + private String mUserAccount; + + @Override + protected int getLayoutId() { + return R.layout.activity_user_home; + } + + @Override + protected void initView() { + mIvHeadImg = findViewById(R.id.userHome_iv_headImg); + mTvName = findViewById(R.id.userHome_tv_name); + mTvCodeName = findViewById(R.id.userHome_tv_codename); + mTvAccount = findViewById(R.id.userHome_tv_account); + mTvIntro = findViewById(R.id.userHome_tv_intro); + mBarSetCodeName = findViewById(R.id.userHome_bar_setCodeName); + mBarOpenChat = findViewById(R.id.userHome_bar_openChat); + mBarAddFriend = findViewById(R.id.userHome_bar_addFriend); + mBarDeleteFriend = findViewById(R.id.userHome_bar_deleteFriend); + mBarAddBlack = findViewById(R.id.userHome_bar_addBlack); + mBarDeleteBlack = findViewById(R.id.userHome_bar_deleteBlack); + + setOnClickListener(mBarSetCodeName,mBarOpenChat,mBarAddFriend + ,mBarDeleteFriend,mBarAddBlack,mBarDeleteBlack); + + } + + @Override + protected void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + } + + @Override + protected void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + } + + /** + * 获取到新消息 + */ + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFriendChange(ContactListenerBean contactListenerBean) { + if (contactListenerBean.getUsername().equals(mUserAccount)){ + initUserInfo(); + } + }; + + + @Override + protected void initData() { + if (getIntent() != null && getIntent().getExtras() != null){ + mUserAccount = getIntent().getExtras().getString(JumpDataContants.JUMP_DATA_USER_ACCOUNT,""); + } + if (EmptyUtils.isEmpty(mUserAccount) ){ + toast("找不到该用户"); + finish(); + } + + initUserInfo(); + } + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.userHome_bar_setCodeName: + //设置备注 + setCodeName(); + break; + case R.id.userHome_bar_openChat: + //发消息 + Intent intent = new Intent(getContext(), ChatActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,mUserAccount); + bundle.putInt(JUMP_DATA_CHAT_TYPE, IMConstant.CHAT_TYPE_CHAT); + intent.putExtras(bundle); + startActivity(intent); + break; + case R.id.userHome_bar_addFriend: + //加好友 + addFriend(); + break; + case R.id.userHome_bar_deleteFriend: + //删除好友 + deleteFriend(); + break; + case R.id.userHome_bar_addBlack: + //加入黑名单 + + break; + case R.id.userHome_bar_deleteBlack: + //移出黑名单 + + break; + + } + } + + /** + * 展示用户信息 + */ + private void initUserInfo(){ + + UserInfoBean userInfoBean = DbUserHelper.queryUser2Account(mUserAccount); + + ContactsBean contactsBean = DbContactsHelper.queryContacts2Account(mUserAccount); + + if (contactsBean != null) { + //有通讯录关系 + if (EmptyUtils.isNotEmpty(contactsBean.getCodeName())) { + mTvCodeName.setText(String.format("备注:%s", contactsBean.getCodeName())); + } + } + //根据状态显示操作按钮 + barStatusIsShow(contactsBean); + + + mTvAccount.setText(String.format("账号:%s",mUserAccount)); + + if (userInfoBean == null){ + GlideApp.with(this) + .load(R.drawable.icon) + .circleCrop() + .into(mIvHeadImg); + + mTvName.setText(mUserAccount); + + return; + } + + GlideApp.with(this) + .load(userInfoBean.getUserHead()) + .circleCrop() + .into(mIvHeadImg); + + mTvName.setText(userInfoBean.getUserName()); + mTvIntro.setText(userInfoBean.getUserIntro()); + + } + + + /** + * 重新根据状态展示操作按钮 + * @param contactsBean + */ + private void barStatusIsShow(ContactsBean contactsBean){ + if (contactsBean != null) { + + if (contactsBean.getIsFriend()){ + //好友关系 + if (contactsBean.getIsBlack()){ + //拉黑 + mBarOpenChat.setVisibility(View.GONE); + mBarAddFriend.setVisibility(View.GONE); + mBarDeleteFriend.setVisibility(View.GONE); + mBarAddBlack.setVisibility(View.GONE); + mBarDeleteBlack.setVisibility(View.VISIBLE); + }else { + //正常好友 + mBarOpenChat.setVisibility(View.VISIBLE); + mBarAddFriend.setVisibility(View.GONE); + mBarDeleteFriend.setVisibility(View.VISIBLE); + mBarAddBlack.setVisibility(View.VISIBLE); + mBarDeleteBlack.setVisibility(View.GONE); + } + }else { + mBarOpenChat.setVisibility(View.GONE); + mBarAddFriend.setVisibility(View.VISIBLE); + mBarDeleteFriend.setVisibility(View.GONE); + mBarAddBlack.setVisibility(View.GONE); + mBarDeleteBlack.setVisibility(View.GONE); + } + + }else { + //没有通讯录关系 + mBarOpenChat.setVisibility(View.GONE); + mBarAddFriend.setVisibility(View.VISIBLE); + mBarDeleteFriend.setVisibility(View.GONE); + mBarAddBlack.setVisibility(View.GONE); + mBarDeleteBlack.setVisibility(View.GONE); + + } + + + + } + + + /** + * 设置备注 + */ + private void setCodeName(){ + + new InputDialog.Builder(this) + .setTitle("设置备注") + .setHint("请输入要给你好友设置的备注") + .setContent(DbContactsHelper.queryCodeName2Account(mUserAccount)) + .setListener(new InputDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog, String content) { + if (content.trim().length() == 0 || content.trim().length() >16){ + toast("备注长度限制为 0 - 16 位。"); + return; + } + + DbContactsHelper.updateUserCodeName(mUserAccount,content.trim()); + + initUserInfo(); + } + }) + .show(); + + } + + /** + * 加好友 + */ + private void addFriend(){ + SendMsg.sendFriendAdd(mUserAccount, "", new EMCallBack() { + @Override + public void onSuccess() { + runOnUiThread(new Runnable() { + @Override + public void run() { + //刷新页面 + initUserInfo(); + toast("好友请求已发送,如果对方在线,则可自动成为好友"); + } + }); + } + + @Override + public void onError(int code, String error) { + toast("好友请求发送失败:"+code+"-"+error); + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + } + + /** + * 删除好友 + */ + private void deleteFriend(){ + SendMsg.deleteFriend(mUserAccount, new EMCallBack() { + @Override + public void onSuccess() { + + runOnUiThread(new Runnable() { + @Override + public void run() { + //修改数据库中的好友关系 + DbContactsHelper.upDateAccount2Friend(mUserAccount,false); + toast("好友已删除"); + initUserInfo(); + } + }); + } + + @Override + public void onError(int code, String error) { + + } + + @Override + public void onProgress(int progress, String status) { + + } + }); + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/acitivity/user/UserInfoSettingActivity.java b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/UserInfoSettingActivity.java new file mode 100644 index 0000000..6911ef3 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/acitivity/user/UserInfoSettingActivity.java @@ -0,0 +1,193 @@ +package com.example.alcoholic.ui.acitivity.user; + +import android.view.View; +import android.widget.ImageView; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyActivity; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.http.request.ImgUpLoadApi; +import com.example.alcoholic.http.response.ImgUploadBean; +import com.example.alcoholic.http.server.ImgBBServer; +import com.example.alcoholic.ui.acitivity.image.ImageSelectActivity; +import com.example.alcoholic.ui.dialog.InputDialog; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.example.alcoholic.utils.TimeUtils; +import com.example.base.BaseActivity; +import com.example.base.BaseDialog; +import com.example.widget.layout.SettingBar; +import com.hjq.http.EasyHttp; +import com.hjq.http.listener.OnHttpListener; +import com.orhanobut.logger.Logger; + +import java.io.File; +import java.util.List; + +import androidx.lifecycle.LifecycleOwner; + +/** + * Created by + * Description: 用户信息设置页面 + * on 2020/11/19. + */ +public class UserInfoSettingActivity extends MyActivity { + + private ImageView mIvHead; + private SettingBar mBarHead; + private SettingBar mBarAccount; + private SettingBar mBarName; + private SettingBar mBarIntro; + + @Override + protected int getLayoutId() { + return R.layout.activity_user_info_setting; + } + + @Override + protected void initView() { + mIvHead = findViewById(R.id.userInfoSetting_iv_head); + mBarHead = findViewById(R.id.userInfoSetting_bar_head); + mBarAccount = findViewById(R.id.userInfoSetting_bar_account); + mBarName = findViewById(R.id.userInfoSetting_bar_name); + mBarIntro = findViewById(R.id.userInfoSetting_bar_intro); + + + setOnClickListener(mBarHead,mBarName,mBarIntro); + } + + @Override + protected void initData() { + initUserInfo(); + } + + private void initUserInfo(){ + GlideApp.with(this) + .load(MMKVUserUtils.getInstance().getUserHead()) + .circleCrop() + .into(mIvHead); + + mBarAccount.setRightText(MMKVUserUtils.getInstance().getUserAccount()); + mBarName.setRightText(MMKVUserUtils.getInstance().getUserName()); + mBarIntro.setRightText(MMKVUserUtils.getInstance().getUserIntro()); + + } + + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.userInfoSetting_bar_head: + setHead(); + break; + case R.id.userInfoSetting_bar_name: + setName(); + break; + case R.id.userInfoSetting_bar_intro: + setIntro(); + break; + } + } + + /** + * 设置头像 + */ + private void setHead(){ + ImageSelectActivity.start((BaseActivity) getActivity(), new ImageSelectActivity.OnPhotoSelectListener() { + + @Override + public void onSelected(List files) { + showDialog(); + EasyHttp.post((BaseActivity) getActivity()) + .server(new ImgBBServer()) + .api(new ImgUpLoadApi() + .setImage(files.get(0)) + .setName(MMKVUserUtils.getInstance().getUserAccount()+"_header_"+ TimeUtils.getNowMills())) + .request(new OnHttpListener() { + @Override + public void onSucceed(ImgUploadBean result) { + MMKVUserUtils.getInstance().saveUserHead(result.getData().getUrl()); + initUserInfo(); + hideDialog(); + } + + @Override + public void onFail(Exception e) { + hideDialog(); + toast("图片上传失败,请重试"); + } + }); + + } + + @Override + public void onCancel() { + toast("取消了"); + } + }); + + } + + /** + * 设置昵称 + */ + private void setName(){ + new InputDialog.Builder(this) + .setTitle("设置昵称") + .setHint("请输入您的昵称") + .setContent(MMKVUserUtils.getInstance().getUserName()) + .setAutoDismiss(false) + .setListener(new InputDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog, String content) { + if (content.trim().length() == 0 || content.trim().length() > 16){ + toast("昵称长度限制为 0-16 位"); + return; + } + MMKVUserUtils.getInstance().saveUserName(content.trim()); + DbUserHelper.reUserInfo();//刷新数据库中的当前用户的信息 + initUserInfo(); + dialog.dismiss(); + } + + @Override + public void onCancel(BaseDialog dialog) { + dialog.dismiss(); + } + }) + .show(); + + } + + + /** + * 设置个性签名 + */ + private void setIntro(){ + new InputDialog.Builder(this) + .setTitle("设置个性签名") + .setHint("请输入符合您个性的签名") + .setContent(MMKVUserUtils.getInstance().getUserIntro()) + .setAutoDismiss(false) + .setListener(new InputDialog.OnListener() { + @Override + public void onConfirm(BaseDialog dialog, String content) { + if (content.trim().length() == 0 || content.trim().length() > 50){ + toast("签名长度限制为 0-50 位"); + return; + } + MMKVUserUtils.getInstance().saveUserIntro(content.trim()); + DbUserHelper.reUserInfo();//刷新数据库中的当前用户的信息 + initUserInfo(); + dialog.dismiss(); + } + + @Override + public void onCancel(BaseDialog dialog) { + dialog.dismiss(); + } + }) + .show(); + } + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/adapter/ChatListAdapter.java b/app/src/main/java/com/example/alcoholic/ui/adapter/ChatListAdapter.java new file mode 100644 index 0000000..7ba8c01 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/adapter/ChatListAdapter.java @@ -0,0 +1,71 @@ +package com.example.alcoholic.ui.adapter; + +import android.widget.ImageView; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.db.DbContactsHelper; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.im.IMConstant; +import com.example.alcoholic.utils.EmptyUtils; +import com.orhanobut.logger.Logger; + +import org.jetbrains.annotations.NotNull; + +/** + * Created by + * Description: 聊天列表适配器 + * on 2020/11/13. + */ +public class ChatListAdapter extends BaseQuickAdapter { + public ChatListAdapter() { + super(R.layout.item_chat_list); + } + + @Override + protected void convert(@NotNull BaseViewHolder holder, MessageBean bean) { + + ImageView ivHead = holder.getView(R.id.item_chatList_iv_head); + + + if (bean.getDirection() == IMConstant.MESSAGE_DIRECT_SEND){ + GlideApp.with(getContext()) + .load(EmptyUtils.isEmpty(bean.getToHead())?R.drawable.icon:bean.getToHead()) + .circleCrop() + .into(ivHead); + if (EmptyUtils.isEmpty(bean.getToName())){ + holder.setText(R.id.item_chatList_tv_name,bean.getToAccount()); + }else { + holder.setText(R.id.item_chatList_tv_name,DbUserHelper.getUserShowName(bean.getOtherSide(),bean.getToName())); + } + }else { + if (bean.getChatType() == IMConstant.CHAT_TYPE_CHAT){ + //普通会话 + GlideApp.with(getContext()) + .load(EmptyUtils.isEmpty(bean.getFromHead())?R.drawable.icon:bean.getFromHead()) + .circleCrop() + .into(ivHead); + holder.setText(R.id.item_chatList_tv_name, DbUserHelper.getUserShowName(bean.getOtherSide(),bean.getFromName())); + }else { + //群聊 + GlideApp.with(getContext()) + .load(EmptyUtils.isEmpty(bean.getToHead())?R.drawable.icon:bean.getToHead()) + .circleCrop() + .into(ivHead); + + holder.setText(R.id.item_chatList_tv_name, DbUserHelper.getUserShowName(bean.getOtherSide(),bean.getToName())); + } + + } + + holder.setText(R.id.item_chatList_tv_content,bean.getMsgTextContent()); + + + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/adapter/ContactsAdapter.java b/app/src/main/java/com/example/alcoholic/ui/adapter/ContactsAdapter.java new file mode 100644 index 0000000..4c02649 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/adapter/ContactsAdapter.java @@ -0,0 +1,66 @@ +package com.example.alcoholic.ui.adapter; + +import android.widget.ImageView; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.module.BaseDraggableModule; +import com.chad.library.adapter.base.module.DraggableModule; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.ContactsBean; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.utils.EmptyUtils; + +import org.jetbrains.annotations.NotNull; + +import static com.superrtc.mediamanager.EMediaManager.getContext; + +/** + * Created by + * Description: 通讯录列表适配器BaseItemDraggableAdapter + * on 2020/11/18. + */ +public class ContactsAdapter extends BaseQuickAdapter implements DraggableModule { + + public ContactsAdapter() { + super(R.layout.item_contacts); + } + + @Override + protected void convert(@NotNull BaseViewHolder holder, ContactsBean contactsBean) { + ImageView ivHead = holder.getView(R.id.item_contacts_iv_head); + + UserInfoBean userInfoBean = DbUserHelper.queryUser2Account(contactsBean.getContactsAccount()); + + if (userInfoBean != null){ + GlideApp.with(getContext()) + .load(userInfoBean.getUserHead()) + .circleCrop() + .into(ivHead); + }else { + GlideApp.with(getContext()) + .load(R.drawable.icon) + .circleCrop() + .into(ivHead); + } + + if (EmptyUtils.isEmpty(userInfoBean)){ + if (EmptyUtils.isEmpty(contactsBean.getCodeName())){ + holder.setText(R.id.item_contacts_tv_name,contactsBean.getContactsAccount() + (contactsBean.getIsFriend() ? "":" / 非好友")); + }else { + holder.setText(R.id.item_contacts_tv_name,contactsBean.getContactsAccount() +"("+contactsBean.getCodeName()+")"+ (contactsBean.getIsFriend() ? "":" / 非好友")); + } + }else{ + if (EmptyUtils.isEmpty(contactsBean.getCodeName())){ + + holder.setText(R.id.item_contacts_tv_name,userInfoBean.getUserName()+ (contactsBean.getIsFriend() ? "":" / 非好友")); + }else { + holder.setText(R.id.item_contacts_tv_name,userInfoBean.getUserName()+"("+contactsBean.getCodeName()+")"+ (contactsBean.getIsFriend() ? "":" / 非好友")); + } + } + + + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/adapter/GroupMembersSimpleAdapter.java b/app/src/main/java/com/example/alcoholic/ui/adapter/GroupMembersSimpleAdapter.java new file mode 100644 index 0000000..d17df36 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/adapter/GroupMembersSimpleAdapter.java @@ -0,0 +1,76 @@ +package com.example.alcoholic.ui.adapter; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; +import android.widget.Toast; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.UserInfoBean; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.ui.acitivity.user.UserHomeActivity; +import com.example.alcoholic.utils.EmptyUtils; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.hjq.toast.ToastUtils; +import com.tencent.mmkv.MMKV; + +import org.jetbrains.annotations.NotNull; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description:群组成员适配器 + * on 2020/11/19. + */ +public class GroupMembersSimpleAdapter extends BaseQuickAdapter { + public GroupMembersSimpleAdapter() { + super(R.layout.item_group_members_simple); + } + + @Override + protected void convert(@NotNull BaseViewHolder holder, String userAccount) { + ImageView ivHead = holder.getView(R.id.item_groupMembersSimple_iv_head); + + + UserInfoBean userInfoBean = DbUserHelper.queryUser2Account(userAccount); + + if (EmptyUtils.isNotEmpty(userInfoBean)){ + + GlideApp.with(getContext()) + .load(DbUserHelper.queryUser2Account(userAccount).getUserHead()) + .circleCrop() + .into(ivHead); + }else { + + GlideApp.with(getContext()) + .load(R.drawable.icon) + .circleCrop() + .into(ivHead); + } + + if (userAccount.equals(MMKVUserUtils.getInstance().getUserAccount())){ + + }else { + ivHead.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(getContext(), UserHomeActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,userAccount); + intent.putExtras(bundle); + getContext().startActivity(intent); + } + }); + } + + holder.setText(R.id.item_groupMembersSimple_tv_name, DbUserHelper.getUserShowName(userAccount,DbUserHelper.getUserName(userAccount))); + + + + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/adapter/ImageSelectAdapter.java b/app/src/main/java/com/example/alcoholic/ui/adapter/ImageSelectAdapter.java new file mode 100644 index 0000000..1b8b3cf --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/adapter/ImageSelectAdapter.java @@ -0,0 +1,65 @@ +package com.example.alcoholic.ui.adapter; + +import android.content.Context; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyAdapter; +import com.example.alcoholic.http.glide.GlideApp; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/07/24 + * desc : 图片选择适配器 + */ +public final class ImageSelectAdapter extends MyAdapter { + + private final List mSelectImages; + + public ImageSelectAdapter(Context context, List images) { + super(context); + mSelectImages = images; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(); + } + + private final class ViewHolder extends MyAdapter.ViewHolder { + + private ImageView mImageView; + private CheckBox mCheckBox; + + private ViewHolder() { + super(R.layout.item_image_select); + mImageView = (ImageView) findViewById(R.id.iv_image_select_image); + mCheckBox = (CheckBox) findViewById(R.id.iv_image_select_check); + } + + @Override + public void onBindView(int position) { + String imagePath = getItem(position); + GlideApp.with(getContext()) + .load(imagePath) + .into(mImageView); + + mCheckBox.setChecked(mSelectImages.contains(imagePath)); + } + } + + @Override + protected RecyclerView.LayoutManager generateDefaultLayoutManager(Context context) { + return new GridLayoutManager(context, 3); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/adapter/MsgListAdapter.java b/app/src/main/java/com/example/alcoholic/ui/adapter/MsgListAdapter.java new file mode 100644 index 0000000..93a89a0 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/adapter/MsgListAdapter.java @@ -0,0 +1,157 @@ +package com.example.alcoholic.ui.adapter; + +import android.content.Entity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.chad.library.adapter.base.BaseDelegateMultiAdapter; +import com.chad.library.adapter.base.BaseMultiItemQuickAdapter; +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.delegate.BaseMultiTypeDelegate; +import com.chad.library.adapter.base.entity.MultiItemEntity; +import com.chad.library.adapter.base.module.UpFetchModule; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.im.IMConstant; +import com.example.alcoholic.ui.acitivity.user.UserHomeActivity; +import com.example.alcoholic.utils.TimeUtils; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description: + * on 2020/11/13. + */ +public class MsgListAdapter extends BaseDelegateMultiAdapter implements UpFetchModule { + + private int chatType = IMConstant.CHAT_TYPE_CHAT; + + public MsgListAdapter() { + super(null); + + //Step.1 + setMultiTypeDelegate(new BaseMultiTypeDelegate(){ + @Override + public int getItemType(@NotNull List list, int i) { + return list.get(i).getDirection(); + } + }); + + + //Step.2 + getMultiTypeDelegate() + .addItemType(IMConstant.MESSAGE_DIRECT_RECEIVE, R.layout.item_msg_left) + .addItemType(IMConstant.MESSAGE_DIRECT_SEND, R.layout.item_msg_right); + + + + } + + @Override + protected void convert(@NotNull BaseViewHolder holder, MessageBean item) { + + + switch (holder.getItemViewType()){ + case IMConstant.MESSAGE_DIRECT_RECEIVE: + convertLeft(holder,item); + break; + case IMConstant.MESSAGE_DIRECT_SEND: + convertRight(holder,item); + break; + default: + break; + } + + + + } + + + /** + * 左边聊天框 + */ + private void convertLeft(@NotNull BaseViewHolder holder, MessageBean item){ + ImageView ivHead = holder.getView(R.id.item_msgLeft_iv_head); + + GlideApp.with(getContext()) + .load(item.getFromHead()) + .circleCrop() + .into(ivHead); + + ivHead.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(getContext(), UserHomeActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,item.getFromAccount()); + intent.putExtras(bundle); + getContext().startActivity(intent); + } + }); + + //名字 + if (chatType == IMConstant.CHAT_TYPE_CHAT){ + holder.setGone(R.id.item_msgLeft_tv_name,true); + }else { + holder.setGone(R.id.item_msgLeft_tv_name,false); + holder.setText(R.id.item_msgLeft_tv_name,item.getFromName()); + } + + + if (holder.getLayoutPosition() == 0){ + holder.setGone(R.id.item_msgLeft_tv_date,false); + }else if (item.getMsgTime() - getItem(holder.getLayoutPosition()-1).getMsgTime() < TimeUtils.TimeConstants.MIN){ + holder.setGone(R.id.item_msgLeft_tv_date,false); + }else { + holder.setGone(R.id.item_msgLeft_tv_date,true); + } + //时间 + holder.setText(R.id.item_msgLeft_tv_date, TimeUtils.millis2String(item.getMsgTime())); + + //内容 + holder.setText(R.id.item_msgLeft_tv_content,item.getMsgTextContent()); + + + } + + + /** + * 右边聊天框 + */ + private void convertRight(@NotNull BaseViewHolder holder, MessageBean item){ + ImageView ivHead = holder.getView(R.id.item_msgRight_iv_head); + + GlideApp.with(getContext()) + .load(item.getFromHead()) + .circleCrop() + .into(ivHead); + + //时间 + if (holder.getLayoutPosition() == 0){ + holder.setGone(R.id.item_msgRight_tv_date,false); + }else if (item.getMsgTime() - getItem(holder.getLayoutPosition()-1).getMsgTime() < TimeUtils.TimeConstants.MIN){ + holder.setGone(R.id.item_msgRight_tv_date,false); + }else { + holder.setGone(R.id.item_msgRight_tv_date,true); + } + holder.setText(R.id.item_msgRight_tv_date, TimeUtils.millis2String(item.getMsgTime())); + + //内容 + holder.setText(R.id.item_msgRight_tv_content,item.getMsgTextContent()); + } + + + public void setChatType(int chatType) { + this.chatType = chatType; + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/dialog/AlbumDialog.java b/app/src/main/java/com/example/alcoholic/ui/dialog/AlbumDialog.java new file mode 100644 index 0000000..1688203 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/dialog/AlbumDialog.java @@ -0,0 +1,191 @@ +package com.example.alcoholic.ui.dialog; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyAdapter; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.base.BaseAdapter; +import com.example.base.BaseDialog; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/07/27 + * desc : 相册专辑选取对话框 + */ +public final class AlbumDialog { + + public static final class Builder + extends BaseDialog.Builder + implements BaseAdapter.OnItemClickListener { + + private OnListener mListener; + + private final RecyclerView mRecyclerView; + private final AlbumAdapter mAdapter; + + public Builder(Context context) { + super(context); + + setContentView(R.layout.dialog_album); + setHeight(getResources().getDisplayMetrics().heightPixels / 2); + + mRecyclerView = findViewById(R.id.rv_album_list); + mAdapter = new AlbumAdapter(context); + mAdapter.setOnItemClickListener(this); + mRecyclerView.setAdapter(mAdapter); + } + + public Builder setData(List data) { + mAdapter.setData(data); + // 滚动到选中的位置 + for (int i = 0; i < data.size(); i++) { + if (data.get(i).isSelect()) { + mRecyclerView.scrollToPosition(i); + } + } + return this; + } + + public Builder setListener(OnListener listener) { + mListener = listener; + return this; + } + + @Override + public void onItemClick(RecyclerView recyclerView, View itemView, int position) { + List data = mAdapter.getData(); + if (data == null) { + return; + } + + for (AlbumInfo info : data) { + if (info.isSelect()) { + info.setSelect(false); + break; + } + } + mAdapter.getItem(position).setSelect(true); + mAdapter.notifyDataSetChanged(); + + // 延迟消失 + postDelayed(() -> { + + if (mListener != null) { + mListener.onSelected(getDialog(), position, mAdapter.getItem(position)); + } + dismiss(); + + }, 300); + } + } + + private static final class AlbumAdapter extends MyAdapter { + + private AlbumAdapter(Context context) { + super(context); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(); + } + + private final class ViewHolder extends MyAdapter.ViewHolder { + + private final ImageView mIconView; + private final TextView mNameView; + private final TextView mRemarkView; + private final CheckBox mCheckBox; + + private ViewHolder() { + super(R.layout.item_album); + mIconView = (ImageView) findViewById(R.id.iv_album_icon); + mNameView = (TextView) findViewById(R.id.tv_album_name); + mRemarkView = (TextView) findViewById(R.id.tv_album_remark); + mCheckBox = (CheckBox) findViewById(R.id.rb_album_check); + } + + @Override + public void onBindView(int position) { + AlbumInfo info = getItem(position); + + GlideApp.with(getContext()) + .load(info.getIcon()) + .into(mIconView); + + mNameView.setText(info.getName()); + mRemarkView.setText(info.getRemark()); + mCheckBox.setChecked(info.isSelect()); + mCheckBox.setVisibility(info.isSelect() ? View.VISIBLE : View.INVISIBLE); + } + } + } + + /** + * 专辑信息类 + */ + public static class AlbumInfo { + + /** 封面 */ + private String icon; + /** 名称 */ + private String name; + /** 备注 */ + private String remark; + /** 选中 */ + private boolean select; + + public AlbumInfo(String icon, String name, String remark, boolean select) { + this.icon = icon; + this.name = name; + this.remark = remark; + this.select = select; + } + + public void setName(String name) { + this.name = name; + } + + public void setSelect(boolean select) { + this.select = select; + } + + public String getIcon() { + return icon; + } + + public String getName() { + return name; + } + + public String getRemark() { + return remark; + } + + public boolean isSelect() { + return select; + } + } + + public interface OnListener { + + /** + * 选择条目时回调 + */ + void onSelected(BaseDialog dialog, int position, AlbumInfo bean); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/dialog/InputDialog.java b/app/src/main/java/com/example/alcoholic/ui/dialog/InputDialog.java new file mode 100644 index 0000000..7ba1788 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/dialog/InputDialog.java @@ -0,0 +1,106 @@ +package com.example.alcoholic.ui.dialog; + +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import com.example.alcoholic.R; +import com.example.base.BaseDialog; + +import androidx.annotation.StringRes; + +/** + * Created by + * Description: 输入弹窗 + * on 2020/11/19. + */ +public class InputDialog { + + public static final class Builder + extends UIDialog.Builder + implements BaseDialog.OnShowListener { + + private OnListener mListener; + private final EditText mInputView; + + public Builder(Context context) { + super(context); + + setCustomView(R.layout.dialog_input); + setCanceledOnTouchOutside(false); + + mInputView = findViewById(R.id.dialog_input_et_content); + + addOnShowListener(this); + } + + public Builder setHint(@StringRes int id) { + return setHint(getString(id)); + } + public Builder setHint(CharSequence text) { + mInputView.setHint(text); + return this; + } + + public Builder setContent(@StringRes int id) { + return setContent(getString(id)); + } + public Builder setContent(CharSequence text) { + mInputView.setText(text); + int index = mInputView.getText().toString().length(); + if (index > 0) { + mInputView.requestFocus(); + mInputView.setSelection(index); + } + return this; + } + + public Builder setListener(OnListener listener) { + mListener = listener; + return this; + } + + /** + * {@link BaseDialog.OnShowListener} + */ + @Override + public void onShow(BaseDialog dialog) { + postDelayed(() -> getSystemService(InputMethodManager.class).showSoftInput(mInputView, 0), 500); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.tv_ui_confirm: + autoDismiss(); + if (mListener != null) { + mListener.onConfirm(getDialog(), mInputView.getText().toString()); + } + break; + case R.id.tv_ui_cancel: + autoDismiss(); + if (mListener != null) { + mListener.onCancel(getDialog()); + } + break; + default: + break; + } + } + } + + public interface OnListener { + + /** + * 点击确定时回调 + */ + void onConfirm(BaseDialog dialog, String content); + + /** + * 点击取消时回调 + */ + default void onCancel(BaseDialog dialog) {} + } + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/dialog/MessageDialog.java b/app/src/main/java/com/example/alcoholic/ui/dialog/MessageDialog.java new file mode 100644 index 0000000..926b7ea --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/dialog/MessageDialog.java @@ -0,0 +1,87 @@ +package com.example.alcoholic.ui.dialog; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.base.BaseDialog; + +import androidx.annotation.StringRes; + +/** + * Created by + * Description:消息对话框 + * on 2020/11/18. + */ +public class MessageDialog { + + public static final class Builder + extends UIDialog.Builder { + + private OnListener mListener; + + private final TextView mMessageView; + + public Builder(Context context) { + super(context); + setCustomView(R.layout.dialog_message); + mMessageView = findViewById(R.id.tv_message_message); + } + + public Builder setMessage(@StringRes int id) { + return setMessage(getString(id)); + } + public Builder setMessage(CharSequence text) { + mMessageView.setText(text); + return this; + } + + public Builder setListener(OnListener listener) { + mListener = listener; + return this; + } + + @Override + public BaseDialog create() { + // 如果内容为空就抛出异常 + if ("".equals(mMessageView.getText().toString())) { + throw new IllegalArgumentException("Dialog message not null"); + } + return super.create(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.tv_ui_confirm: + autoDismiss(); + if (mListener != null) { + mListener.onConfirm(getDialog()); + } + break; + case R.id.tv_ui_cancel: + autoDismiss(); + if (mListener != null) { + mListener.onCancel(getDialog()); + } + break; + default: + break; + } + } + } + + public interface OnListener { + + /** + * 点击确定时回调 + */ + void onConfirm(BaseDialog dialog); + + /** + * 点击取消时回调 + */ + default void onCancel(BaseDialog dialog) {} + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/dialog/SyncContactsDialog.java b/app/src/main/java/com/example/alcoholic/ui/dialog/SyncContactsDialog.java new file mode 100644 index 0000000..5968494 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/dialog/SyncContactsDialog.java @@ -0,0 +1,171 @@ +package com.example.alcoholic.ui.dialog; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.bean.ContactsBean; +import com.example.alcoholic.db.DbContactsHelper; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.example.base.BaseDialog; +import com.hyphenate.chat.EMClient; +import com.hyphenate.chat.EMGroup; +import com.hyphenate.exceptions.HyphenateException; +import com.orhanobut.logger.Logger; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableEmitter; +import io.reactivex.rxjava3.core.ObservableOnSubscribe; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Created by + * Description: 联系人同步dialog + * on 2020/11/18. + */ +public class SyncContactsDialog { + + public static final class Builder + extends UIDialog.Builder{ + + private TextView mTvContent; + private TextView mTvSchedule; + private View mLineButton; + private TextView mBtnOver; + + public Builder(Context context) { + super(context); + setCustomView(R.layout.dialog_contacts_sync); + setCanceledOnTouchOutside(false); + + mTvContent = findViewById(R.id.dialog_contactsSync_tv_content); + mTvSchedule = findViewById(R.id.dialog_contactsSync_tv_schedule); + mLineButton = findViewById(R.id.dialog_contactsSync_line_button); + mBtnOver = findViewById(R.id.dialog_contactsSync_btn_over); + mLineButton.setVisibility(View.GONE); + mBtnOver.setVisibility(View.GONE); + hideButton(); + + setOnClickListener(mBtnOver); + + + addOnShowListener(new BaseDialog.OnShowListener() { + @Override + public void onShow(BaseDialog dialog) { + + sync(); + } + }); + + } + + /** + * 开始同步 + */ + private void sync(){ + + Observable observable = Observable.create(new ObservableOnSubscribe() { + @Override + public void subscribe(@NonNull ObservableEmitter emitter) throws Throwable { + List contacts = EMClient.getInstance().contactManager().getAllContactsFromServer(); + List blacks = EMClient.getInstance().contactManager().getBlackListFromServer(); + + //从服务器获取自己加入的和创建的群组列表,此api获取的群组sdk会自动保存到内存和db。 +// List grouplist = EMClient.getInstance().groupManager().getJoinedGroupsFromServer(); +// EMClient.getInstance().chatroomManager().fetchChatRoomFromServer(roomId) + + + int contactsSize = contacts.size(); + int blackSize = blacks.size(); + for (int i = 0 ; i < contactsSize; i++) { + emitter.onNext((i+1)+" / "+ (contactsSize+blackSize)); + //存储一个新联系人 + DbContactsHelper.insetContacts(DbContactsHelper.createContact(contacts.get(i),true)); + } + + + for (int i = 0; i < blackSize; i++) { + emitter.onNext((contactsSize+i+1)+" / "+ (contactsSize+blackSize)); + //存储一个新联系人 + ContactsBean contactsBean = DbContactsHelper.createContact(contacts.get(i),true); + contactsBean.setIsBlack(true); + DbContactsHelper.insetContacts(contactsBean); + } + + emitter.onComplete(); + } + }); + + //创建观察者 + Observer observer = new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + + } + + @Override + public void onNext(@NonNull String s) { + mTvSchedule.setText(s); + } + + @Override + public void onError(@NonNull Throwable e) { + + } + + @Override + public void onComplete() { + //结束 + mTvContent.setText("已同步完成"); + mLineButton.setVisibility(View.VISIBLE); + mBtnOver.setVisibility(View.VISIBLE); + } + }; + + observable.subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(observer); + + + + } + + + + + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.dialog_contactsSync_btn_over: + autoDismiss(); + + break; + case R.id.tv_ui_confirm: +// autoDismiss(); + + break; + case R.id.tv_ui_cancel: + autoDismiss(); + break; + default: + break; + } + } + + + + + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/dialog/UIDialog.java b/app/src/main/java/com/example/alcoholic/ui/dialog/UIDialog.java new file mode 100644 index 0000000..336b328 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/dialog/UIDialog.java @@ -0,0 +1,111 @@ +package com.example.alcoholic.ui.dialog; + +import android.content.Context; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + + +import com.example.alcoholic.R; +import com.example.base.BaseDialog; + +import androidx.annotation.LayoutRes; +import androidx.annotation.StringRes; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/21 + * desc : 项目通用 Dialog 布局封装 + */ +public final class UIDialog { + + @SuppressWarnings("unchecked") + public static class Builder + extends BaseDialog.Builder { + + private boolean mAutoDismiss = true; + + private final ViewGroup mContainerLayout; + private final TextView mTitleView; + + private final View mLineButton; + private final View mLayoutButton; + private final TextView mCancelView; + private final View mLineView; + private final TextView mConfirmView; + + public Builder(Context context) { + super(context); + + setContentView(R.layout.dialog_ui); + setAnimStyle(BaseDialog.ANIM_IOS); + setGravity(Gravity.CENTER); + + mContainerLayout = findViewById(R.id.ll_ui_container); + mTitleView = findViewById(R.id.tv_ui_title); + mLineButton = findViewById(R.id.dialog_ui_line_button); + mLayoutButton = findViewById(R.id.dialog_ui_layout_button); + mCancelView = findViewById(R.id.tv_ui_cancel); + mLineView = findViewById(R.id.v_ui_line); + mConfirmView = findViewById(R.id.tv_ui_confirm); + setOnClickListener(mCancelView, mConfirmView); + } + + public B setCustomView(@LayoutRes int id) { + return setCustomView(LayoutInflater.from(getContext()).inflate(id, mContainerLayout, false)); + } + + public B setCustomView(View view) { + mContainerLayout.addView(view, 1); + return (B) this; + } + + + public B setTitle(@StringRes int id) { + return setTitle(getString(id)); + } + public B setTitle(CharSequence text) { + mTitleView.setText(text); + return (B) this; + } + + public B hideButton(){ + mCancelView.setVisibility(View.GONE); + mConfirmView.setVisibility(View.GONE); + mLineButton.setVisibility(View.GONE); + mLayoutButton.setVisibility(View.GONE); + return (B)this; + } + + public B setCancel(@StringRes int id) { + return setCancel(getString(id)); + } + public B setCancel(CharSequence text) { + mCancelView.setText(text); + mLineView.setVisibility((text == null || "".equals(text.toString())) ? View.GONE : View.VISIBLE); + return (B) this; + } + + public B setConfirm(@StringRes int id) { + return setConfirm(getString(id)); + } + public B setConfirm(CharSequence text) { + mConfirmView.setText(text); + return (B) this; + } + + public B setAutoDismiss(boolean dismiss) { + mAutoDismiss = dismiss; + return (B) this; + } + + public void autoDismiss() { + if (mAutoDismiss) { + dismiss(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/dialog/WaitDialog.java b/app/src/main/java/com/example/alcoholic/ui/dialog/WaitDialog.java new file mode 100644 index 0000000..8b68f2b --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/dialog/WaitDialog.java @@ -0,0 +1,46 @@ +package com.example.alcoholic.ui.dialog; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; + + +import com.example.alcoholic.R; +import com.example.base.BaseDialog; + +import androidx.annotation.StringRes; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/12/2 + * desc : 等待加载对话框 + */ +public final class WaitDialog { + + public static final class Builder + extends BaseDialog.Builder { + + private final TextView mMessageView; + + public Builder(Context context) { + super(context); + setContentView(R.layout.dialog_wait); + setAnimStyle(BaseDialog.ANIM_TOAST); + setBackgroundDimEnabled(false); + setCancelable(false); + + mMessageView = findViewById(R.id.tv_wait_message); + } + + public Builder setMessage(@StringRes int id) { + return setMessage(getString(id)); + } + + public Builder setMessage(CharSequence text) { + mMessageView.setText(text); + mMessageView.setVisibility(text == null ? View.GONE : View.VISIBLE); + return this; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/ui/fragment/FindFragment.java b/app/src/main/java/com/example/alcoholic/ui/fragment/FindFragment.java new file mode 100644 index 0000000..ae21bae --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/fragment/FindFragment.java @@ -0,0 +1,34 @@ +package com.example.alcoholic.ui.fragment; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyFragment; +import com.example.alcoholic.ui.acitivity.HomeActivity; + +import androidx.fragment.app.Fragment; + +/** + * Created by + * Description:探索页面 + * on 2020/11/13. + */ +public final class FindFragment extends MyFragment { + + public static FindFragment newInstance() { + return new FindFragment(); + } + + @Override + protected int getLayoutId() { + return R.layout.fragment_find; + } + + @Override + protected void initView() { + + } + + @Override + protected void initData() { + + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/fragment/HomeFragment.java b/app/src/main/java/com/example/alcoholic/ui/fragment/HomeFragment.java new file mode 100644 index 0000000..92e765a --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/fragment/HomeFragment.java @@ -0,0 +1,32 @@ +package com.example.alcoholic.ui.fragment; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyFragment; +import com.example.alcoholic.ui.acitivity.HomeActivity; + +/** + * Created by + * Description: 首页 + * on 2020/11/13. + */ +public final class HomeFragment extends MyFragment { + + public static HomeFragment newInstance() { + return new HomeFragment(); + } + + @Override + protected int getLayoutId() { + return R.layout.fragment_home; + } + + @Override + protected void initView() { + + } + + @Override + protected void initData() { + + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/fragment/MeFragment.java b/app/src/main/java/com/example/alcoholic/ui/fragment/MeFragment.java new file mode 100644 index 0000000..5e22573 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/fragment/MeFragment.java @@ -0,0 +1,116 @@ +package com.example.alcoholic.ui.fragment; + +import android.content.Intent; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.example.alcoholic.R; +import com.example.alcoholic.common.MyFragment; +import com.example.alcoholic.db.DbUserHelper; +import com.example.alcoholic.http.glide.GlideApp; +import com.example.alcoholic.ui.acitivity.HomeActivity; +import com.example.alcoholic.ui.acitivity.user.ContactsActivity; +import com.example.alcoholic.ui.acitivity.user.LoginActivity; +import com.example.alcoholic.ui.acitivity.user.SettingActivity; +import com.example.alcoholic.ui.acitivity.user.UserInfoSettingActivity; +import com.example.alcoholic.utils.MMKVUserUtils; +import com.example.base.BaseActivity; +import com.example.widget.layout.SettingBar; +import com.gyf.immersionbar.ImmersionBar; +import com.hyphenate.EMCallBack; +import com.hyphenate.EMConnectionListener; +import com.hyphenate.EMError; +import com.hyphenate.chat.EMClient; +import com.hyphenate.exceptions.HyphenateException; +import com.hyphenate.util.EMLog; +import com.orhanobut.logger.Logger; + +import androidx.annotation.Nullable; + +/** + * Created by + * Description: ‘我的’页面 + * on 2020/11/13. + */ +public final class MeFragment extends MyFragment { + + private LinearLayout mLayoutUserInfo; + private ImageView mIvHead; + private TextView mTvName; + private SettingBar mBarContacts; + private SettingBar mBarSetting; + + public static MeFragment newInstance() { + return new MeFragment(); + } + + @Override + protected int getLayoutId() { + return R.layout.fragment_me; + } + + @Override + protected void initView() { + getStatusBarConfig().titleBar(R.id.f_me_layout_userInfo).init(); + + mLayoutUserInfo = findViewById(R.id.f_me_layout_userInfo); + mIvHead = findViewById(R.id.f_me_iv_head); + mTvName = findViewById(R.id.f_me_tv_name); + mBarContacts = findViewById(R.id.f_me_bar_contacts); + mBarSetting = findViewById(R.id.f_me_bar_setting); + + setOnClickListener(mLayoutUserInfo,mBarContacts,mBarSetting); + + + initUserInfo(); + } + + @Override + protected void initData() { + + + + + } + + + private void initUserInfo(){ + GlideApp.with(getContext()) + .load(MMKVUserUtils.getInstance().getUserHead()) + .circleCrop() + .into(mIvHead); + + mTvName.setText(MMKVUserUtils.getInstance().getUserName()); + + } + + + @Override + public void onClick(View v) { + switch (v.getId()){ + case R.id.f_me_layout_userInfo: + startActivityForResult(UserInfoSettingActivity.class, new BaseActivity.OnActivityCallback() { + @Override + public void onActivityResult(int resultCode, @Nullable Intent data) { + initUserInfo(); + } + }); + break; + case R.id.f_me_bar_contacts: + //联系人 + startActivity(ContactsActivity.class); + break; + case R.id.f_me_bar_setting: + //联系人 + startActivity(SettingActivity.class); + break; + } + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/ui/fragment/MessageFragment.java b/app/src/main/java/com/example/alcoholic/ui/fragment/MessageFragment.java new file mode 100644 index 0000000..f920a06 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/fragment/MessageFragment.java @@ -0,0 +1,131 @@ +package com.example.alcoholic.ui.fragment; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.listener.OnItemClickListener; +import com.example.alcoholic.R; +import com.example.alcoholic.bean.MessageBean; +import com.example.alcoholic.common.MyFragment; +import com.example.alcoholic.db.DbMsgHelper; +import com.example.alcoholic.ui.acitivity.ChatActivity; +import com.example.alcoholic.ui.acitivity.HomeActivity; +import com.example.alcoholic.ui.adapter.ChatListAdapter; +import com.example.alcoholic.widget.decoration.ChatListDecoration; +import com.orhanobut.logger.Logger; +import com.scwang.smart.refresh.layout.SmartRefreshLayout; +import com.scwang.smart.refresh.layout.api.RefreshLayout; +import com.scwang.smart.refresh.layout.listener.OnRefreshListener; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_CHAT_TYPE; +import static com.example.alcoholic.constant.JumpDataContants.JUMP_DATA_USER_ACCOUNT; + +/** + * Created by + * Description: 消息页面 + * on 2020/11/13. + */ +public final class MessageFragment extends MyFragment implements OnRefreshListener { + + private RecyclerView mRvChat; + private SmartRefreshLayout mRefreshLayout; + + private ChatListAdapter mAdapter; + + + + public static MessageFragment newInstance() { + return new MessageFragment(); + } + + @Override + protected int getLayoutId() { + return R.layout.fragment_message; + } + + + + @Override + protected void initView() { + EventBus.getDefault().register(this); + mRefreshLayout = findViewById(R.id.f_message_refresh_chat); + mRvChat = findViewById(R.id.f_message_rv_chat); + + initList(); + } + + @Override + public void onDestroy() { + EventBus.getDefault().unregister(this); + super.onDestroy(); + } + + @Override + public boolean isStatusBarEnabled() { + // 使用沉浸式状态栏 + return !super.isStatusBarEnabled(); + } + + + /** + * 获取到新消息 + */ + @Subscribe(threadMode = ThreadMode.MAIN) + public void onReceiveNewMessage(MessageBean newMsgBean) { + initData(); + }; + + @Override + protected void initData() { + + mAdapter.setNewData(DbMsgHelper.queryChat2Me()); + + if (mRefreshLayout.isRefreshing()){ + mRefreshLayout.finishRefresh(); + } + } + + private void initList(){ + mRefreshLayout.setOnRefreshListener(this); + mRefreshLayout.setEnableLoadMore(false); + mAdapter = new ChatListAdapter(); + mRvChat.setLayoutManager(new LinearLayoutManager(getContext())); + mRvChat.addItemDecoration(new ChatListDecoration(getContext())); + mRvChat.setAdapter(mAdapter); + mAdapter.setEmptyView(R.layout.layout_empty); + + mAdapter.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(@NonNull BaseQuickAdapter adapter, @NonNull View view, int position) { + Intent intent = new Intent(getContext(),ChatActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(JUMP_DATA_USER_ACCOUNT,mAdapter.getItem(position).getOtherSide()); + bundle.putInt(JUMP_DATA_CHAT_TYPE,mAdapter.getItem(position).getChatType()); + intent.putExtras(bundle); + startActivity(intent); + } + }); + + + } + + /** + * 刷新 + * @param refreshLayout + */ + @Override + public void onRefresh(@NonNull RefreshLayout refreshLayout) { + initData(); + } +} diff --git a/app/src/main/java/com/example/alcoholic/ui/page/ImagePagerAdapter.java b/app/src/main/java/com/example/alcoholic/ui/page/ImagePagerAdapter.java new file mode 100644 index 0000000..40cba90 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/ui/page/ImagePagerAdapter.java @@ -0,0 +1,68 @@ +package com.example.alcoholic.ui.page; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; + +import com.example.alcoholic.http.glide.GlideApp; +import com.github.chrisbanes.photoview.PhotoView; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/03/05 + * desc : 图片加载适配器 + */ +public final class ImagePagerAdapter extends PagerAdapter + implements View.OnClickListener { + + private final Activity mActivity; + private final List mData; + + public ImagePagerAdapter(Activity activity, List data) { + mActivity = activity; + mData = data; + } + + @Override + public int getCount() { + return mData.size(); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + PhotoView view = new PhotoView(mActivity); + view.setOnClickListener(this); + + GlideApp.with(container.getContext()) + .load(mData.get(position)) + .into(view); + + container.addView(view); + return view; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + container.removeView((View) object); + } + + @Override + public void onClick(View v) { + // 单击图片退出当前的 Activity + if (!mActivity.isFinishing()) { + mActivity.finish(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/utils/AESUtils.java b/app/src/main/java/com/example/alcoholic/utils/AESUtils.java new file mode 100644 index 0000000..1579df6 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/AESUtils.java @@ -0,0 +1,63 @@ +package com.example.alcoholic.utils; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Created by + * Description: aes 工具类 + * on 2020/11/17. + */ +public class AESUtils { + /* 使用CFB加密,需要设置IV */ + private static final String CIPHER_MODE = "AES/CBC/PKCS5Padding"; + private static final String IV_STRING = "16-Bytes--String"; + + /** + * 加密 + */ + public static String encrypt(String key, String content) + throws InvalidKeyException, NoSuchAlgorithmException, + NoSuchPaddingException, UnsupportedEncodingException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + byte[] byteContent = content.getBytes("UTF-8"); + // 注意,为了能与 iOS 统一 + // 这里的 key 不可以使用 KeyGenerator、SecureRandom、SecretKey 生成 + byte[] enCodeFormat = key.getBytes(); + SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES"); + byte[] initParam = IV_STRING.getBytes(); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initParam); + // 指定加密的算法、工作模式和填充方式 + Cipher cipher = Cipher.getInstance(CIPHER_MODE); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + byte[] encryptedBytes = cipher.doFinal(byteContent); + // 同样对加密后数据进行 base64 编码 + return Base64Utils.encodeToString(encryptedBytes); + } + + /** + * 解密 + */ + public static String decrypt(String key ,String content) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException { + // base64 解码 + byte[] encryptedBytes = Base64Utils.decodeFromString(content); + byte[] enCodeFormat = key.getBytes(); + SecretKeySpec secretKey = new SecretKeySpec(enCodeFormat, "AES"); + byte[] initParam = IV_STRING.getBytes(); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initParam); + Cipher cipher = Cipher.getInstance(CIPHER_MODE); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); + byte[] result = cipher.doFinal(encryptedBytes); + return new String(result, "UTF-8"); + } + +} diff --git a/app/src/main/java/com/example/alcoholic/utils/Base64Utils.java b/app/src/main/java/com/example/alcoholic/utils/Base64Utils.java new file mode 100644 index 0000000..c143986 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/Base64Utils.java @@ -0,0 +1,120 @@ +package com.example.alcoholic.utils; + +import android.os.Build; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import androidx.annotation.RequiresApi; + +/** + * Created by + * Description: + * on 2020/11/17. + */ +public class Base64Utils { + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + + /** + * Base64-encode the given byte array. + * @param src the original byte array + * @return the encoded byte array + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public static byte[] encode(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getEncoder().encode(src); + } + + /** + * Base64-decode the given byte array. + * @param src the encoded byte array + * @return the original byte array + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public static byte[] decode(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getDecoder().decode(src); + } + + /** + * Base64-encode the given byte array using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the original byte array + * @return the encoded byte array + * @since 4.2.4 + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public static byte[] encodeUrlSafe(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getUrlEncoder().encode(src); + } + + /** + * Base64-decode the given byte array using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the encoded byte array + * @return the original byte array + * @since 4.2.4 + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public static byte[] decodeUrlSafe(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getUrlDecoder().decode(src); + } + + /** + * Base64-encode the given byte array to a String. + * @param src the original byte array + * @return the encoded byte array as a UTF-8 String + */ + public static String encodeToString(byte[] src) { + if (src.length == 0) { + return ""; + } + return new String(encode(src), DEFAULT_CHARSET); + } + + /** + * Base64-decode the given byte array from an UTF-8 String. + * @param src the encoded UTF-8 String + * @return the original byte array + */ + public static byte[] decodeFromString(String src) { + if (src.isEmpty()) { + return new byte[0]; + } + return decode(src.getBytes(DEFAULT_CHARSET)); + } + + /** + * Base64-encode the given byte array to a String using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the original byte array + * @return the encoded byte array as a UTF-8 String + */ + public static String encodeToUrlSafeString(byte[] src) { + return new String(encodeUrlSafe(src), DEFAULT_CHARSET); + } + + /** + * Base64-decode the given byte array from an UTF-8 String using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the encoded UTF-8 String + * @return the original byte array + */ + public static byte[] decodeFromUrlSafeString(String src) { + return decodeUrlSafe(src.getBytes(DEFAULT_CHARSET)); + } + +} diff --git a/app/src/main/java/com/example/alcoholic/utils/ContactsUtils.java b/app/src/main/java/com/example/alcoholic/utils/ContactsUtils.java new file mode 100644 index 0000000..3122e3b --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/ContactsUtils.java @@ -0,0 +1,9 @@ +package com.example.alcoholic.utils; + +/** + * Created by + * Description: + * on 2020/11/18. + */ +public class ContactsUtils { +} diff --git a/app/src/main/java/com/example/alcoholic/utils/EmptyUtils.java b/app/src/main/java/com/example/alcoholic/utils/EmptyUtils.java new file mode 100644 index 0000000..6ca7324 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/EmptyUtils.java @@ -0,0 +1,73 @@ +package com.example.alcoholic.utils; + +import android.os.Build; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import android.util.SparseLongArray; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; + +/** + * Created by + * Description: 判空工具类 + * on 2020/11/17. + */ +public class EmptyUtils { + + private EmptyUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 判断对象是否为空 + * + * @param obj 对象 + * @return {@code true}: 为空
{@code false}: 不为空 + */ + public static boolean isEmpty(Object obj) { + if (obj == null) { + return true; + } + if (obj instanceof String && obj.toString().length() == 0) { + return true; + } + if (obj.getClass().isArray() && Array.getLength(obj) == 0) { + return true; + } + if (obj instanceof Collection && ((Collection) obj).isEmpty()) { + return true; + } + if (obj instanceof Map && ((Map) obj).isEmpty()) { + return true; + } + if (obj instanceof SparseArray && ((SparseArray) obj).size() == 0) { + return true; + } + if (obj instanceof SparseBooleanArray && ((SparseBooleanArray) obj).size() == 0) { + return true; + } + if (obj instanceof SparseIntArray && ((SparseIntArray) obj).size() == 0) { + return true; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (obj instanceof SparseLongArray && ((SparseLongArray) obj).size() == 0) { + return true; + } + } + return false; + } + + /** + * 判断对象是否非空 + * + * @param obj 对象 + * @return {@code true}: 非空
{@code false}: 空 + */ + public static boolean isNotEmpty(Object obj) { + return !isEmpty(obj); + } + +} diff --git a/app/src/main/java/com/example/alcoholic/utils/MMKVStytemUtils.java b/app/src/main/java/com/example/alcoholic/utils/MMKVStytemUtils.java new file mode 100644 index 0000000..7a2d9f9 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/MMKVStytemUtils.java @@ -0,0 +1,86 @@ +package com.example.alcoholic.utils; + +import android.text.format.Time; + +import com.tencent.mmkv.MMKV; + +/** + * Created by + * Description:mmkv 系统信息 + * on 2020/11/16. + */ +public class MMKVStytemUtils { + + private static MMKVStytemUtils mInstance; + + private static MMKV mKv; + + /** + * 密文密钥 + * 危机时刻:存储当前时间戳,如果在当前时间 +1 天内,则处于危急时刻 + */ + private static final String STYTEM_AES_KEY = "stytemAesKey"; + private static final String STYTEM_ALARM_TIME = "stytemAlarmTime"; + + + public MMKVStytemUtils() { + mKv = MMKV.defaultMMKV(); + } + + /** + * 单例 + */ + public static MMKVStytemUtils getInstance(){ + if (mInstance == null) { + synchronized (MMKVStytemUtils.class) { + if (mInstance == null) { + mInstance = new MMKVStytemUtils(); + } + } + } + return mInstance; + } + + + /** + * 保存当前的密文密钥 + */ + public void saveAesKey(String key){ + mKv.encode(STYTEM_AES_KEY,key); + } + + /** + * 获取当前的密文密钥 + */ + public String getAesKey(){ + return mKv.decodeString(STYTEM_AES_KEY,"1234567890123456"); + } + + /** + * 开启危机时刻 + */ + public void openAlarmTime(){ + mKv.encode(STYTEM_ALARM_TIME, TimeUtils.getNowMills()); + } + + /** + * 关闭危机时刻,(时间清零) + */ + public void closeAlarmTime(){ + mKv.encode(STYTEM_ALARM_TIME,0); + } + + /** + * 获取是否处于危急时刻 + */ + public boolean isAlarmTime(){ + long diff = TimeUtils.getNowMills() - mKv.decodeLong(STYTEM_ALARM_TIME,0); + if (diff <= TimeUtils.TimeConstants.DAY){ + return true; + } + return false; + } + + + +} diff --git a/app/src/main/java/com/example/alcoholic/utils/MMKVUserUtils.java b/app/src/main/java/com/example/alcoholic/utils/MMKVUserUtils.java new file mode 100644 index 0000000..4720067 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/MMKVUserUtils.java @@ -0,0 +1,169 @@ +package com.example.alcoholic.utils; + +import com.tencent.mmkv.MMKV; + +/** + * Created by + * Description: + * on 2020/11/16. + */ +public class MMKVUserUtils { + + private static MMKVUserUtils mInstance; + private static MMKV mUserKv; + + /** + * 信息 TAG ,可以理解为用户信息的版本号 Alcoholic+时间戳 + * 账户 + * 密码 + * 昵称 + * 头像 + * 签名 + */ + private static final String USER_TAG = "userTag"; + private static final String USER_ACCOUNT = "userAccount"; + private static final String USER_PASSWORD = "userPassword"; + private static final String USER_NAME = "userName"; + private static final String USER_HEAD = "userHead"; + private static final String USER_INTRO = "userIntro"; + + public MMKVUserUtils() { + mUserKv = MMKV.mmkvWithID("userInfo"); + } + + /** + * 单例 + */ + public static MMKVUserUtils getInstance(){ + if (mInstance == null) { + synchronized (MMKVUserUtils.class) { + if (mInstance == null) { + mInstance = new MMKVUserUtils(); + } + } + } + return mInstance; + } + + + /** + * 刷新信息 TAG + */ + public void reUserInfoTag(){ + mUserKv.encode(USER_TAG, createTag()); + } + + /** + * 生成一个TAG + */ + public String createTag(){ + return "Alcoholic"+ TimeUtils.getNowMills(); + } + + /** + * 获取信息 TAG + */ + public String getUserInfoTag(){ + return mUserKv.decodeString(USER_TAG,""); + } + + + /** + * 判断是否登录 + */ + public boolean isLogin(){ + if (EmptyUtils.isEmpty(getUserAccount())){ + return false; + } + + if (EmptyUtils.isEmpty(getUserPsd())){ + return false; + } + + return true; + } + + /** + * 保存账户信息 + */ + public void saveUserAccount(String userAccount){ + mUserKv.encode(USER_ACCOUNT,userAccount); + reUserInfoTag(); + } + + /** + * 读取用户账户 + */ + public String getUserAccount(){ + return mUserKv.decodeString(USER_ACCOUNT,""); + } + + /** + * 保存用户密码 + */ + public void saveUserPsd(String password){ + mUserKv.encode(USER_PASSWORD,password); + reUserInfoTag(); + } + + /** + * 读取用户密码 + */ + public String getUserPsd(){ + return mUserKv.decodeString(USER_PASSWORD,""); + } + + /** + * 保存用户昵称 + */ + public void saveUserName(String name){ + mUserKv.encode(USER_NAME,name); + reUserInfoTag(); + } + + /** + * 读取用户昵称 + */ + public String getUserName(){ + return mUserKv.decodeString(USER_NAME,""); + } + + /** + * 保存用户头像 + */ + public void saveUserHead(String head){ + mUserKv.encode(USER_HEAD,head); + reUserInfoTag(); + } + + /** + * 读取用户头像 + */ + public String getUserHead(){ + return mUserKv.decodeString(USER_HEAD,""); + } + + /** + * 保存用户简介 + */ + public void saveUserIntro(String intro){ + mUserKv.encode(USER_INTRO,intro); + reUserInfoTag(); + } + + /** + * 读取用户简介 + */ + public String getUserIntro(){ + return mUserKv.decodeString(USER_INTRO,""); + } + + /** + * 清除所有数据 + */ + public void clearAll(){ + mUserKv.clearAll(); + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/utils/SizeUtils.java b/app/src/main/java/com/example/alcoholic/utils/SizeUtils.java new file mode 100644 index 0000000..6f82ff9 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/SizeUtils.java @@ -0,0 +1,164 @@ +package com.example.alcoholic.utils; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; + +/** + * Created by + * Description: 尺寸工具类 + * on 2020/11/13. + */ +public class SizeUtils { + + private SizeUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * dp转px + * + * @param context 上下文 + * @param dpValue dp值 + * @return px值 + */ + public static int dp2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * px转dp + * + * @param context 上下文 + * @param pxValue px值 + * @return dp值 + */ + public static int px2dp(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * sp转px + * + * @param context 上下文 + * @param spValue sp值 + * @return px值 + */ + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + /** + * px转sp + * + * @param context 上下文 + * @param pxValue px值 + * @return sp值 + */ + public static int px2sp(Context context, float pxValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 各种单位转换 + *

该方法存在于TypedValue

+ * + * @param unit 单位 + * @param value 值 + * @param metrics DisplayMetrics + * @return 转换结果 + */ + public static float applyDimension(int unit, float value, DisplayMetrics metrics) { + switch (unit) { + case TypedValue.COMPLEX_UNIT_PX: + return value; + case TypedValue.COMPLEX_UNIT_DIP: + return value * metrics.density; + case TypedValue.COMPLEX_UNIT_SP: + return value * metrics.scaledDensity; + case TypedValue.COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0f / 72); + case TypedValue.COMPLEX_UNIT_IN: + return value * metrics.xdpi; + case TypedValue.COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0f / 25.4f); + } + return 0; + } + + /** + * 在onCreate()即可强行获取View的尺寸 + *

需回调onGetSizeListener接口,在onGetSize中获取view宽高

+ *

用法示例如下所示

+ *
+     * SizeUtils.forceGetViewSize(view, new SizeUtils.onGetSizeListener() {
+     *     Override
+     *     public void onGetSize(View view) {
+     *         view.getWidth();
+     *     }
+     * });
+     * 
+ * + * @param view 视图 + * @param listener 监听器 + */ + public static void forceGetViewSize(final View view, final onGetSizeListener listener) { + view.post(new Runnable() { + @Override + public void run() { + if (listener != null) { + listener.onGetSize(view); + } + } + }); + } + + /** + * 获取到View尺寸的监听 + */ + public interface onGetSizeListener { + void onGetSize(View view); + } + + public static void setListener(onGetSizeListener listener) { + mListener = listener; + } + + private static onGetSizeListener mListener; + + /** + * ListView中提前测量View尺寸,如headerView + *

用的时候去掉注释拷贝到ListView中即可

+ *

参照以下注释代码

+ * + * @param view 视图 + */ + public static void measureViewInLV(View view) { + Log.d("tips", "U should copy the following code."); + /* + ViewGroup.LayoutParams p = view.getLayoutParams(); + if (p == null) { + p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + int width = ViewGroup.getChildMeasureSpec(0, 0, p.width); + int height; + int tempHeight = p.height; + if (tempHeight > 0) { + height = MeasureSpec.makeMeasureSpec(tempHeight, + MeasureSpec.EXACTLY); + } else { + height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + view.measure(width, height); + */ + } + + +} diff --git a/app/src/main/java/com/example/alcoholic/utils/TimeUtils.java b/app/src/main/java/com/example/alcoholic/utils/TimeUtils.java new file mode 100644 index 0000000..28605ba --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/utils/TimeUtils.java @@ -0,0 +1,1530 @@ +package com.example.alcoholic.utils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +/** + * Created by + * Description: 时间工具类 + * on 2020/11/16. + */ +public class TimeUtils { + + /** + * timeParse : 从时间(毫秒)中提取出时间(时:分) + * millis2String : 将时间戳转为时间字符串 + * string2Millis : 将时间字符串转为时间戳 + * string2Date : 将时间字符串转为 Date 类型 + * date2String : 将 Date 类型转为时间字符串 + * date2Millis : 将 Date 类型转为时间戳 + * millis2Date : 将时间戳转为 Date 类型 + * getTimeSpan : 获取两个时间差(单位:unit) + * getFitTimeSpan : 获取合适型两个时间差 + * getNowMills : 获取当前毫秒时间戳 + * getNowString : 获取当前时间字符串 + * getNowDate : 获取当前 Date + * getTimeSpanByNow : 获取与当前时间的差(单位:unit) + * getFitTimeSpanByNow : 获取合适型与当前时间的差 + * getFriendlyTimeSpanByNow: 获取友好型与当前时间的差 + * getMillis : 获取与给定时间等于时间差的时间戳 + * getString : 获取与给定时间等于时间差的时间字符串 + * getDate : 获取与给定时间等于时间差的 Date + * getMillisByNow : 获取与当前时间等于时间差的时间戳 + * getStringByNow : 获取与当前时间等于时间差的时间字符串 + * getDateByNow : 获取与当前时间等于时间差的 Date + * isToday : 判断是否今天 + * isLeapYear : 判断是否闰年 + * getChineseWeek : 获取中式星期 + * getUSWeek : 获取美式式星期 + * getChineseZodiac : 获取生肖 + * getZodiac : 获取星座 + */ + + private static final ThreadLocal SDF_THREAD_LOCAL = new ThreadLocal<>(); + private static final ThreadLocal SDF_THREAD_LOCAL_DAY = new ThreadLocal<>(); + + //获取日期的时间格式 + private static SimpleDateFormat getDayFormat() { + SimpleDateFormat simpleDayDateFormat = SDF_THREAD_LOCAL_DAY.get(); + if (simpleDayDateFormat == null) { + simpleDayDateFormat = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault()); + SDF_THREAD_LOCAL_DAY.set(simpleDayDateFormat); + } + return simpleDayDateFormat; + } + + private static SimpleDateFormat getDefaultFormat() { + SimpleDateFormat simpleDateFormat = SDF_THREAD_LOCAL.get(); + if (simpleDateFormat == null) { + simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm", Locale.getDefault()); + SDF_THREAD_LOCAL.set(simpleDateFormat); + } + return simpleDateFormat; + } + + private TimeUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 从时间(毫秒)中提取出时间(时:分) + * 时间格式: 时:分 + */ + public static String timeParse(Long duration) { + String time = "" ; + long minute = duration / 60000 ; + long seconds = duration % 60000 ; + long second = Math.round((float)seconds/1000) ; + if( minute < 10 ){ + time += "0" ; + } + time += minute+":" ; + if( second < 10 ){ + time += "0" ; + } + time += second ; + return time ; + } + + /** + * Milliseconds to the formatted time string. + * 将时间戳转为时间字符串 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param millis The milliseconds. + * @return the formatted time string + */ + public static String millis2String(final long millis) { + return millis2String(millis, getDefaultFormat()); + } + + /** + * Milliseconds to the formatted time string. + * 将时间戳转为时间字符串 + * @param millis The milliseconds. + * @param format The format. + * @return the formatted time string + */ + public static String millis2String(final long millis, @NonNull final DateFormat format) { + return format.format(new Date(millis)); + } + + /** + * Formatted time string to the milliseconds. + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * 将时间字符串转为时间戳 + * @param time The formatted time string. + * @return the milliseconds + */ + public static long string2Millis(final String time) { + return string2Millis(time, getDefaultFormat()); + } + + /** + * Formatted time string to the milliseconds. + * 将时间字符串转为时间戳 + * @param time The formatted time string. + * @param format The format. + * @return the milliseconds + */ + public static long string2Millis(final String time, @NonNull final DateFormat format) { + try { + return format.parse(time).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + return -1; + } + + /** + * Formatted time string to the date. + * 将时间字符串转为 Date 类型 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return the date + */ + public static Date string2Date(final String time) { + return string2Date(time, getDefaultFormat()); + } + + /** + * Formatted time string to the date. + * 将时间字符串转为 Date 类型 + * @param time The formatted time string. + * @param format The format. + * @return the date + */ + public static Date string2Date(final String time, @NonNull final DateFormat format) { + try { + return format.parse(time); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Date to the formatted time string. + * 将 Date 类型转为时间字符串 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param date The date. + * @return the formatted time string + */ + public static String date2String(final Date date) { + return date2String(date, getDefaultFormat()); + } + + /** + * Date to the formatted time string. + * 将 Date 类型转为时间字符串 + * @param date The date. + * @param format The format. + * @return the formatted time string + */ + public static String date2String(final Date date, @NonNull final DateFormat format) { + return format.format(date); + } + + /** + * Date to the milliseconds. + * 将 Date 类型转为时间戳 + * @param date The date. + * @return the milliseconds + */ + public static long date2Millis(final Date date) { + return date.getTime(); + } + + /** + * Milliseconds to the date. + * 将时间戳转为 Date 类型 + * @param millis The milliseconds. + * @return the date + */ + public static Date millis2Date(final long millis) { + return new Date(millis); + } + + /** + * Return the time span, in unit. + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * 获取两个时间差(单位:unit) + * @param time1 The first formatted time string. + * @param time2 The second formatted time string. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span, in unit + */ + public static long getTimeSpan(final String time1, + final String time2, + @TimeConstants.Unit final int unit) { + return getTimeSpan(time1, time2, getDefaultFormat(), unit); + } + + /** + * Return the time span, in unit. + * 获取两个时间差(单位:unit) + * @param time1 The first formatted time string. + * @param time2 The second formatted time string. + * @param format The format. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span, in unit + */ + public static long getTimeSpan(final String time1, + final String time2, + @NonNull final DateFormat format, + @TimeConstants.Unit final int unit) { + return millis2TimeSpan(string2Millis(time1, format) - string2Millis(time2, format), unit); + } + + /** + * Return the time span, in unit. + * 获取两个时间差(单位:unit) + * @param date1 The first date. + * @param date2 The second date. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span, in unit + */ + public static long getTimeSpan(final Date date1, + final Date date2, + @TimeConstants.Unit final int unit) { + return millis2TimeSpan(date2Millis(date1) - date2Millis(date2), unit); + } + + /** + * Return the time span, in unit. + * 获取两个时间差(单位:unit) + * @param millis1 The first milliseconds. + * @param millis2 The second milliseconds. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span, in unit + */ + public static long getTimeSpan(final long millis1, + final long millis2, + @TimeConstants.Unit final int unit) { + return millis2TimeSpan(millis1 - millis2, unit); + } + + /** + * Return the fit time span. + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * 获取合适型两个时间差 + * @param time1 The first formatted time string. + * @param time2 The second formatted time string. + * @param precision The precision of time span. + *
    + *
  • precision = 0, return null
  • + *
  • precision = 1, return 天
  • + *
  • precision = 2, return 天, 小时
  • + *
  • precision = 3, return 天, 小时, 分钟
  • + *
  • precision = 4, return 天, 小时, 分钟, 秒
  • + *
  • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
  • + *
+ * @return the fit time span + */ + public static String getFitTimeSpan(final String time1, + final String time2, + final int precision) { + long delta = string2Millis(time1, getDefaultFormat()) - string2Millis(time2, getDefaultFormat()); + return millis2FitTimeSpan(delta, precision); + } + + /** + * Return the fit time span. + * 获取合适型两个时间差 + * @param time1 The first formatted time string. + * @param time2 The second formatted time string. + * @param format The format. + * @param precision The precision of time span. + *
    + *
  • precision = 0, return null
  • + *
  • precision = 1, return 天
  • + *
  • precision = 2, return 天, 小时
  • + *
  • precision = 3, return 天, 小时, 分钟
  • + *
  • precision = 4, return 天, 小时, 分钟, 秒
  • + *
  • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
  • + *
+ * @return the fit time span + */ + public static String getFitTimeSpan(final String time1, + final String time2, + @NonNull final DateFormat format, + final int precision) { + long delta = string2Millis(time1, format) - string2Millis(time2, format); + return millis2FitTimeSpan(delta, precision); + } + + /** + * Return the fit time span. + * 获取合适型两个时间差 + * @param date1 The first date. + * @param date2 The second date. + * @param precision The precision of time span. + *
    + *
  • precision = 0, return null
  • + *
  • precision = 1, return 天
  • + *
  • precision = 2, return 天, 小时
  • + *
  • precision = 3, return 天, 小时, 分钟
  • + *
  • precision = 4, return 天, 小时, 分钟, 秒
  • + *
  • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
  • + *
+ * @return the fit time span + */ + public static String getFitTimeSpan(final Date date1, final Date date2, final int precision) { + return millis2FitTimeSpan(date2Millis(date1) - date2Millis(date2), precision); + } + + /** + * Return the fit time span. + * 获取合适型两个时间差 + * @param millis1 The first milliseconds. + * @param millis2 The second milliseconds. + * @param precision The precision of time span. + *
    + *
  • precision = 0, return null
  • + *
  • precision = 1, return 天
  • + *
  • precision = 2, return 天, 小时
  • + *
  • precision = 3, return 天, 小时, 分钟
  • + *
  • precision = 4, return 天, 小时, 分钟, 秒
  • + *
  • precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
  • + *
+ * @return the fit time span + */ + public static String getFitTimeSpan(final long millis1, + final long millis2, + final int precision) { + return millis2FitTimeSpan(millis1 - millis2, precision); + } + + /** + * Return the current time in milliseconds. + * 获取当前毫秒时间戳 + * @return the current time in milliseconds + */ + public static long getNowMills() { + return System.currentTimeMillis(); + } + + /** + * Return the current formatted time string. + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * 获取当前时间字符串 + * @return the current formatted time string + */ + public static String getNowString() { + return millis2String(System.currentTimeMillis(), getDefaultFormat()); + } + + /** + * Return the current formatted time string. + * 获取当前时间字符串 + * @param format The format. + * @return the current formatted time string + */ + public static String getNowString(@NonNull final DateFormat format) { + return millis2String(System.currentTimeMillis(), format); + } + + /** + * Return the current date. + * 获取当前 Date + * @return the current date + */ + public static Date getNowDate() { + return new Date(); + } + + /** + * Return the time span by now, in unit. + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * 获取与当前时间的差(单位:unit) + * @param time The formatted time string. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span by now, in unit + */ + public static long getTimeSpanByNow(final String time, @TimeConstants.Unit final int unit) { + return getTimeSpan(time, getNowString(), getDefaultFormat(), unit); + } + + /** + * Return the time span by now, in unit. + * 获取与当前时间的差(单位:unit) + * @param time The formatted time string. + * @param format The format. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span by now, in unit + */ + public static long getTimeSpanByNow(final String time, + @NonNull final DateFormat format, + @TimeConstants.Unit final int unit) { + return getTimeSpan(time, getNowString(format), format, unit); + } + + /** + * Return the time span by now, in unit. + * 获取与当前时间的差(单位:unit) + * @param date The date. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span by now, in unit + */ + public static long getTimeSpanByNow(final Date date, @TimeConstants.Unit final int unit) { + return getTimeSpan(date, new Date(), unit); + } + + /** + * Return the time span by now, in unit. + * 获取与当前时间的差(单位:unit) + * @param millis The milliseconds. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the time span by now, in unit + */ + public static long getTimeSpanByNow(final long millis, @TimeConstants.Unit final int unit) { + return getTimeSpan(millis, System.currentTimeMillis(), unit); + } + + /** + * Return the fit time span by now. + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * 获取合适型与当前时间的差 + * @param time The formatted time string. + * @param precision The precision of time span. + *
    + *
  • precision = 0,返回 null
  • + *
  • precision = 1,返回天
  • + *
  • precision = 2,返回天和小时
  • + *
  • precision = 3,返回天、小时和分钟
  • + *
  • precision = 4,返回天、小时、分钟和秒
  • + *
  • precision >= 5,返回天、小时、分钟、秒和毫秒
  • + *
+ * @return the fit time span by now + */ + public static String getFitTimeSpanByNow(final String time, final int precision) { + return getFitTimeSpan(time, getNowString(), getDefaultFormat(), precision); + } + + /** + * Return the fit time span by now. + * 获取合适型与当前时间的差 + * @param time The formatted time string. + * @param format The format. + * @param precision The precision of time span. + *
    + *
  • precision = 0,返回 null
  • + *
  • precision = 1,返回天
  • + *
  • precision = 2,返回天和小时
  • + *
  • precision = 3,返回天、小时和分钟
  • + *
  • precision = 4,返回天、小时、分钟和秒
  • + *
  • precision >= 5,返回天、小时、分钟、秒和毫秒
  • + *
+ * @return the fit time span by now + */ + public static String getFitTimeSpanByNow(final String time, + @NonNull final DateFormat format, + final int precision) { + return getFitTimeSpan(time, getNowString(format), format, precision); + } + + /** + * Return the fit time span by now. + * 获取合适型与当前时间的差 + * @param date The date. + * @param precision The precision of time span. + *
    + *
  • precision = 0,返回 null
  • + *
  • precision = 1,返回天
  • + *
  • precision = 2,返回天和小时
  • + *
  • precision = 3,返回天、小时和分钟
  • + *
  • precision = 4,返回天、小时、分钟和秒
  • + *
  • precision >= 5,返回天、小时、分钟、秒和毫秒
  • + *
+ * @return the fit time span by now + */ + public static String getFitTimeSpanByNow(final Date date, final int precision) { + return getFitTimeSpan(date, getNowDate(), precision); + } + + /** + * Return the fit time span by now. + * 获取合适型与当前时间的差 + * @param millis The milliseconds. + * @param precision The precision of time span. + *
    + *
  • precision = 0,返回 null
  • + *
  • precision = 1,返回天
  • + *
  • precision = 2,返回天和小时
  • + *
  • precision = 3,返回天、小时和分钟
  • + *
  • precision = 4,返回天、小时、分钟和秒
  • + *
  • precision >= 5,返回天、小时、分钟、秒和毫秒
  • + *
+ * @return the fit time span by now + */ + public static String getFitTimeSpanByNow(final long millis, final int precision) { + return getFitTimeSpan(millis, System.currentTimeMillis(), precision); + } + + /** + * Return the friendly time span by now. + * 获取友好型与当前时间的差 + * + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return the friendly time span by now + *
    + *
  • 如果小于 1 秒钟内,显示刚刚
  • + *
  • 如果在 1 分钟内,显示 XXX秒前
  • + *
  • 如果在 1 小时内,显示 XXX分钟前
  • + *
  • 如果在 1 小时外的今天内,显示今天15:32
  • + *
  • 如果是昨天的,显示昨天15:32
  • + *
  • 其余显示,2016-10-15
  • + *
  • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
  • + *
+ */ + public static String getFriendlyTimeSpanByNow(final String time) { + return getFriendlyTimeSpanByNow(time, getDefaultFormat()); + } + + /** + * Return the friendly time span by now. + * 获取友好型与当前时间的差 + * @param time The formatted time string. + * @param format The format. + * @return the friendly time span by now + *
    + *
  • 如果小于 1 秒钟内,显示刚刚
  • + *
  • 如果在 1 分钟内,显示 XXX秒前
  • + *
  • 如果在 1 小时内,显示 XXX分钟前
  • + *
  • 如果在 1 小时外的今天内,显示今天15:32
  • + *
  • 如果是昨天的,显示昨天15:32
  • + *
  • 其余显示,2016-10-15
  • + *
  • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
  • + *
+ */ + public static String getFriendlyTimeSpanByNow(final String time, + @NonNull final DateFormat format) { + return getFriendlyTimeSpanByNow(string2Millis(time, format)); + } + + /** + * Return the friendly time span by now. + * 获取友好型与当前时间的差 + * @param date The date. + * @return the friendly time span by now + *
    + *
  • 如果小于 1 秒钟内,显示刚刚
  • + *
  • 如果在 1 分钟内,显示 XXX秒前
  • + *
  • 如果在 1 小时内,显示 XXX分钟前
  • + *
  • 如果在 1 小时外的今天内,显示今天15:32
  • + *
  • 如果是昨天的,显示昨天15:32
  • + *
  • 其余显示,2016-10-15
  • + *
  • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
  • + *
+ */ + public static String getFriendlyTimeSpanByNow(final Date date) { + return getFriendlyTimeSpanByNow(date.getTime()); + } + + /** + * Return the friendly time span by now. + * 获取友好型与当前时间的差 + * @param millis The milliseconds. + * @return the friendly time span by now + *
    + *
  • 如果小于 1 秒钟内,显示刚刚
  • + *
  • 如果在 1 分钟内,显示 XXX秒前
  • + *
  • 如果在 1 小时内,显示 XXX分钟前
  • + *
  • 如果在 1 小时外的今天内,显示今天15:32
  • + *
  • 如果是昨天的,显示昨天15:32
  • + *
  • 其余显示,2016-10-15
  • + *
  • 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
  • + *
+ */ + public static String getFriendlyTimeSpanByNow(final long millis) { + long now = System.currentTimeMillis(); + long span = now - millis; + if (span < 0) + // U can read http://www.apihome.cn/api/java/Formatter.html to understand it. + return String.format("%tc", millis); + if (span < 1000) { + return "刚刚"; + } else if (span < TimeConstants.MIN) { +// return String.format(Locale.getDefault(), "%d秒前", span / TimeConstants.SEC); + return "刚刚"; + } else if (span < TimeConstants.HOUR) { + return String.format(Locale.getDefault(), "%d分钟前", span / TimeConstants.MIN); + } + // 获取当天 00:00 + long wee = getWeeOfToday(); + if (millis >= wee) { + return String.format("今天%tR", millis); + } else if (millis >= wee - TimeConstants.DAY) { + return String.format("昨天%tR", millis); + } else { +// return millis2String(millis); + return millis2String(millis,getDayFormat()); +// return String.format("%tc%n", millis); + } + } + + private static long getWeeOfToday() { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTimeInMillis(); + } + + /** + * Return the milliseconds differ time span. + * 获取与给定时间等于时间差的时间戳 + * @param millis The milliseconds. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the milliseconds differ time span + */ + public static long getMillis(final long millis, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return millis + timeSpan2Millis(timeSpan, unit); + } + + /** + * Return the milliseconds differ time span. + * 获取与给定时间等于时间差的时间戳 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the milliseconds differ time span + */ + public static long getMillis(final String time, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return getMillis(time, getDefaultFormat(), timeSpan, unit); + } + + /** + * Return the milliseconds differ time span. + * 获取与给定时间等于时间差的时间戳 + * @param time The formatted time string. + * @param format The format. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the milliseconds differ time span. + */ + public static long getMillis(final String time, + @NonNull final DateFormat format, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return string2Millis(time, format) + timeSpan2Millis(timeSpan, unit); + } + + /** + * Return the milliseconds differ time span. + * 获取与给定时间等于时间差的时间戳 + * @param date The date. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the milliseconds differ time span. + */ + public static long getMillis(final Date date, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return date2Millis(date) + timeSpan2Millis(timeSpan, unit); + } + + /** + * Return the formatted time string differ time span. + * 获取与给定时间等于时间差的时间字符串 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param millis The milliseconds. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span + */ + public static String getString(final long millis, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return getString(millis, getDefaultFormat(), timeSpan, unit); + } + + /** + * Return the formatted time string differ time span. + * 获取与给定时间等于时间差的时间字符串 + * @param millis The milliseconds. + * @param format The format. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span + */ + public static String getString(final long millis, + @NonNull final DateFormat format, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return millis2String(millis + timeSpan2Millis(timeSpan, unit), format); + } + + /** + * Return the formatted time string differ time span. + * 获取与给定时间等于时间差的时间字符串 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span + */ + public static String getString(final String time, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return getString(time, getDefaultFormat(), timeSpan, unit); + } + + /** + * Return the formatted time string differ time span. + * 获取与给定时间等于时间差的时间字符串 + * @param time The formatted time string. + * @param format The format. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span + */ + public static String getString(final String time, + @NonNull final DateFormat format, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return millis2String(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit), format); + } + + /** + * Return the formatted time string differ time span. + * 获取与给定时间等于时间差的时间字符串 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param date The date. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span + */ + public static String getString(final Date date, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return getString(date, getDefaultFormat(), timeSpan, unit); + } + + /** + * Return the formatted time string differ time span. + * 获取与给定时间等于时间差的时间字符串 + * @param date The date. + * @param format The format. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span + */ + public static String getString(final Date date, + @NonNull final DateFormat format, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return millis2String(date2Millis(date) + timeSpan2Millis(timeSpan, unit), format); + } + + /** + * Return the date differ time span. + * 获取与给定时间等于时间差的 Date + * @param millis The milliseconds. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the date differ time span + */ + public static Date getDate(final long millis, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return millis2Date(millis + timeSpan2Millis(timeSpan, unit)); + } + + /** + * Return the date differ time span. + * 获取与给定时间等于时间差的 Date + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the date differ time span + */ + public static Date getDate(final String time, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return getDate(time, getDefaultFormat(), timeSpan, unit); + } + + /** + * Return the date differ time span. + * 获取与给定时间等于时间差的 Date + * @param time The formatted time string. + * @param format The format. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the date differ time span + */ + public static Date getDate(final String time, + @NonNull final DateFormat format, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return millis2Date(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit)); + } + + /** + * Return the date differ time span. + * 获取与给定时间等于时间差的 Date + * @param date The date. + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the date differ time span + */ + public static Date getDate(final Date date, + final long timeSpan, + @TimeConstants.Unit final int unit) { + return millis2Date(date2Millis(date) + timeSpan2Millis(timeSpan, unit)); + } + + /** + * Return the milliseconds differ time span by now. + * 获取与当前时间等于时间差的时间戳 + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the milliseconds differ time span by now + */ + public static long getMillisByNow(final long timeSpan, @TimeConstants.Unit final int unit) { + return getMillis(getNowMills(), timeSpan, unit); + } + + /** + * Return the formatted time string differ time span by now. + * 获取与当前时间等于时间差的时间字符串 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span by now + */ + public static String getStringByNow(final long timeSpan, @TimeConstants.Unit final int unit) { + return getStringByNow(timeSpan, getDefaultFormat(), unit); + } + + /** + * Return the formatted time string differ time span by now. + * 获取与当前时间等于时间差的时间字符串 + * @param timeSpan The time span. + * @param format The format. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the formatted time string differ time span by now + */ + public static String getStringByNow(final long timeSpan, + @NonNull final DateFormat format, + @TimeConstants.Unit final int unit) { + return getString(getNowMills(), format, timeSpan, unit); + } + + /** + * Return the date differ time span by now. + * 获取与当前时间等于时间差的 Date + * @param timeSpan The time span. + * @param unit The unit of time span. + *
    + *
  • {@link TimeConstants#MSEC}
  • + *
  • {@link TimeConstants#SEC }
  • + *
  • {@link TimeConstants#MIN }
  • + *
  • {@link TimeConstants#HOUR}
  • + *
  • {@link TimeConstants#DAY }
  • + *
+ * @return the date differ time span by now + */ + public static Date getDateByNow(final long timeSpan, @TimeConstants.Unit final int unit) { + return getDate(getNowMills(), timeSpan, unit); + } + + /** + * Return whether it is today. + * 判断是否今天 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isToday(final String time) { + return isToday(string2Millis(time, getDefaultFormat())); + } + + /** + * Return whether it is today. + * 判断是否今天 + * @param time The formatted time string. + * @param format The format. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isToday(final String time, @NonNull final DateFormat format) { + return isToday(string2Millis(time, format)); + } + + /** + * Return whether it is today. + * 判断是否今天 + * @param date The date. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isToday(final Date date) { + return isToday(date.getTime()); + } + + /** + * Return whether it is today. + * 判断是否今天 + * @param millis The milliseconds. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isToday(final long millis) { + long wee = getWeeOfToday(); + return millis >= wee && millis < wee + TimeConstants.DAY; + } + + /** + * Return whether it is leap year. + * 判断是否闰年 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isLeapYear(final String time) { + return isLeapYear(string2Date(time, getDefaultFormat())); + } + + /** + * Return whether it is leap year. + * 判断是否闰年 + * @param time The formatted time string. + * @param format The format. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isLeapYear(final String time, @NonNull final DateFormat format) { + return isLeapYear(string2Date(time, format)); + } + + /** + * Return whether it is leap year. + * 判断是否闰年 + * @param date The date. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isLeapYear(final Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + int year = cal.get(Calendar.YEAR); + return isLeapYear(year); + } + + /** + * Return whether it is leap year. + * 判断是否闰年 + * @param millis The milliseconds. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isLeapYear(final long millis) { + return isLeapYear(millis2Date(millis)); + } + + /** + * Return whether it is leap year. + * 判断是否闰年 + * @param year The year. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isLeapYear(final int year) { + return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; + } + + /** + * Return the day of week in Chinese. + * 获取中式星期 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return the day of week in Chinese + */ + public static String getChineseWeek(final String time) { + return getChineseWeek(string2Date(time, getDefaultFormat())); + } + + /** + * Return the day of week in Chinese. + * 获取中式星期 + * @param time The formatted time string. + * @param format The format. + * @return the day of week in Chinese + */ + public static String getChineseWeek(final String time, @NonNull final DateFormat format) { + return getChineseWeek(string2Date(time, format)); + } + + /** + * Return the day of week in Chinese. + * 获取中式星期 + * @param date The date. + * @return the day of week in Chinese + */ + public static String getChineseWeek(final Date date) { + return new SimpleDateFormat("E", Locale.CHINA).format(date); + } + + /** + * Return the day of week in Chinese. + * 获取中式星期 + * @param millis The milliseconds. + * @return the day of week in Chinese + */ + public static String getChineseWeek(final long millis) { + return getChineseWeek(new Date(millis)); + } + + /** + * Return the day of week in US. + * 获取美式式星期 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return the day of week in US + */ + public static String getUSWeek(final String time) { + return getUSWeek(string2Date(time, getDefaultFormat())); + } + + /** + * Return the day of week in US. + * 获取美式式星期 + * @param time The formatted time string. + * @param format The format. + * @return the day of week in US + */ + public static String getUSWeek(final String time, @NonNull final DateFormat format) { + return getUSWeek(string2Date(time, format)); + } + + /** + * Return the day of week in US. + * 获取美式式星期 + * @param date The date. + * @return the day of week in US + */ + public static String getUSWeek(final Date date) { + return new SimpleDateFormat("EEEE", Locale.US).format(date); + } + + /** + * Return the day of week in US. + * 获取美式式星期 + * @param millis The milliseconds. + * @return the day of week in US + */ + public static String getUSWeek(final long millis) { + return getUSWeek(new Date(millis)); + } + + /** + * Returns the value of the given calendar field. + * 返回给定日历字段的值。 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @param field The given calendar field. + *
    + *
  • {@link Calendar#ERA}
  • + *
  • {@link Calendar#YEAR}
  • + *
  • {@link Calendar#MONTH}
  • + *
  • ...
  • + *
  • {@link Calendar#DST_OFFSET}
  • + *
+ * @return the value of the given calendar field + */ + public static int getValueByCalendarField(final String time, final int field) { + return getValueByCalendarField(string2Date(time, getDefaultFormat()), field); + } + + /** + * Returns the value of the given calendar field. + * 返回给定日历字段的值。 + * @param time The formatted time string. + * @param format The format. + * @param field The given calendar field. + *
    + *
  • {@link Calendar#ERA}
  • + *
  • {@link Calendar#YEAR}
  • + *
  • {@link Calendar#MONTH}
  • + *
  • ...
  • + *
  • {@link Calendar#DST_OFFSET}
  • + *
+ * @return the value of the given calendar field + */ + public static int getValueByCalendarField(final String time, + @NonNull final DateFormat format, + final int field) { + return getValueByCalendarField(string2Date(time, format), field); + } + + /** + * Returns the value of the given calendar field. + * 返回给定日历字段的值。 + * @param date The date. + * @param field The given calendar field. + *
    + *
  • {@link Calendar#ERA}
  • + *
  • {@link Calendar#YEAR}
  • + *
  • {@link Calendar#MONTH}
  • + *
  • ...
  • + *
  • {@link Calendar#DST_OFFSET}
  • + *
+ * @return the value of the given calendar field + */ + public static int getValueByCalendarField(final Date date, final int field) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(field); + } + + /** + * Returns the value of the given calendar field. + * 返回给定日历字段的值。 + * @param millis The milliseconds. + * @param field The given calendar field. + *
    + *
  • {@link Calendar#ERA}
  • + *
  • {@link Calendar#YEAR}
  • + *
  • {@link Calendar#MONTH}
  • + *
  • ...
  • + *
  • {@link Calendar#DST_OFFSET}
  • + *
+ * @return the value of the given calendar field + */ + public static int getValueByCalendarField(final long millis, final int field) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(millis); + return cal.get(field); + } + + private static final String[] CHINESE_ZODIAC = + {"猴", "鸡", "狗", "猪", "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊"}; + + /** + * Return the Chinese zodiac. + * 获取生肖 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return the Chinese zodiac + */ + public static String getChineseZodiac(final String time) { + return getChineseZodiac(string2Date(time, getDefaultFormat())); + } + + /** + * Return the Chinese zodiac. + * 获取生肖 + * @param time The formatted time string. + * @param format The format. + * @return the Chinese zodiac + */ + public static String getChineseZodiac(final String time, @NonNull final DateFormat format) { + return getChineseZodiac(string2Date(time, format)); + } + + /** + * Return the Chinese zodiac. + * 获取生肖 + * @param date The date. + * @return the Chinese zodiac + */ + public static String getChineseZodiac(final Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return CHINESE_ZODIAC[cal.get(Calendar.YEAR) % 12]; + } + + /** + * Return the Chinese zodiac. + * 获取生肖 + * @param millis The milliseconds. + * @return the Chinese zodiac + */ + public static String getChineseZodiac(final long millis) { + return getChineseZodiac(millis2Date(millis)); + } + + /** + * Return the Chinese zodiac. + * 获取生肖 + * @param year The year. + * @return the Chinese zodiac + */ + public static String getChineseZodiac(final int year) { + return CHINESE_ZODIAC[year % 12]; + } + + private static final int[] ZODIAC_FLAGS = {20, 19, 21, 21, 21, 22, 23, 23, 23, 24, 23, 22}; + private static final String[] ZODIAC = { + "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", + "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "魔羯座" + }; + + /** + * Return the zodiac. + * 获取星座 + *

The pattern is {@code yyyy-MM-dd HH:mm:ss}.

+ * + * @param time The formatted time string. + * @return the zodiac + */ + public static String getZodiac(final String time) { + return getZodiac(string2Date(time, getDefaultFormat())); + } + + /** + * Return the zodiac. + * 获取星座 + * @param time The formatted time string. + * @param format The format. + * @return the zodiac + */ + public static String getZodiac(final String time, @NonNull final DateFormat format) { + return getZodiac(string2Date(time, format)); + } + + /** + * Return the zodiac. + * 获取星座 + * @param date The date. + * @return the zodiac + */ + public static String getZodiac(final Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + int month = cal.get(Calendar.MONTH) + 1; + int day = cal.get(Calendar.DAY_OF_MONTH); + return getZodiac(month, day); + } + + /** + * Return the zodiac. + * 获取星座 + * @param millis The milliseconds. + * @return the zodiac + */ + public static String getZodiac(final long millis) { + return getZodiac(millis2Date(millis)); + } + + /** + * Return the zodiac. + * 获取星座 + * @param month The month. + * @param day The day. + * @return the zodiac + */ + public static String getZodiac(final int month, final int day) { + return ZODIAC[day >= ZODIAC_FLAGS[month - 1] + ? month - 1 + : (month + 10) % 12]; + } + + private static long timeSpan2Millis(final long timeSpan, @TimeConstants.Unit final int unit) { + return timeSpan * unit; + } + + private static long millis2TimeSpan(final long millis, @TimeConstants.Unit final int unit) { + return millis / unit; + } + + private static String millis2FitTimeSpan(long millis, int precision) { + if (precision <= 0) return null; + precision = Math.min(precision, 5); + String[] units = {"天", "小时", "分钟", "秒", "毫秒"}; + if (millis == 0) return 0 + units[precision - 1]; + StringBuilder sb = new StringBuilder(); + if (millis < 0) { + sb.append("-"); + millis = -millis; + } + int[] unitLen = {86400000, 3600000, 60000, 1000, 1}; + for (int i = 0; i < precision; i++) { + if (millis >= unitLen[i]) { + long mode = millis / unitLen[i]; + millis -= mode * unitLen[i]; + sb.append(mode).append(units[i]); + } + } + return sb.toString(); + } + + + public static final class TimeConstants { + + public static final int MSEC = 1; + public static final int SEC = 1000; + public static final int MIN = 60000; + public static final int HOUR = 3600000; + public static final int DAY = 86400000; + + @IntDef({MSEC, SEC, MIN, HOUR, DAY}) + @Retention(RetentionPolicy.SOURCE) + public @interface Unit { + } + } + +} diff --git a/app/src/main/java/com/example/alcoholic/widget/HintLayout.java b/app/src/main/java/com/example/alcoholic/widget/HintLayout.java new file mode 100644 index 0000000..1b3e913 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/widget/HintLayout.java @@ -0,0 +1,155 @@ +package com.example.alcoholic.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + + +import com.example.alcoholic.R; + +import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RawRes; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/04/18 + * desc : 状态布局(网络错误,异常错误,空数据) + */ +public final class HintLayout extends FrameLayout { + + /** 提示布局 */ + private ViewGroup mMainLayout; + /** 提示图标 */ +// private LottieAnimationView mImageView; + /** 提示文本 */ + private TextView mTextView; + + public HintLayout(@NonNull Context context) { + this(context, null); + } + + public HintLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public HintLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public HintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + setClickable(true); + setFocusable(true); + setFocusableInTouchMode(true); + } + + /** + * 显示 + */ + public void show() { + + if (mMainLayout == null) { + //初始化布局 + initLayout(); + } + + if (!isShow()) { + // 显示布局 + mMainLayout.setVisibility(VISIBLE); + } + } + + /** + * 隐藏 + */ + public void hide() { + + if (mMainLayout != null && isShow()) { + //隐藏布局 + mMainLayout.setVisibility(INVISIBLE); + } + } + + /** + * 是否显示了 + */ + public boolean isShow() { + return mMainLayout != null && mMainLayout.getVisibility() == VISIBLE; + } + + /** + * 设置提示图标,请在show方法之后调用 + */ + public void setIcon(@DrawableRes int id) { + setIcon(ContextCompat.getDrawable(getContext(), id)); + } + + public void setIcon(Drawable drawable) { +// if (mImageView != null) { +// mImageView.setImageDrawable(drawable); +// } + } + + /** + * 设置提示动画 + */ + public void setAnim(@RawRes int id) { +// mImageView.setAnimation(id); +// 这里需要调用播放动画,否则会出现第一次显示动画效果正常,第二次显示动画会不动 +// mImageView.playAnimation(); + } + + /** + * 设置提示文本,请在show方法之后调用 + */ + public void setHint(@StringRes int id) { + setHint(getResources().getString(id)); + } + + public void setHint(CharSequence text) { + if (mTextView != null && text != null) { + mTextView.setText(text); + } + } + + /** + * 初始化提示的布局 + */ + private void initLayout() { + + mMainLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.layout_widget_hint, this, false); + +// mImageView = mMainLayout.findViewById(R.id.iv_hint_icon); + mTextView = mMainLayout.findViewById(R.id.layout_widgetHint_tv_text); + + if (mMainLayout.getBackground() == null) { + // 默认使用 windowBackground 作为背景 + TypedArray ta = getContext().obtainStyledAttributes(new int[]{android.R.attr.windowBackground}); + mMainLayout.setBackground(ta.getDrawable(0)); + ta.recycle(); + } + + addView(mMainLayout); + } + + @Override + public void setOnClickListener(@Nullable OnClickListener l) { + if (isShow()) { + mMainLayout.setOnClickListener(l); + } else { + super.setOnClickListener(l); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/widget/PhotoViewPager.java b/app/src/main/java/com/example/alcoholic/widget/PhotoViewPager.java new file mode 100644 index 0000000..d67d548 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/widget/PhotoViewPager.java @@ -0,0 +1,35 @@ +package com.example.alcoholic.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.viewpager.widget.ViewPager; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/05/07 + * desc : ViewPager 中使用 PhotoView 时出现 pointerIndex out of range 异常 + */ +public final class PhotoViewPager extends ViewPager { + + public PhotoViewPager(Context context) { + super(context); + } + + public PhotoViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // 当 PhotoView 和 ViewPager 组合时 ,用双指进行放大时 是没有问题的,但是用双指进行缩小的时候,程序就会崩掉 + // 并且抛出java.lang.IllegalArgumentException: pointerIndex out of range + try { + return super.onInterceptTouchEvent(ev); + } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ignored) { + return false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/widget/ProgressView.java b/app/src/main/java/com/example/alcoholic/widget/ProgressView.java new file mode 100644 index 0000000..fbe85e6 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/widget/ProgressView.java @@ -0,0 +1,702 @@ +package com.example.alcoholic.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import com.example.alcoholic.R; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * author : Todd-Davies + * github : https://github.com/Todd-Davies/ProgressWheel + * time : 2019/07/13 + * desc : 进度条控件 + */ +public final class ProgressView extends View { + + private final static int BAR_LENGTH = 16; + private final static int BAR_MAX_LENGTH = 270; + private final static long PAUSE_GROWING_TIME = 200; + + /** Sizes (with defaults in DP) */ + private int mCircleRadius = 28; + private int mBarWidth = 4; + private int mRimWidth = 4; + private boolean mFillRadius; + private double mTimeStartGrowing = 0; + private double mBarSpinCycleTime = 400; + private float mBarExtraLength = 0; + private boolean mBarGrowingFromFront = true; + private long mPausedTimeWithoutGrowing = 0; + /** Colors (with defaults) */ + private int mBarColor = 0xAA000000; + private int mRimColor = 0x00FFFFFF; + + /** Paints */ + private final Paint mBarPaint = new Paint(); + private final Paint mRimPaint = new Paint(); + + /** Rectangles */ + private RectF mCircleBounds = new RectF(); + + /** Animation The amount of degrees per second */ + private float mSpinSpeed = 230.0f; + // private float mSpinSpeed = 120.0f; + /** The last time the spinner was animated */ + private long mLastTimeAnimated = 0; + + private boolean mLinearProgress; + + private float mProgress = 0.0f; + private float mTargetProgress = 0.0f; + private boolean isSpinning = false; + + private ProgressCallback mCallback; + + private final boolean mShouldAnimate; + + public ProgressView(Context context) { + this(context, null, 0); + } + + public ProgressView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ProgressView); + mBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mBarWidth, getResources().getDisplayMetrics()); + mRimWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mRimWidth, getResources().getDisplayMetrics()); + mCircleRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mCircleRadius, getResources().getDisplayMetrics()); + mCircleRadius = (int) array.getDimension(R.styleable.ProgressView_circleRadius, mCircleRadius); + mFillRadius = array.getBoolean(R.styleable.ProgressView_fillRadius, false); + mBarWidth = (int) array.getDimension(R.styleable.ProgressView_barWidth, mBarWidth); + mRimWidth = (int) array.getDimension(R.styleable.ProgressView_rimWidth, mRimWidth); + float baseSpinSpeed = array.getFloat(R.styleable.ProgressView_spinSpeed, mSpinSpeed / 360.0f); + mSpinSpeed = baseSpinSpeed * 360; + mBarSpinCycleTime = array.getInt(R.styleable.ProgressView_barSpinCycleTime, (int) mBarSpinCycleTime); + mBarColor = array.getColor(R.styleable.ProgressView_barColor, mBarColor); + mRimColor = array.getColor(R.styleable.ProgressView_rimColor, mRimColor); + mLinearProgress = array.getBoolean(R.styleable.ProgressView_linearProgress, false); + if (array.getBoolean(R.styleable.ProgressView_progressIndeterminate, false)) { + spin(); + } + array.recycle(); + + float animationValue = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1); + mShouldAnimate = animationValue != 0; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width; + int height; + + int viewWidth = mCircleRadius + this.getPaddingLeft() + this.getPaddingRight(); + int viewHeight = mCircleRadius + this.getPaddingTop() + this.getPaddingBottom(); + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + switch (widthMode) { + case MeasureSpec.EXACTLY: + width = widthSize; + break; + case MeasureSpec.AT_MOST: + case MeasureSpec.UNSPECIFIED: + width = Math.min(viewWidth, widthSize); + break; + default: + width = viewWidth; + break; + } + + if (heightMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.EXACTLY) { + height = heightSize; + } else if (heightMode == MeasureSpec.AT_MOST) { + height = Math.min(viewHeight, heightSize); + } else { + height = viewHeight; + } + + setMeasuredDimension(width, height); + } + + /** + * Use onSizeChanged instead of onAttachedToWindow to get the dimensions of the view, + * because this method is called after measuring the dimensions of MATCH_PARENT & WRAP_CONTENT. + * Use this dimensions to setup the bounds and paints. + */ + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + + setupBounds(width, height); + setupPaints(); + invalidate(); + } + + /** + * Set the properties of the paints we're using to + * draw the progress wheel + */ + private void setupPaints() { + mBarPaint.setColor(mBarColor); + mBarPaint.setAntiAlias(true); + mBarPaint.setStyle(Style.STROKE); + mBarPaint.setStrokeWidth(mBarWidth); + + mRimPaint.setColor(mRimColor); + mRimPaint.setAntiAlias(true); + mRimPaint.setStyle(Style.STROKE); + mRimPaint.setStrokeWidth(mRimWidth); + } + + /** + * Set the bounds of the component + */ + private void setupBounds(int layoutWidth, int layoutHeight) { + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + + if (!mFillRadius) { + // Width should equal to Height, find the min value to setup the circle + int minValue = Math.min(layoutWidth - paddingLeft - paddingRight, + layoutHeight - paddingBottom - paddingTop); + + int circleDiameter = Math.min(minValue, mCircleRadius * 2 - mBarWidth * 2); + + // Calc the Offset if needed for centering the wheel in the available space + int xOffset = (layoutWidth - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeft; + int yOffset = (layoutHeight - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTop; + + mCircleBounds = new RectF(xOffset + mBarWidth, yOffset + mBarWidth, xOffset + circleDiameter - mBarWidth, + yOffset + circleDiameter - mBarWidth); + } else { + mCircleBounds = new RectF(paddingLeft + mBarWidth, paddingTop + mBarWidth, + layoutWidth - paddingRight - mBarWidth, layoutHeight - paddingBottom - mBarWidth); + } + } + + public void setCallback(ProgressCallback progressCallback) { + mCallback = progressCallback; + + if (!isSpinning) { + runCallback(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawArc(mCircleBounds, 360, 360, false, mRimPaint); + + boolean mustInvalidate = false; + + if (!mShouldAnimate) { + return; + } + + if (isSpinning) { + //Draw the spinning bar + mustInvalidate = true; + + long deltaTime = (SystemClock.uptimeMillis() - mLastTimeAnimated); + float deltaNormalized = deltaTime * mSpinSpeed / 1000.0f; + + updateBarLength(deltaTime); + + mProgress += deltaNormalized; + if (mProgress > 360) { + mProgress -= 360f; + + // A full turn has been completed + // we run the callback with -1 in case we want to + // do something, like changing the color + runCallback(-1.0f); + } + mLastTimeAnimated = SystemClock.uptimeMillis(); + + float from = mProgress - 90; + float length = BAR_LENGTH + mBarExtraLength; + + if (isInEditMode()) { + from = 0; + length = 135; + } + + canvas.drawArc(mCircleBounds, from, length, false, mBarPaint); + } else { + float oldProgress = mProgress; + + if (mProgress != mTargetProgress) { + //We smoothly increase the progress bar + mustInvalidate = true; + + float deltaTime = (float) (SystemClock.uptimeMillis() - mLastTimeAnimated) / 1000; + float deltaNormalized = deltaTime * mSpinSpeed; + + mProgress = Math.min(mProgress + deltaNormalized, mTargetProgress); + mLastTimeAnimated = SystemClock.uptimeMillis(); + } + + if (oldProgress != mProgress) { + runCallback(); + } + + float offset = 0.0f; + float progress = mProgress; + if (!mLinearProgress) { + float factor = 2.0f; + offset = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, 2.0f * factor)) * 360.0f; + progress = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, factor)) * 360.0f; + } + + if (isInEditMode()) { + progress = 360; + } + + canvas.drawArc(mCircleBounds, offset - 90, progress, false, mBarPaint); + } + + if (mustInvalidate) { + invalidate(); + } + } + + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (visibility == VISIBLE) { + mLastTimeAnimated = SystemClock.uptimeMillis(); + } + } + + private void updateBarLength(long deltaTimeInMilliSeconds) { + if (mPausedTimeWithoutGrowing >= PAUSE_GROWING_TIME) { + mTimeStartGrowing += deltaTimeInMilliSeconds; + + if (mTimeStartGrowing > mBarSpinCycleTime) { + // We completed a size change cycle + // (growing or shrinking) + mTimeStartGrowing -= mBarSpinCycleTime; + //if(mBarGrowingFromFront) { + mPausedTimeWithoutGrowing = 0; + //} + mBarGrowingFromFront = !mBarGrowingFromFront; + } + + float distance = + (float) Math.cos((mTimeStartGrowing / mBarSpinCycleTime + 1) * Math.PI) / 2 + 0.5f; + float destLength = (BAR_MAX_LENGTH - BAR_LENGTH); + + if (mBarGrowingFromFront) { + mBarExtraLength = distance * destLength; + } else { + float newLength = destLength * (1 - distance); + mProgress += (mBarExtraLength - newLength); + mBarExtraLength = newLength; + } + } else { + mPausedTimeWithoutGrowing += deltaTimeInMilliSeconds; + } + } + + /** + * Check if the wheel is currently spinning + */ + + public boolean isSpinning() { + return isSpinning; + } + + /** + * Reset the count (in increment mode) + */ + public void resetCount() { + mProgress = 0.0f; + mTargetProgress = 0.0f; + invalidate(); + } + + /** + * Turn off spin mode + */ + public void stopSpinning() { + isSpinning = false; + mProgress = 0.0f; + mTargetProgress = 0.0f; + invalidate(); + } + + /** + * Puts the view on spin mode + */ + public void spin() { + mLastTimeAnimated = SystemClock.uptimeMillis(); + isSpinning = true; + invalidate(); + } + + private void runCallback(float value) { + if (mCallback != null) { + mCallback.onProgressUpdate(value); + } + } + + private void runCallback() { + if (mCallback != null) { + float normalizedProgress = (float) Math.round(mProgress * 100 / 360.0f) / 100; + mCallback.onProgressUpdate(normalizedProgress); + } + } + + /** + * Set the progress to a specific value, + * the bar will be set instantly to that value + * + * @param progress the progress between 0 and 1 + */ + public void setInstantProgress(float progress) { + if (isSpinning) { + mProgress = 0.0f; + isSpinning = false; + } + + if (progress > 1.0f) { + progress -= 1.0f; + } else if (progress < 0) { + progress = 0; + } + + if (progress == mTargetProgress) { + return; + } + + mTargetProgress = Math.min(progress * 360.0f, 360.0f); + mProgress = mTargetProgress; + mLastTimeAnimated = SystemClock.uptimeMillis(); + invalidate(); + } + + // Great way to save a view's state http://stackoverflow.com/a/7089687/1991053 + @Override + public Parcelable onSaveInstanceState() { + WheelSavedState savedState = new WheelSavedState(super.onSaveInstanceState()); + // We save everything that can be changed at runtime + savedState.mProgress = this.mProgress; + savedState.mTargetProgress = this.mTargetProgress; + savedState.isSpinning = this.isSpinning; + savedState.spinSpeed = this.mSpinSpeed; + savedState.barWidth = this.mBarWidth; + savedState.barColor = this.mBarColor; + savedState.rimWidth = this.mRimWidth; + savedState.rimColor = this.mRimColor; + savedState.circleRadius = this.mCircleRadius; + savedState.linearProgress = this.mLinearProgress; + savedState.fillRadius = this.mFillRadius; + return savedState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof WheelSavedState)) { + super.onRestoreInstanceState(state); + return; + } + + WheelSavedState savedState = (WheelSavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + + this.mProgress = savedState.mProgress; + this.mTargetProgress = savedState.mTargetProgress; + this.isSpinning = savedState.isSpinning; + this.mSpinSpeed = savedState.spinSpeed; + this.mBarWidth = savedState.barWidth; + this.mBarColor = savedState.barColor; + this.mRimWidth = savedState.rimWidth; + this.mRimColor = savedState.rimColor; + this.mCircleRadius = savedState.circleRadius; + this.mLinearProgress = savedState.linearProgress; + this.mFillRadius = savedState.fillRadius; + + this.mLastTimeAnimated = SystemClock.uptimeMillis(); + } + + /** + * @return the current progress between 0.0 and 1.0, + * if the wheel is indeterminate, then the result is -1 + */ + public float getProgress() { + return isSpinning ? -1 : mProgress / 360.0f; + } + + //---------------------------------- + //Getters + setters + //---------------------------------- + + /** + * Set the progress to a specific value, + * the bar will smoothly animate until that value + * + * @param progress the progress between 0 and 1 + */ + public void setProgress(float progress) { + if (isSpinning) { + mProgress = 0.0f; + isSpinning = false; + + runCallback(); + } + + if (progress > 1.0f) { + progress -= 1.0f; + } else if (progress < 0) { + progress = 0; + } + + if (progress == mTargetProgress) { + return; + } + + // If we are currently in the right position + // we set again the last time animated so the + // animation starts smooth from here + if (mProgress == mTargetProgress) { + mLastTimeAnimated = SystemClock.uptimeMillis(); + } + + mTargetProgress = Math.min(progress * 360.0f, 360.0f); + + invalidate(); + } + + /** + * Sets the determinate progress mode + * + * @param isLinear if the progress should increase linearly + */ + public void setLinearProgress(boolean isLinear) { + mLinearProgress = isLinear; + if (!isSpinning) { + invalidate(); + } + } + + /** + * @return the radius of the wheel in pixels + */ + public int getCircleRadius() { + return mCircleRadius; + } + + /** + * Sets the radius of the wheel + * + * @param circleRadius the expected radius, in pixels + */ + public void setCircleRadius(int circleRadius) { + this.mCircleRadius = circleRadius; + if (!isSpinning) { + invalidate(); + } + } + + /** + * @return the width of the spinning bar + */ + public int getBarWidth() { + return mBarWidth; + } + + /** + * Sets the width of the spinning bar + * + * @param barWidth the spinning bar width in pixels + */ + public void setBarWidth(int barWidth) { + this.mBarWidth = barWidth; + if (!isSpinning) { + invalidate(); + } + } + + /** + * @return the color of the spinning bar + */ + public int getBarColor() { + return mBarColor; + } + + /** + * Sets the color of the spinning bar + * + * @param barColor The spinning bar color + */ + public void setBarColor(int barColor) { + this.mBarColor = barColor; + setupPaints(); + if (!isSpinning) { + invalidate(); + } + } + + /** + * @return the color of the wheel's contour + */ + public int getRimColor() { + return mRimColor; + } + + /** + * Sets the color of the wheel's contour + * + * @param rimColor the color for the wheel + */ + public void setRimColor(int rimColor) { + this.mRimColor = rimColor; + setupPaints(); + if (!isSpinning) { + invalidate(); + } + } + + /** + * @return the base spinning speed, in full circle turns per second + * (1.0 equals on full turn in one second), this value also is applied for + * the smoothness when setting a progress + */ + public float getSpinSpeed() { + return mSpinSpeed / 360.0f; + } + + /** + * Sets the base spinning speed, in full circle turns per second + * (1.0 equals on full turn in one second), this value also is applied for + * the smoothness when setting a progress + * + * @param spinSpeed the desired base speed in full turns per second + */ + public void setSpinSpeed(float spinSpeed) { + this.mSpinSpeed = spinSpeed * 360.0f; + } + + /** + * @return the width of the wheel's contour in pixels + */ + public int getRimWidth() { + return mRimWidth; + } + + /** + * Sets the width of the wheel's contour + * + * @param rimWidth the width in pixels + */ + public void setRimWidth(int rimWidth) { + this.mRimWidth = rimWidth; + if (!isSpinning) { + invalidate(); + } + } + + public interface ProgressCallback { + /** + * Method to call when the progress reaches a value + * in order to avoid float precision issues, the progress + * is rounded to a float with two decimals. + *

+ * In indeterminate mode, the callback is called each time + * the wheel completes an animation cycle, with, the progress value is -1.0f + * + * @param progress a double value between 0.00 and 1.00 both included + */ + void onProgressUpdate(float progress); + } + + static class WheelSavedState extends BaseSavedState { + // required field that makes Parcelables from a Parcel + public static final Creator CREATOR = + new Creator() { + @Override + public WheelSavedState createFromParcel(Parcel in) { + return new WheelSavedState(in); + } + + @Override + public WheelSavedState[] newArray(int size) { + return new WheelSavedState[size]; + } + }; + float mProgress; + float mTargetProgress; + boolean isSpinning; + float spinSpeed; + int barWidth; + int barColor; + int rimWidth; + int rimColor; + int circleRadius; + boolean linearProgress; + boolean fillRadius; + + WheelSavedState(Parcelable superState) { + super(superState); + } + + private WheelSavedState(Parcel in) { + super(in); + this.mProgress = in.readFloat(); + this.mTargetProgress = in.readFloat(); + this.isSpinning = in.readByte() != 0; + this.spinSpeed = in.readFloat(); + this.barWidth = in.readInt(); + this.barColor = in.readInt(); + this.rimWidth = in.readInt(); + this.rimColor = in.readInt(); + this.circleRadius = in.readInt(); + this.linearProgress = in.readByte() != 0; + this.fillRadius = in.readByte() != 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeFloat(this.mProgress); + out.writeFloat(this.mTargetProgress); + out.writeByte((byte) (isSpinning ? 1 : 0)); + out.writeFloat(this.spinSpeed); + out.writeInt(this.barWidth); + out.writeInt(this.barColor); + out.writeInt(this.rimWidth); + out.writeInt(this.rimColor); + out.writeInt(this.circleRadius); + out.writeByte((byte) (linearProgress ? 1 : 0)); + out.writeByte((byte) (fillRadius ? 1 : 0)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/alcoholic/widget/decoration/ChatListDecoration.java b/app/src/main/java/com/example/alcoholic/widget/decoration/ChatListDecoration.java new file mode 100644 index 0000000..4846e50 --- /dev/null +++ b/app/src/main/java/com/example/alcoholic/widget/decoration/ChatListDecoration.java @@ -0,0 +1,87 @@ +package com.example.alcoholic.widget.decoration; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; + +import com.example.alcoholic.utils.SizeUtils; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by + * Description:聊天列表分割线 + * on 2020/11/13. + */ +public class ChatListDecoration extends RecyclerView.ItemDecoration { + + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + + private Context mContext; + private Paint mPaint; + private Drawable mDivider; + + private int mDividerHeight = 1;//分割线高度,默认为1px + + public ChatListDecoration(Context context) { + this.mContext = context; + + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(0XFFCCCCCC); + mPaint.setStyle(Paint.Style.FILL); + } + + @Override + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + super.onDraw(c, parent, state); + if (parent.getLayoutManager() == null) { + return; + } + + drawHorizontal(c, parent); + } + + @Override + public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + super.onDrawOver(c, parent, state); + + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + + outRect.set(0, 0, 0, mDividerHeight); + } + + + //绘制横向 item 分割线 + private void drawHorizontal(Canvas canvas, RecyclerView parent) { + final int left = parent.getPaddingLeft()+ SizeUtils.dp2px(mContext,15+50+10); + final int right = parent.getMeasuredWidth() - parent.getPaddingRight(); + final int childSize = parent.getChildCount(); + for (int i = 0; i < childSize; i++) { + final View child = parent.getChildAt(i); + RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); + final int top = child.getBottom() + layoutParams.bottomMargin; + final int bottom = top + mDividerHeight; + if (mDivider != null) { + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + if (mPaint != null) { + canvas.drawRect(left, top, right, bottom, mPaint); + } + } + } +} diff --git a/app/src/main/res/anim/left_in_activity.xml b/app/src/main/res/anim/left_in_activity.xml new file mode 100644 index 0000000..e03efb4 --- /dev/null +++ b/app/src/main/res/anim/left_in_activity.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/left_out_activity.xml b/app/src/main/res/anim/left_out_activity.xml new file mode 100644 index 0000000..464168e --- /dev/null +++ b/app/src/main/res/anim/left_out_activity.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/right_in_activity.xml b/app/src/main/res/anim/right_in_activity.xml new file mode 100644 index 0000000..253a11f --- /dev/null +++ b/app/src/main/res/anim/right_in_activity.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/right_out_activity.xml b/app/src/main/res/anim/right_out_activity.xml new file mode 100644 index 0000000..912cc35 --- /dev/null +++ b/app/src/main/res/anim/right_out_activity.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..fde1368 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrows_bottom_ic.xml b/app/src/main/res/drawable/arrows_bottom_ic.xml new file mode 100644 index 0000000..11e2987 --- /dev/null +++ b/app/src/main/res/drawable/arrows_bottom_ic.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrows_left_ic.xml b/app/src/main/res/drawable/arrows_left_ic.xml new file mode 100644 index 0000000..d2958cb --- /dev/null +++ b/app/src/main/res/drawable/arrows_left_ic.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrows_right_ic.xml b/app/src/main/res/drawable/arrows_right_ic.xml new file mode 100644 index 0000000..d2a3f87 --- /dev/null +++ b/app/src/main/res/drawable/arrows_right_ic.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrows_top_ic.xml b/app/src/main/res/drawable/arrows_top_ic.xml new file mode 100644 index 0000000..7c4ed34 --- /dev/null +++ b/app/src/main/res/drawable/arrows_top_ic.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_btn_chat_send.xml b/app/src/main/res/drawable/bg_btn_chat_send.xml new file mode 100644 index 0000000..4c96143 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_chat_send.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_btn_exit_login.xml b/app/src/main/res/drawable/bg_btn_exit_login.xml new file mode 100644 index 0000000..8e48480 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_exit_login.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_et_chat_input.xml b/app/src/main/res/drawable/bg_et_chat_input.xml new file mode 100644 index 0000000..a181f8a --- /dev/null +++ b/app/src/main/res/drawable/bg_et_chat_input.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_left_grey.xml b/app/src/main/res/drawable/bg_left_grey.xml new file mode 100644 index 0000000..97a86b2 --- /dev/null +++ b/app/src/main/res/drawable/bg_left_grey.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/camera_ic.xml b/app/src/main/res/drawable/camera_ic.xml new file mode 100644 index 0000000..e9615ee --- /dev/null +++ b/app/src/main/res/drawable/camera_ic.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/checkbox_checked_ic.xml b/app/src/main/res/drawable/checkbox_checked_ic.xml new file mode 100644 index 0000000..d8ce448 --- /dev/null +++ b/app/src/main/res/drawable/checkbox_checked_ic.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/checkbox_disable_ic.xml b/app/src/main/res/drawable/checkbox_disable_ic.xml new file mode 100644 index 0000000..a73bee9 --- /dev/null +++ b/app/src/main/res/drawable/checkbox_disable_ic.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/checkbox_selector.xml b/app/src/main/res/drawable/checkbox_selector.xml new file mode 100644 index 0000000..0d9bdc7 --- /dev/null +++ b/app/src/main/res/drawable/checkbox_selector.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/compound_normal_ic.xml b/app/src/main/res/drawable/compound_normal_ic.xml new file mode 100644 index 0000000..f9789e3 --- /dev/null +++ b/app/src/main/res/drawable/compound_normal_ic.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_rounded_corner_bg.xml b/app/src/main/res/drawable/dialog_rounded_corner_bg.xml new file mode 100644 index 0000000..820f8c2 --- /dev/null +++ b/app/src/main/res/drawable/dialog_rounded_corner_bg.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/hint_empty_ic.xml b/app/src/main/res/drawable/hint_empty_ic.xml new file mode 100644 index 0000000..18ae238 --- /dev/null +++ b/app/src/main/res/drawable/hint_empty_ic.xml @@ -0,0 +1,35 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/hint_error_ic.xml b/app/src/main/res/drawable/hint_error_ic.xml new file mode 100644 index 0000000..4c44126 --- /dev/null +++ b/app/src/main/res/drawable/hint_error_ic.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/hint_nerwork_ic.xml b/app/src/main/res/drawable/hint_nerwork_ic.xml new file mode 100644 index 0000000..472af06 --- /dev/null +++ b/app/src/main/res/drawable/hint_nerwork_ic.xml @@ -0,0 +1,147 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_found_off_ic.xml b/app/src/main/res/drawable/home_found_off_ic.xml new file mode 100644 index 0000000..26557c2 --- /dev/null +++ b/app/src/main/res/drawable/home_found_off_ic.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_found_on_ic.xml b/app/src/main/res/drawable/home_found_on_ic.xml new file mode 100644 index 0000000..8bd0fa2 --- /dev/null +++ b/app/src/main/res/drawable/home_found_on_ic.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_found_selector.xml b/app/src/main/res/drawable/home_found_selector.xml new file mode 100644 index 0000000..ed38d27 --- /dev/null +++ b/app/src/main/res/drawable/home_found_selector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_home_off_ic.xml b/app/src/main/res/drawable/home_home_off_ic.xml new file mode 100644 index 0000000..fac197b --- /dev/null +++ b/app/src/main/res/drawable/home_home_off_ic.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_home_on_ic.xml b/app/src/main/res/drawable/home_home_on_ic.xml new file mode 100644 index 0000000..ce01600 --- /dev/null +++ b/app/src/main/res/drawable/home_home_on_ic.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_home_selector.xml b/app/src/main/res/drawable/home_home_selector.xml new file mode 100644 index 0000000..6674dd5 --- /dev/null +++ b/app/src/main/res/drawable/home_home_selector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_me_off_ic.xml b/app/src/main/res/drawable/home_me_off_ic.xml new file mode 100644 index 0000000..1d841a9 --- /dev/null +++ b/app/src/main/res/drawable/home_me_off_ic.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_me_on_ic.xml b/app/src/main/res/drawable/home_me_on_ic.xml new file mode 100644 index 0000000..c3d1155 --- /dev/null +++ b/app/src/main/res/drawable/home_me_on_ic.xml @@ -0,0 +1,36 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_me_selector.xml b/app/src/main/res/drawable/home_me_selector.xml new file mode 100644 index 0000000..84f81d8 --- /dev/null +++ b/app/src/main/res/drawable/home_me_selector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_message_off_ic.xml b/app/src/main/res/drawable/home_message_off_ic.xml new file mode 100644 index 0000000..b1d0b38 --- /dev/null +++ b/app/src/main/res/drawable/home_message_off_ic.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_message_on_ic.xml b/app/src/main/res/drawable/home_message_on_ic.xml new file mode 100644 index 0000000..cc46c7f --- /dev/null +++ b/app/src/main/res/drawable/home_message_on_ic.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_message_selector.xml b/app/src/main/res/drawable/home_message_selector.xml new file mode 100644 index 0000000..cbe8bd9 --- /dev/null +++ b/app/src/main/res/drawable/home_message_selector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_navigation_color_selector.xml b/app/src/main/res/drawable/home_navigation_color_selector.xml new file mode 100644 index 0000000..aa66a67 --- /dev/null +++ b/app/src/main/res/drawable/home_navigation_color_selector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_more_horiz_24.xml b/app/src/main/res/drawable/ic_baseline_more_horiz_24.xml new file mode 100644 index 0000000..e4ab25c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_more_horiz_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..1e4408c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/image_error_bg.xml b/app/src/main/res/drawable/image_error_bg.xml new file mode 100644 index 0000000..88586df --- /dev/null +++ b/app/src/main/res/drawable/image_error_bg.xml @@ -0,0 +1,40 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/image_loading_bg.xml b/app/src/main/res/drawable/image_loading_bg.xml new file mode 100644 index 0000000..9d47e5b --- /dev/null +++ b/app/src/main/res/drawable/image_loading_bg.xml @@ -0,0 +1,43 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/radiobutton_checked_ic.xml b/app/src/main/res/drawable/radiobutton_checked_ic.xml new file mode 100644 index 0000000..fd19909 --- /dev/null +++ b/app/src/main/res/drawable/radiobutton_checked_ic.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/radiobutton_disable_ic.xml b/app/src/main/res/drawable/radiobutton_disable_ic.xml new file mode 100644 index 0000000..834d84e --- /dev/null +++ b/app/src/main/res/drawable/radiobutton_disable_ic.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/radiobutton_selector.xml b/app/src/main/res/drawable/radiobutton_selector.xml new file mode 100644 index 0000000..f9a9ae3 --- /dev/null +++ b/app/src/main/res/drawable/radiobutton_selector.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/setting_update_bg.xml b/app/src/main/res/drawable/setting_update_bg.xml new file mode 100644 index 0000000..6a80771 --- /dev/null +++ b/app/src/main/res/drawable/setting_update_bg.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/succeed_ic.xml b/app/src/main/res/drawable/succeed_ic.xml new file mode 100644 index 0000000..5ef7046 --- /dev/null +++ b/app/src/main/res/drawable/succeed_ic.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/transparent_selector.xml b/app/src/main/res/drawable/transparent_selector.xml new file mode 100644 index 0000000..e10d84f --- /dev/null +++ b/app/src/main/res/drawable/transparent_selector.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml new file mode 100644 index 0000000..765137b --- /dev/null +++ b/app/src/main/res/layout/activity_chat.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_contacts.xml b/app/src/main/res/layout/activity_contacts.xml new file mode 100644 index 0000000..ca31f0e --- /dev/null +++ b/app/src/main/res/layout/activity_contacts.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_friend_add.xml b/app/src/main/res/layout/activity_friend_add.xml new file mode 100644 index 0000000..e7d4378 --- /dev/null +++ b/app/src/main/res/layout/activity_friend_add.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_group_add.xml b/app/src/main/res/layout/activity_group_add.xml new file mode 100644 index 0000000..981d472 --- /dev/null +++ b/app/src/main/res/layout/activity_group_add.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_group_add_member.xml b/app/src/main/res/layout/activity_group_add_member.xml new file mode 100644 index 0000000..8869f70 --- /dev/null +++ b/app/src/main/res/layout/activity_group_add_member.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_group_home.xml b/app/src/main/res/layout/activity_group_home.xml new file mode 100644 index 0000000..3de431b --- /dev/null +++ b/app/src/main/res/layout/activity_group_home.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_group_info.xml b/app/src/main/res/layout/activity_group_info.xml new file mode 100644 index 0000000..fafa211 --- /dev/null +++ b/app/src/main/res/layout/activity_group_info.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_group_manage.xml b/app/src/main/res/layout/activity_group_manage.xml new file mode 100644 index 0000000..e1de9a7 --- /dev/null +++ b/app/src/main/res/layout/activity_group_manage.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_help.xml b/app/src/main/res/layout/activity_help.xml new file mode 100644 index 0000000..a634788 --- /dev/null +++ b/app/src/main/res/layout/activity_help.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml new file mode 100644 index 0000000..c1585f8 --- /dev/null +++ b/app/src/main/res/layout/activity_home.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_image_preview.xml b/app/src/main/res/layout/activity_image_preview.xml new file mode 100644 index 0000000..ac40cb2 --- /dev/null +++ b/app/src/main/res/layout/activity_image_preview.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_image_select.xml b/app/src/main/res/layout/activity_image_select.xml new file mode 100644 index 0000000..9ee2519 --- /dev/null +++ b/app/src/main/res/layout/activity_image_select.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..671669f --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setting.xml b/app/src/main/res/layout/activity_setting.xml new file mode 100644 index 0000000..97d051e --- /dev/null +++ b/app/src/main/res/layout/activity_setting.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000..23699d4 --- /dev/null +++ b/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_user_home.xml b/app/src/main/res/layout/activity_user_home.xml new file mode 100644 index 0000000..4059aba --- /dev/null +++ b/app/src/main/res/layout/activity_user_home.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_user_info_setting.xml b/app/src/main/res/layout/activity_user_info_setting.xml new file mode 100644 index 0000000..be1a5bd --- /dev/null +++ b/app/src/main/res/layout/activity_user_info_setting.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_user_login.xml b/app/src/main/res/layout/activity_user_login.xml new file mode 100644 index 0000000..31e53cd --- /dev/null +++ b/app/src/main/res/layout/activity_user_login.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_user_register.xml b/app/src/main/res/layout/activity_user_register.xml new file mode 100644 index 0000000..0ab2160 --- /dev/null +++ b/app/src/main/res/layout/activity_user_register.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_album.xml b/app/src/main/res/layout/dialog_album.xml new file mode 100644 index 0000000..bf9fcc8 --- /dev/null +++ b/app/src/main/res/layout/dialog_album.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_contacts_sync.xml b/app/src/main/res/layout/dialog_contacts_sync.xml new file mode 100644 index 0000000..8749a78 --- /dev/null +++ b/app/src/main/res/layout/dialog_contacts_sync.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_input.xml b/app/src/main/res/layout/dialog_input.xml new file mode 100644 index 0000000..1e20989 --- /dev/null +++ b/app/src/main/res/layout/dialog_input.xml @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_message.xml b/app/src/main/res/layout/dialog_message.xml new file mode 100644 index 0000000..5091b35 --- /dev/null +++ b/app/src/main/res/layout/dialog_message.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_ui.xml b/app/src/main/res/layout/dialog_ui.xml new file mode 100644 index 0000000..1ba7ecb --- /dev/null +++ b/app/src/main/res/layout/dialog_ui.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_wait.xml b/app/src/main/res/layout/dialog_wait.xml new file mode 100644 index 0000000..44e2dfa --- /dev/null +++ b/app/src/main/res/layout/dialog_wait.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_find.xml b/app/src/main/res/layout/fragment_find.xml new file mode 100644 index 0000000..dc0afeb --- /dev/null +++ b/app/src/main/res/layout/fragment_find.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..4fb5065 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_me.xml b/app/src/main/res/layout/fragment_me.xml new file mode 100644 index 0000000..44ec8d8 --- /dev/null +++ b/app/src/main/res/layout/fragment_me.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml new file mode 100644 index 0000000..4f8399b --- /dev/null +++ b/app/src/main/res/layout/fragment_message.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_album.xml b/app/src/main/res/layout/item_album.xml new file mode 100644 index 0000000..5970bde --- /dev/null +++ b/app/src/main/res/layout/item_album.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_chat_list.xml b/app/src/main/res/layout/item_chat_list.xml new file mode 100644 index 0000000..7d3c221 --- /dev/null +++ b/app/src/main/res/layout/item_chat_list.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_contacts.xml b/app/src/main/res/layout/item_contacts.xml new file mode 100644 index 0000000..919e1b2 --- /dev/null +++ b/app/src/main/res/layout/item_contacts.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_group_members_simple.xml b/app/src/main/res/layout/item_group_members_simple.xml new file mode 100644 index 0000000..2ff6d11 --- /dev/null +++ b/app/src/main/res/layout/item_group_members_simple.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_image_select.xml b/app/src/main/res/layout/item_image_select.xml new file mode 100644 index 0000000..1c8c902 --- /dev/null +++ b/app/src/main/res/layout/item_image_select.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_msg_left.xml b/app/src/main/res/layout/item_msg_left.xml new file mode 100644 index 0000000..33aa693 --- /dev/null +++ b/app/src/main/res/layout/item_msg_left.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_msg_right.xml b/app/src/main/res/layout/item_msg_right.xml new file mode 100644 index 0000000..c09b8b3 --- /dev/null +++ b/app/src/main/res/layout/item_msg_right.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_empty.xml b/app/src/main/res/layout/layout_empty.xml new file mode 100644 index 0000000..3fc107c --- /dev/null +++ b/app/src/main/res/layout/layout_empty.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/layout/layout_widget_hint.xml b/app/src/main/res/layout/layout_widget_hint.xml new file mode 100644 index 0000000..45a1729 --- /dev/null +++ b/app/src/main/res/layout/layout_widget_hint.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/menu/menu_home_bottom_nav.xml b/app/src/main/res/menu/menu_home_bottom_nav.xml new file mode 100644 index 0000000..b062132 --- /dev/null +++ b/app/src/main/res/menu/menu_home_bottom_nav.xml @@ -0,0 +1,23 @@ + +

+ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_circle.png b/app/src/main/res/mipmap-xxhdpi/ic_add_circle.png new file mode 100644 index 0000000..a1dd20f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_circle.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_friend.png b/app/src/main/res/mipmap-xxhdpi/ic_add_friend.png new file mode 100644 index 0000000..c931186 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_friend.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_black_list.png b/app/src/main/res/mipmap-xxhdpi/ic_black_list.png new file mode 100644 index 0000000..e526bb3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_black_list.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_group.png b/app/src/main/res/mipmap-xxhdpi/ic_group.png new file mode 100644 index 0000000..0135222 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_group.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_run_sync.png b/app/src/main/res/mipmap-xxhdpi/ic_run_sync.png new file mode 100644 index 0000000..d943390 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_run_sync.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/message_text_receive.9.png b/app/src/main/res/mipmap-xxhdpi/message_text_receive.9.png new file mode 100644 index 0000000..22acb35 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/message_text_receive.9.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/message_text_send.9.png b/app/src/main/res/mipmap-xxhdpi/message_text_send.9.png new file mode 100644 index 0000000..770a820 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/message_text_send.9.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/bg_chat_0.jpg b/app/src/main/res/mipmap-xxxhdpi/bg_chat_0.jpg new file mode 100644 index 0000000..119337b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/bg_chat_0.jpg differ diff --git a/app/src/main/res/mipmap-xxxhdpi/bg_chat_1.jpg b/app/src/main/res/mipmap-xxxhdpi/bg_chat_1.jpg new file mode 100644 index 0000000..2c3f59e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/bg_chat_1.jpg differ diff --git a/app/src/main/res/mipmap-xxxhdpi/de_head_1.jpg b/app/src/main/res/mipmap-xxxhdpi/de_head_1.jpg new file mode 100644 index 0000000..6c80130 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/de_head_1.jpg differ diff --git a/app/src/main/res/mipmap-xxxhdpi/de_head_2.jpg b/app/src/main/res/mipmap-xxxhdpi/de_head_2.jpg new file mode 100644 index 0000000..941fce2 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/de_head_2.jpg differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..257270c --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..dcd8dbc --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,25 @@ + + + @color/white + @color/black + #FFCB57 + #F4F4F4 + #333333 + #757575 + + + #EEEEEE + + #CCCCCC + + + #7C7C7C + + #F13D20 + #1592E6 + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..be97cf1 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,143 @@ + + + + 1px + + + 999dp + + + 4dp + + + 4dp + + + 15dp + + + 1dp + 2dp + 3dp + 4dp + 5dp + 6dp + 7dp + 8dp + 9dp + 10dp + 11dp + 12dp + 13dp + 14dp + 15dp + 16dp + 17dp + 18dp + 19dp + 20dp + 21dp + 22dp + 23dp + 24dp + 25dp + 26dp + 27dp + 28dp + 29dp + 30dp + 31dp + 32dp + 33dp + 34dp + 35dp + 36dp + 37dp + 38dp + 39dp + 40dp + 41dp + 42dp + 43dp + 44dp + 45dp + 46dp + 47dp + 48dp + 49dp + 50dp + 51dp + 52dp + 53dp + 54dp + 55dp + 56dp + 57dp + 58dp + 59dp + 60dp + 61dp + 62dp + 63dp + 64dp + 65dp + 66dp + 67dp + 68dp + 69dp + 70dp + 71dp + 72dp + 73dp + 74dp + 75dp + 76dp + 77dp + 78dp + 79dp + 80dp + 81dp + 82dp + 83dp + 84dp + 85dp + 86dp + 87dp + 88dp + 89dp + 90dp + 91dp + 92dp + 93dp + 94dp + 95dp + 96dp + 97dp + 98dp + 99dp + 100dp + 120dp + 140dp + 160dp + 180dp + 190dp + 200dp + 300dp + 400dp + 500dp + + + + + 8sp + 10sp + 12sp + 14sp + 16sp + 18sp + 20sp + 22sp + 24sp + + + \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 0000000..5baad8d --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,8 @@ + + + + 200 + + + 4 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..44d759c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,213 @@ + + 大呲花 + + + 输入手机号码 + 手机号输入不正确 + + 请输入密码 + 两次密码输入不一致,请重新输入 + + 输入验证码 + 发送验证码 + 验证码已发送,请注意查收 + 验证码错误,请检查输入 + + 下一步 + 完成 + 提交 + + 请先授予权限 + 授权失败,请手动授予权限 + + 加载中… + + 确定 + 完成 + 取消 + + + + + + + + + + 当前没有网络连接,请检查网络设置 + + + 加载中… + + 请求出错,未知错误 + 账号异常,请重新登录 + 数据解析异常,请稍后 + 服务器请求超时,请稍后再试 + 请求失败,请检查网络设置 + 服务器响应异常,请稍后再试 + 服务器连接异常,请稍后再试 + 请求被中断,请重试 + + + 空空如也 + 请求出错,点击重试 + 网络错误,点击重试 + + + 提示 + 错误 + + + 请选择地区 + 请选择 + + + 请选择日期 + + + 请选择时间 + + + 发现新版本 + + 更新内容 + + 下次再说 + 立即更新 + + 必须先要授予权限才能正常下载更新哦 + + 正在下载 + 下载中 %d%% + 下载完成,点击安装 + 下载失败,点击重试 + + 当前已是最新版本 + + + 分享到… + + 微信 + 朋友圈 + QQ + QQ空间 + 复制链接 + + 已复制到剪贴板 + + + 请输入支付密码 + + + 至少要选择 %d 项 + 最多只能选择 %d 项 + + + 身份校验 + + + 再按一次退出 + 首页 + 发现 + 消息 + 我的 + + + 注册 + 忘记密码? + 登录 + + 其他登录方式 + + + 注册 + 手机号仅用于登录和保护账号安全 + 设置6–18位登录密码 + 请再次输入一次密码 + 注册成功 + + + 设置 + + 语言切换 + 简体中文 + 繁体中文 + 检查更新 + + 修改密码 + 修改手机 + + 自动登录 + 清空缓存 + + 隐私协议 + 关于我们 + + 退出登录 + + + 关于我们 + Android 轮子哥 + + Copyright © 2018 – 2020 + + + 忘记密码 + + + 设置登录密码 + 设置6–18位登录密码 + 重新输入一次密码 + 两次密码输入不一致,请重新输入 + 密码重置成功 + + + 设置手机号 + 下次登录请使用更换后的新手机号登录 + 立即绑定 + + 绑定成功,下次登录请使用新手机号登录 + + + 个人资料 + 头像 + + 用户ID + 昵称 + 设置昵称 + + 地区 + 请选择 + + 手机号码 + 立即绑定 + + + 网页加载中… + + + 图片选择 + 所有图片 + + 共 %d 张 + + 本次最多只能选择 %d 张图片 + + + 视频选择 + 所有视频 + + 共 %d 个 + + 本次最多只能选择 %d 个视频 + + + 无法启动相机 + 目标地址错误 + + + + 发送 + qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM1234567890 + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..a1348c6 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..dca93c0 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/com/example/alcoholic/ExampleUnitTest.java b/app/src/test/java/com/example/alcoholic/ExampleUnitTest.java new file mode 100644 index 0000000..752e158 --- /dev/null +++ b/app/src/test/java/com/example/alcoholic/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.alcoholic; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see
Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/base/.gitignore b/base/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/base/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/base/build.gradle b/base/build.gradle new file mode 100644 index 0000000..469c26d --- /dev/null +++ b/base/build.gradle @@ -0,0 +1,6 @@ +apply plugin: 'com.android.library' +apply from: '../config.gradle' + + +android { +} \ No newline at end of file diff --git a/base/consumer-rules.pro b/base/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/base/proguard-rules.pro b/base/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/base/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/base/src/androidTest/java/com/example/base/ExampleInstrumentedTest.java b/base/src/androidTest/java/com/example/base/ExampleInstrumentedTest.java new file mode 100644 index 0000000..98ec5a1 --- /dev/null +++ b/base/src/androidTest/java/com/example/base/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.base; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.base.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1ef51aa --- /dev/null +++ b/base/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/base/src/main/java/com/example/base/BaseActivity.java b/base/src/main/java/com/example/base/BaseActivity.java new file mode 100644 index 0000000..6811a53 --- /dev/null +++ b/base/src/main/java/com/example/base/BaseActivity.java @@ -0,0 +1,181 @@ +package com.example.base; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.inputmethod.InputMethodManager; + +import com.example.base.action.ActivityAction; +import com.example.base.action.BundleAction; +import com.example.base.action.ClickAction; +import com.example.base.action.HandlerAction; + +import java.util.Random; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : Activity 基类 + */ +public abstract class BaseActivity extends AppCompatActivity + implements ActivityAction, ClickAction, HandlerAction, BundleAction { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + initActivity(); + } + + protected void initActivity() { + initLayout(); + initView(); + initData(); + } + + /** + * 获取布局 ID + */ + protected abstract int getLayoutId(); + + /** + * 初始化控件 + */ + protected abstract void initView(); + + /** + * 初始化数据 + */ + protected abstract void initData(); + + /** + * 初始化布局 + */ + protected void initLayout() { + if (getLayoutId() > 0) { + setContentView(getLayoutId()); + initSoftKeyboard(); + } + } + + /** + * 初始化软键盘 + */ + protected void initSoftKeyboard() { + // 点击外部隐藏软键盘,提升用户体验 + getContentView().setOnClickListener(v -> hideSoftKeyboard()); + } + + @Override + protected void onDestroy() { + removeCallbacks(); + super.onDestroy(); + } + + @Override + public void finish() { + hideSoftKeyboard(); + super.finish(); + } + + /** + * 如果当前的 Activity(singleTop 启动模式) 被复用时会回调 + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + // 设置为当前的 Intent,避免 Activity 被杀死后重启 Intent 还是最原先的那个 + setIntent(intent); + } + + @Override + public Bundle getBundle() { + return getIntent().getExtras(); + } + + /** + * 和 setContentView 对应的方法 + */ + public ViewGroup getContentView() { + return findViewById(Window.ID_ANDROID_CONTENT); + } + + @Override + public Context getContext() { + return this; + } + + /** + * startActivityForResult 方法优化 + */ + + private OnActivityCallback mActivityCallback; + private int mActivityRequestCode; + + public void startActivityForResult(Class clazz, OnActivityCallback callback) { + startActivityForResult(new Intent(this, clazz), null, callback); + } + + public void startActivityForResult(Intent intent, OnActivityCallback callback) { + startActivityForResult(intent, null, callback); + } + + public void startActivityForResult(Intent intent, @Nullable Bundle options, OnActivityCallback callback) { + // 回调还没有结束,所以不能再次调用此方法,这个方法只适合一对一回调,其他需求请使用原生的方法实现 + if (mActivityCallback == null) { + mActivityCallback = callback; + // 随机生成请求码,这个请求码必须在 2 的 16 次幂以内,也就是 0 - 65535 + mActivityRequestCode = new Random().nextInt((int) Math.pow(2, 16)); + startActivityForResult(intent, mActivityRequestCode, options); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (mActivityCallback != null && mActivityRequestCode == requestCode) { + mActivityCallback.onActivityResult(resultCode, data); + mActivityCallback = null; + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + hideSoftKeyboard(); + // 查看源码得知 startActivity 最终也会调用 startActivityForResult + super.startActivityForResult(intent, requestCode, options); + } + + /** + * 隐藏软键盘 + */ + private void hideSoftKeyboard() { + // 隐藏软键盘,避免软键盘引发的内存泄露 + View view = getCurrentFocus(); + if (view != null) { + InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (manager != null && manager.isActive(view)) { + manager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } + } + + public interface OnActivityCallback { + + /** + * 结果回调 + * + * @param resultCode 结果码 + * @param data 数据 + */ + void onActivityResult(int resultCode, @Nullable Intent data); + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/BaseAdapter.java b/base/src/main/java/com/example/base/BaseAdapter.java new file mode 100644 index 0000000..d5b2c25 --- /dev/null +++ b/base/src/main/java/com/example/base/BaseAdapter.java @@ -0,0 +1,398 @@ +package com.example.base; + +import android.content.Context; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; + + +import com.example.base.action.ResourcesAction; + +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : RecyclerView 适配器基类 + */ +public abstract class BaseAdapter + extends RecyclerView.Adapter implements ResourcesAction { + + /** 上下文对象 */ + private final Context mContext; + + /** RecyclerView 对象 */ + private RecyclerView mRecyclerView; + + /** 条目点击监听器 */ + private OnItemClickListener mItemClickListener; + /** 条目长按监听器 */ + private OnItemLongClickListener mItemLongClickListener; + /** RecyclerView 滚动事件 */ + private OnScrollingListener mScrollingListener; + + /** 条目子 View 点击监听器 */ + private SparseArray mChildClickListeners; + /** 条目子 View 长按监听器 */ + private SparseArray mChildLongClickListeners; + + /** ViewHolder 位置偏移值 */ + private int mPositionOffset = 0; + + public BaseAdapter(Context context) { + mContext = context; + if (mContext == null) { + throw new IllegalArgumentException("are you ok?"); + } + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public final void onBindViewHolder(@NonNull VH holder, int position) { + // 根据 ViewHolder 绑定的位置和传入的位置进行对比 + // 一般情况下这两个位置值是相等的,但是有一种特殊的情况 + // 在外层添加头部 View 的情况下,这两个位置值是不对等的 + mPositionOffset = position - holder.getAdapterPosition(); + holder.onBindView(position); + } + + /** + * 获取RecyclerView 对象,需要在setAdapter之后绑定 + */ + public RecyclerView getRecyclerView() { + return mRecyclerView; + } + + @Override + public Context getContext() { + return mContext; + } + + /** + * 条目 ViewHolder,需要子类 ViewHolder 继承 + */ + public abstract class ViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + + public ViewHolder(@LayoutRes int id) { + this(LayoutInflater.from(getContext()).inflate(id, getRecyclerView(), false)); + } + + public ViewHolder(View itemView) { + super(itemView); + + // 设置条目的点击和长按事件 + if (mItemClickListener != null) { + itemView.setOnClickListener(this); + } + if (mItemLongClickListener != null) { + itemView.setOnLongClickListener(this); + } + + // 设置条目子 View 点击事件 + if (mChildClickListeners != null) { + for (int i = 0; i < mChildClickListeners.size(); i++) { + View childView = findViewById(mChildClickListeners.keyAt(i)); + if (childView != null) { + childView.setOnClickListener(this); + } + } + } + + // 设置条目子 View 长按事件 + if (mChildLongClickListeners != null) { + for (int i = 0; i < mChildLongClickListeners.size(); i++) { + View childView = findViewById(mChildLongClickListeners.keyAt(i)); + if (childView != null) { + childView.setOnLongClickListener(this); + } + } + } + } + + public abstract void onBindView(int position); + + /** + * 获取 ViewHolder 位置 + */ + protected final int getViewHolderPosition() { + // 这里解释一下为什么用 getLayoutPosition 而不用 getAdapterPosition + // 如果是使用 getAdapterPosition 会导致一个问题,那就是快速点击删除条目的时候会出现 -1 的情况,因为这个 ViewHolder 已经解绑了 + // 而使用 getLayoutPosition 则不会出现位置为 -1 的情况,因为解绑之后在布局中不会立马消失,所以不用担心在动画执行中获取位置有异常的情况 + return getLayoutPosition() + mPositionOffset; + } + + @Override + public void onClick(View v) { + int position = getViewHolderPosition(); + if (position >= 0 && position < getItemCount()) { + if (v == getItemView()) { + if(mItemClickListener != null) { + mItemClickListener.onItemClick(mRecyclerView, v, position); + } + } else { + if (mChildClickListeners != null) { + OnChildClickListener listener = mChildClickListeners.get(v.getId()); + if (listener != null) { + listener.onChildClick(mRecyclerView, v, position); + } + } + } + } + } + + /** + * {@link View.OnLongClickListener} + */ + + @Override + public boolean onLongClick(View v) { + int position = getViewHolderPosition(); + if (position >= 0 && position < getItemCount()) { + if (v == getItemView()) { + if (mItemLongClickListener != null) { + return mItemLongClickListener.onItemLongClick(mRecyclerView, v, position); + } + } else { + if (mChildLongClickListeners != null) { + OnChildLongClickListener listener = mChildLongClickListeners.get(v.getId()); + if (listener != null) { + return listener.onChildLongClick(mRecyclerView, v, position); + } + } + } + } + return false; + } + + public final View getItemView() { + return itemView; + } + + public final V findViewById(@IdRes int id) { + return getItemView().findViewById(id); + } + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + mRecyclerView = recyclerView; + // 用户设置了滚动监听,需要给 RecyclerView 设置监听 + if (mScrollListener != null) { + // 添加滚动监听 + mRecyclerView.addOnScrollListener(mScrollListener); + } + // 判断当前的布局管理器是否为空,如果为空则设置默认的布局管理器 + if (mRecyclerView.getLayoutManager() == null) { + RecyclerView.LayoutManager layoutManager = generateDefaultLayoutManager(mContext); + if (layoutManager != null) { + mRecyclerView.setLayoutManager(layoutManager); + } + } + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + // 移除滚动监听 + if (mScrollListener != null) { + mRecyclerView.removeOnScrollListener(mScrollListener); + } + mRecyclerView = null; + } + + /** + * 生成默认的布局摆放器 + */ + protected RecyclerView.LayoutManager generateDefaultLayoutManager(Context context) { + return new LinearLayoutManager(context); + } + + /** + * 设置 RecyclerView 条目点击监听 + */ + public void setOnItemClickListener(OnItemClickListener listener) { + checkRecyclerViewState(); + mItemClickListener = listener; + } + + /** + * 设置 RecyclerView 条目子 View 点击监听 + */ + public void setOnChildClickListener(@IdRes int id, OnChildClickListener listener) { + checkRecyclerViewState(); + if (mChildClickListeners == null) { + mChildClickListeners = new SparseArray<>(); + } + mChildClickListeners.put(id, listener); + } + + /** + * 设置RecyclerView条目长按监听 + */ + public void setOnItemLongClickListener(OnItemLongClickListener listener) { + checkRecyclerViewState(); + mItemLongClickListener = listener; + } + + /** + * 设置 RecyclerView 条目子 View 长按监听 + */ + public void setOnChildLongClickListener(@IdRes int id, OnChildLongClickListener listener) { + checkRecyclerViewState(); + if (mChildLongClickListeners == null) { + mChildLongClickListeners = new SparseArray<>(); + } + mChildLongClickListeners.put(id, listener); + } + + private void checkRecyclerViewState() { + if (mRecyclerView != null) { + // 必须在 RecyclerView.setAdapter() 之前设置监听 + throw new IllegalStateException("are you ok?"); + } + } + + /** + * 设置 RecyclerView 条目滚动监听 + */ + public void setOnScrollingListener(OnScrollingListener listener) { + mScrollingListener = listener; + + //如果当前已经有设置滚动监听,再次设置需要移除原有的监听器 + if (mScrollListener == null) { + mScrollListener = new ScrollListener(); + } else { + mRecyclerView.removeOnScrollListener(mScrollListener); + } + //用户设置了滚动监听,需要给RecyclerView设置监听 + if (mRecyclerView != null) { + //添加滚动监听 + mRecyclerView.addOnScrollListener(mScrollListener); + } + } + + /** 自定义滚动监听器 */ + private ScrollListener mScrollListener; + + private class ScrollListener extends RecyclerView.OnScrollListener { + + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + if (mScrollingListener == null) { + return; + } + + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + + if (!recyclerView.canScrollVertically(1)) { + // 已经到底了 + mScrollingListener.onScrollDown(recyclerView); + } else if (!recyclerView.canScrollVertically(-1)) { + // 已经到顶了 + mScrollingListener.onScrollTop(recyclerView); + } + + } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + // 正在滚动中 + mScrollingListener.onScrolling(recyclerView); + } + } + } + + /** + * RecyclerView 滚动监听类 + */ + public interface OnScrollingListener { + + /** + * 列表滚动到最顶部 + * + * @param recyclerView RecyclerView 对象 + */ + void onScrollTop(RecyclerView recyclerView); + + /** + * 列表滚动中 + * + * @param recyclerView RecyclerView 对象 + */ + void onScrolling(RecyclerView recyclerView); + + /** + * 列表滚动到最底部 + * + * @param recyclerView RecyclerView 对象 + */ + void onScrollDown(RecyclerView recyclerView); + } + + /** + * RecyclerView 条目点击监听类 + */ + public interface OnItemClickListener{ + + /** + * 当 RecyclerView 某个条目被点击时回调 + * + * @param recyclerView RecyclerView 对象 + * @param itemView 被点击的条目对象 + * @param position 被点击的条目位置 + */ + void onItemClick(RecyclerView recyclerView, View itemView, int position); + } + + /** + * RecyclerView 条目长按监听类 + */ + public interface OnItemLongClickListener { + + /** + * 当 RecyclerView 某个条目被长按时回调 + * + * @param recyclerView RecyclerView 对象 + * @param itemView 被点击的条目对象 + * @param position 被点击的条目位置 + * @return 是否拦截事件 + */ + boolean onItemLongClick(RecyclerView recyclerView, View itemView, int position); + } + + /** + * RecyclerView 条目子 View 点击监听类 + */ + public interface OnChildClickListener { + + /** + * 当 RecyclerView 某个条目 子 View 被点击时回调 + * + * @param recyclerView RecyclerView 对象 + * @param childView 被点击的条目子 View + * @param position 被点击的条目位置 + */ + void onChildClick(RecyclerView recyclerView, View childView, int position); + } + + /** + * RecyclerView 条目子 View 长按监听类 + */ + public interface OnChildLongClickListener { + + /** + * 当 RecyclerView 某个条目子 View 被长按时回调 + * + * @param recyclerView RecyclerView 对象 + * @param childView 被点击的条目子 View + * @param position 被点击的条目位置 + */ + boolean onChildLongClick(RecyclerView recyclerView, View childView, int position); + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/BaseDialog.java b/base/src/main/java/com/example/base/BaseDialog.java new file mode 100644 index 0000000..fec4e7e --- /dev/null +++ b/base/src/main/java/com/example/base/BaseDialog.java @@ -0,0 +1,1350 @@ +package com.example.base; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.example.base.action.ActivityAction; +import com.example.base.action.AnimAction; +import com.example.base.action.ClickAction; +import com.example.base.action.HandlerAction; +import com.example.base.action.ResourcesAction; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.FloatRange; +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.AppCompatDialog; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/11/24 + * desc : Dialog 基类 + */ +public class BaseDialog extends AppCompatDialog implements LifecycleOwner, + ActivityAction, ResourcesAction, HandlerAction, ClickAction, AnimAction, + DialogInterface.OnShowListener, DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { + + private final ListenersWrapper mListeners = new ListenersWrapper<>(this); + private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); + + private List mShowListeners; + private List mCancelListeners; + private List mDismissListeners; + + public BaseDialog(Context context) { + this(context, R.style.BaseDialogStyle); + } + + public BaseDialog(Context context, @StyleRes int themeResId) { + super(context, themeResId); + } + + /** + * 获取 Dialog 的根布局 + */ + public View getContentView() { + return findViewById(Window.ID_ANDROID_CONTENT); + } + + /** + * 获取当前设置重心 + */ + public int getGravity() { + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams params = window.getAttributes(); + return params.gravity; + } + return Gravity.NO_GRAVITY; + } + + /** + * 设置宽度 + */ + public void setWidth(int width) { + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.width = width; + window.setAttributes(params); + } + } + + /** + * 设置高度 + */ + public void setHeight(int height) { + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.height = height; + window.setAttributes(params); + } + } + + /** + * 设置 Dialog 重心 + */ + public void setGravity(int gravity) { + Window window = getWindow(); + if (window != null) { + window.setGravity(gravity); + } + } + + /** + * 设置 Dialog 的动画 + */ + public void setWindowAnimations(@StyleRes int id) { + Window window = getWindow(); + if (window != null) { + window.setWindowAnimations(id); + } + } + + /** + * 获取 Dialog 的动画 + */ + public int getWindowAnimations() { + Window window = getWindow(); + if (window != null) { + return window.getAttributes().windowAnimations; + } + return BaseDialog.ANIM_DEFAULT; + } + + /** + * 设置背景遮盖层开关 + */ + public void setBackgroundDimEnabled(boolean enabled) { + Window window = getWindow(); + if (window != null) { + if (enabled) { + window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } + } + } + + /** + * 设置背景遮盖层的透明度(前提条件是背景遮盖层开关必须是为开启状态) + */ + public void setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) { + Window window = getWindow(); + if (window != null) { + window.setDimAmount(dimAmount); + } + } + + @Override + public void dismiss() { + removeCallbacks(); + View focusView = getCurrentFocus(); + if (focusView != null) { + getSystemService(InputMethodManager.class).hideSoftInputFromWindow(focusView.getWindowToken(), 0); + } + super.dismiss(); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mLifecycle; + } + + /** + * 设置一个显示监听器 + * + * @param listener 显示监听器对象 + * @deprecated 请使用 {@link #addOnShowListener(BaseDialog.OnShowListener)}} + */ + @Deprecated + @Override + public void setOnShowListener(@Nullable DialogInterface.OnShowListener listener) { + if (listener == null) { + return; + } + addOnShowListener(new ShowListenerWrapper(listener)); + } + + /** + * 设置一个取消监听器 + * + * @param listener 取消监听器对象 + * @deprecated 请使用 {@link #addOnCancelListener(BaseDialog.OnCancelListener)} + */ + @Deprecated + @Override + public void setOnCancelListener(@Nullable DialogInterface.OnCancelListener listener) { + if (listener == null) { + return; + } + addOnCancelListener(new CancelListenerWrapper(listener)); + } + + /** + * 设置一个销毁监听器 + * + * @param listener 销毁监听器对象 + * @deprecated 请使用 {@link #addOnDismissListener(BaseDialog.OnDismissListener)} + */ + @Deprecated + @Override + public void setOnDismissListener(@Nullable DialogInterface.OnDismissListener listener) { + if (listener == null) { + return; + } + addOnDismissListener(new DismissListenerWrapper(listener)); + } + + /** + * 设置一个按键监听器 + * + * @param listener 按键监听器对象 + * @deprecated 请使用 {@link #setOnKeyListener(BaseDialog.OnKeyListener)} + */ + @Deprecated + @Override + public void setOnKeyListener(@Nullable DialogInterface.OnKeyListener listener) { + super.setOnKeyListener(listener); + } + + public void setOnKeyListener(@Nullable BaseDialog.OnKeyListener listener) { + super.setOnKeyListener(new KeyListenerWrapper(listener)); + } + + /** + * 添加一个显示监听器 + * + * @param listener 监听器对象 + */ + public void addOnShowListener(@Nullable BaseDialog.OnShowListener listener) { + if (mShowListeners == null) { + mShowListeners = new ArrayList<>(); + super.setOnShowListener(mListeners); + } + mShowListeners.add(listener); + } + + /** + * 添加一个取消监听器 + * + * @param listener 监听器对象 + */ + public void addOnCancelListener(@Nullable BaseDialog.OnCancelListener listener) { + if (mCancelListeners == null) { + mCancelListeners = new ArrayList<>(); + super.setOnCancelListener(mListeners); + } + mCancelListeners.add(listener); + } + + /** + * 添加一个销毁监听器 + * + * @param listener 监听器对象 + */ + public void addOnDismissListener(@Nullable BaseDialog.OnDismissListener listener) { + if (mDismissListeners == null) { + mDismissListeners = new ArrayList<>(); + super.setOnDismissListener(mListeners); + } + mDismissListeners.add(listener); + } + + /** + * 移除一个显示监听器 + * + * @param listener 监听器对象 + */ + public void removeOnShowListener(@Nullable BaseDialog.OnShowListener listener) { + if (mShowListeners != null) { + mShowListeners.remove(listener); + } + } + + /** + * 移除一个取消监听器 + * + * @param listener 监听器对象 + */ + public void removeOnCancelListener(@Nullable BaseDialog.OnCancelListener listener) { + if (mCancelListeners != null) { + mCancelListeners.remove(listener); + } + } + + /** + * 移除一个销毁监听器 + * + * @param listener 监听器对象 + */ + public void removeOnDismissListener(@Nullable BaseDialog.OnDismissListener listener) { + if (mDismissListeners != null) { + mDismissListeners.remove(listener); + } + } + + /** + * 设置显示监听器集合 + */ + private void setOnShowListeners(@Nullable List listeners) { + super.setOnShowListener(mListeners); + mShowListeners = listeners; + } + + /** + * 设置取消监听器集合 + */ + private void setOnCancelListeners(@Nullable List listeners) { + super.setOnCancelListener(mListeners); + mCancelListeners = listeners; + } + + /** + * 设置销毁监听器集合 + */ + private void setOnDismissListeners(@Nullable List listeners) { + super.setOnDismissListener(mListeners); + mDismissListeners = listeners; + } + + /** + * {@link DialogInterface.OnShowListener} + */ + @Override + public void onShow(DialogInterface dialog) { + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + + if (mShowListeners != null) { + for (int i = 0; i < mShowListeners.size(); i++) { + mShowListeners.get(i).onShow(this); + } + } + } + + /** + * {@link DialogInterface.OnCancelListener} + */ + @Override + public void onCancel(DialogInterface dialog) { + if (mCancelListeners != null) { + for (int i = 0; i < mCancelListeners.size(); i++) { + mCancelListeners.get(i).onCancel(this); + } + } + } + + /** + * {@link DialogInterface.OnDismissListener} + */ + @Override + public void onDismiss(DialogInterface dialog) { + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + + if (mDismissListeners != null) { + for (int i = 0; i < mDismissListeners.size(); i++) { + mDismissListeners.get(i).onDismiss(this); + } + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + } + + @Override + protected void onStart() { + super.onStart(); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START); + } + + @Override + protected void onStop() { + super.onStop(); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + } + + @SuppressWarnings("unchecked") + public static class Builder implements LifecycleOwner, ActivityAction, ResourcesAction, ClickAction { + + /** 上下文对象 */ + private final Context mContext; + /** Dialog 对象 */ + private BaseDialog mDialog; + /** Dialog 布局 */ + private View mContentView; + + /** 主题样式 */ + private int mThemeId = R.style.BaseDialogStyle; + /** 动画样式 */ + private int mAnimStyle = BaseDialog.ANIM_DEFAULT; + /** 重心位置 */ + private int mGravity = Gravity.NO_GRAVITY; + + /** 水平偏移 */ + private int mXOffset; + /** 垂直偏移 */ + private int mYOffset; + + /** 宽度和高度 */ + private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + + /** 背景遮盖层开关 */ + private boolean mBackgroundDimEnabled = true; + /** 背景遮盖层透明度 */ + private float mBackgroundDimAmount = 0.5f; + + /** 是否能够被取消 */ + private boolean mCancelable = true; + /** 点击空白是否能够取消 前提是这个对话框可以被取消 */ + private boolean mCanceledOnTouchOutside = true; + + /** Dialog Show 监听 */ + private List mOnShowListeners; + /** Dialog Cancel 监听 */ + private List mOnCancelListeners; + /** Dialog Dismiss 监听 */ + private List mOnDismissListeners; + /** Dialog Key 监听 */ + private BaseDialog.OnKeyListener mOnKeyListener; + + /** 点击事件集合 */ + private SparseArray mClickArray; + + public Builder(Activity activity) { + this((Context) activity); + } + + public Builder(Context context) { + mContext = context; + } + + /** + * 设置主题 id + */ + public B setThemeStyle(@StyleRes int id) { + if (isCreated()) { + // Dialog 创建之后不能再设置主题 id + throw new IllegalStateException("are you ok?"); + } + mThemeId = id; + return (B) this; + } + + /** + * 设置布局 + */ + public B setContentView(@LayoutRes int id) { + // 这里解释一下,为什么要传 new FrameLayout,因为如果不传的话,XML 的根布局获取到的 LayoutParams 对象会为空,也就会导致宽高参数解析不出来 + return setContentView(LayoutInflater.from(mContext).inflate(id, new FrameLayout(mContext), false)); + } + public B setContentView(View view) { + mContentView = view; + + if (isCreated()) { + mDialog.setContentView(view); + } else { + if (mContentView != null) { + ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); + if (layoutParams != null && mWidth == ViewGroup.LayoutParams.WRAP_CONTENT && mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + // 如果当前 Dialog 的宽高设置了自适应,就以布局中设置的宽高为主 + setWidth(layoutParams.width); + setHeight(layoutParams.height); + } + + // 如果当前没有设置重心,就自动获取布局重心 + if (mGravity == Gravity.NO_GRAVITY) { + if (layoutParams instanceof FrameLayout.LayoutParams) { + setGravity(((FrameLayout.LayoutParams) layoutParams).gravity); + } else if (layoutParams instanceof LinearLayout.LayoutParams) { + setGravity(((LinearLayout.LayoutParams) layoutParams).gravity); + } else { + // 默认重心是居中 + setGravity(Gravity.CENTER); + } + } + } + } + return (B) this; + } + + /** + * 设置重心位置 + */ + public B setGravity(int gravity) { + // 适配 Android 4.2 新特性,布局反方向(开发者选项 - 强制使用从右到左的布局方向) + mGravity = gravity; + if (isCreated()) { + mDialog.setGravity(gravity); + } + return (B) this; + } + + /** + * 设置水平偏移 + */ + public B setXOffset(int offset) { + mXOffset = offset; + return (B) this; + } + + /** + * 设置垂直偏移 + */ + public B setYOffset(int offset) { + mYOffset = offset; + return (B) this; + } + + /** + * 设置宽度 + */ + public B setWidth(int width) { + mWidth = width; + if (isCreated()) { + mDialog.setWidth(width); + } else { + ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null; + if (params != null) { + params.width = width; + mContentView.setLayoutParams(params); + } + } + return (B) this; + } + + /** + * 设置高度 + */ + public B setHeight(int height) { + mHeight = height; + if (isCreated()) { + mDialog.setHeight(height); + } else { + // 这里解释一下为什么要重新设置 LayoutParams + // 因为如果不这样设置的话,第一次显示的时候会按照 Dialog 宽高显示 + // 但是 Layout 内容变更之后就不会按照之前的设置宽高来显示 + // 所以这里我们需要对 View 的 LayoutParams 也进行设置 + ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null; + if (params != null) { + params.height = height; + mContentView.setLayoutParams(params); + } + } + return (B) this; + } + + /** + * 是否可以取消 + */ + public B setCancelable(boolean cancelable) { + mCancelable = cancelable; + if (isCreated()) { + mDialog.setCancelable(cancelable); + } + return (B) this; + } + + /** + * 是否可以通过点击空白区域取消 + */ + public B setCanceledOnTouchOutside(boolean cancel) { + mCanceledOnTouchOutside = cancel; + if (isCreated() && mCancelable) { + mDialog.setCanceledOnTouchOutside(cancel); + } + return (B) this; + } + + /** + * 设置动画,已经封装好几种样式,具体可见{@link AnimAction}类 + */ + public B setAnimStyle(@StyleRes int id) { + mAnimStyle = id; + if (isCreated()) { + mDialog.setWindowAnimations(id); + } + return (B) this; + } + + /** + * 设置背景遮盖层开关 + */ + public B setBackgroundDimEnabled(boolean enabled) { + mBackgroundDimEnabled = enabled; + if (isCreated()) { + mDialog.setBackgroundDimEnabled(enabled); + } + return (B) this; + } + + /** + * 设置背景遮盖层的透明度(前提条件是背景遮盖层开关必须是为开启状态) + */ + public B setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) { + mBackgroundDimAmount = dimAmount; + if (isCreated()) { + mDialog.setBackgroundDimAmount(dimAmount); + } + return (B) this; + } + + /** + * 添加显示监听 + */ + public B addOnShowListener(@NonNull BaseDialog.OnShowListener listener) { + if (isCreated()) { + mDialog.addOnShowListener(listener); + } else { + if (mOnShowListeners == null) { + mOnShowListeners = new ArrayList<>(); + } + mOnShowListeners.add(listener); + } + return (B) this; + } + + /** + * 添加取消监听 + */ + public B addOnCancelListener(@NonNull BaseDialog.OnCancelListener listener) { + if (isCreated()) { + mDialog.addOnCancelListener(listener); + } else { + if (mOnCancelListeners == null) { + mOnCancelListeners = new ArrayList<>(); + } + mOnCancelListeners.add(listener); + } + return (B) this; + } + + /** + * 添加销毁监听 + */ + public B addOnDismissListener(@NonNull BaseDialog.OnDismissListener listener) { + if (isCreated()) { + mDialog.addOnDismissListener(listener); + } else { + if (mOnDismissListeners == null) { + mOnDismissListeners = new ArrayList<>(); + } + mOnDismissListeners.add(listener); + } + return (B) this; + } + + /** + * 设置按键监听 + */ + public B setOnKeyListener(@NonNull BaseDialog.OnKeyListener listener) { + if (isCreated()) { + mDialog.setOnKeyListener(listener); + } else { + mOnKeyListener = listener; + } + return (B) this; + } + + /** + * 设置文本 + */ + public B setText(@IdRes int viewId, @StringRes int stringId) { + return setText(viewId, getString(stringId)); + } + public B setText(@IdRes int id, CharSequence text) { + ((TextView) findViewById(id)).setText(text); + return (B) this; + } + + /** + * 设置文本颜色 + */ + public B setTextColor(@IdRes int id, @ColorInt int color) { + ((TextView) findViewById(id)).setTextColor(color); + return (B) this; + } + + /** + * 设置提示 + */ + public B setHint(@IdRes int viewId, @StringRes int stringId) { + return setHint(viewId, getString(stringId)); + } + public B setHint(@IdRes int id, CharSequence text) { + ((TextView) findViewById(id)).setHint(text); + return (B) this; + } + + /** + * 设置可见状态 + */ + public B setVisibility(@IdRes int id, int visibility) { + findViewById(id).setVisibility(visibility); + return (B) this; + } + + /** + * 设置背景 + */ + public B setBackground(@IdRes int viewId, @DrawableRes int drawableId) { + return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId)); + } + public B setBackground(@IdRes int id, Drawable drawable) { + findViewById(id).setBackground(drawable); + return (B) this; + } + + /** + * 设置图片 + */ + public B setImageDrawable(@IdRes int viewId, @DrawableRes int drawableId) { + return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId)); + } + public B setImageDrawable(@IdRes int id, Drawable drawable) { + ((ImageView) findViewById(id)).setImageDrawable(drawable); + return (B) this; + } + + /** + * 设置点击事件 + */ + public B setOnClickListener(@IdRes int id, @NonNull BaseDialog.OnClickListener listener) { + if (isCreated()) { + View view = mDialog.findViewById(id); + if (view != null) { + view.setOnClickListener(new ViewClickWrapper(mDialog, listener)); + } + } else { + if (mClickArray == null) { + mClickArray = new SparseArray<>(); + } + mClickArray.put(id, listener); + } + return (B) this; + } + + /** + * 创建 + */ + @SuppressLint("RtlHardcoded") + public BaseDialog create() { + + // 判断布局是否为空 + if (mContentView == null) { + throw new IllegalArgumentException("are you ok?"); + } + + // 如果当前没有设置重心,就设置一个默认的重心 + if (mGravity == Gravity.NO_GRAVITY) { + mGravity = Gravity.CENTER; + } + + // 如果当前没有设置动画效果,就设置一个默认的动画效果 + if (mAnimStyle == BaseDialog.ANIM_DEFAULT) { + switch (mGravity) { + case Gravity.TOP: + mAnimStyle = BaseDialog.ANIM_TOP; + break; + case Gravity.BOTTOM: + mAnimStyle = BaseDialog.ANIM_BOTTOM; + break; + case Gravity.LEFT: + mAnimStyle = BaseDialog.ANIM_LEFT; + break; + case Gravity.RIGHT: + mAnimStyle = BaseDialog.ANIM_RIGHT; + break; + default: + mAnimStyle = BaseDialog.ANIM_DEFAULT; + break; + } + } + + mDialog = createDialog(mContext, mThemeId); + + mDialog.setContentView(mContentView); + mDialog.setCancelable(mCancelable); + if (mCancelable) { + mDialog.setCanceledOnTouchOutside(mCanceledOnTouchOutside); + } + + // 设置参数 + Window window = mDialog.getWindow(); + if (window != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.width = mWidth; + params.height = mHeight; + params.gravity = mGravity; + params.x = mXOffset; + params.y = mYOffset; + params.windowAnimations = mAnimStyle; + window.setAttributes(params); + if (mBackgroundDimEnabled) { + window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.setDimAmount(mBackgroundDimAmount); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } + } + + if (mOnShowListeners != null) { + mDialog.setOnShowListeners(mOnShowListeners); + } + + if (mOnCancelListeners != null) { + mDialog.setOnCancelListeners(mOnCancelListeners); + } + + if (mOnDismissListeners != null) { + mDialog.setOnDismissListeners(mOnDismissListeners); + } + + if (mOnKeyListener != null) { + mDialog.setOnKeyListener(mOnKeyListener); + } + + for (int i = 0; mClickArray != null && i < mClickArray.size(); i++) { + mContentView.findViewById(mClickArray.keyAt(i)).setOnClickListener(new ViewClickWrapper(mDialog, mClickArray.valueAt(i))); + } + + Activity activity = getActivity(); + if (activity != null) { + DialogLifecycle.with(activity, mDialog); + } + + return mDialog; + } + + /** + * 显示 + */ + public BaseDialog show() { + if (!isCreated()) { + create(); + } + mDialog.show(); + return mDialog; + } + + /** + * 销毁当前 Dialog + */ + public void dismiss() { + if (mDialog != null) { + mDialog.dismiss(); + } + } + + @Override + public Context getContext() { + return mContext; + } + + /** + * 当前 Dialog 是否创建了 + */ + public boolean isCreated() { + return mDialog != null; + } + + /** + * 当前 Dialog 是否显示了 + */ + public boolean isShowing() { + return mDialog != null && mDialog.isShowing(); + } + + /** + * 创建 Dialog 对象(子类可以重写此方法来改变 Dialog 类型) + */ + protected BaseDialog createDialog(Context context, @StyleRes int themeId) { + return new BaseDialog(context, themeId); + } + + /** + * 延迟执行 + */ + public final void post(Runnable r) { + if (isShowing()) { + mDialog.post(r); + } else { + addOnShowListener(new ShowPostWrapper(r)); + } + } + + /** + * 延迟一段时间执行 + */ + public final void postDelayed(Runnable r, long delayMillis) { + if (isShowing()) { + mDialog.postDelayed(r, delayMillis); + } else { + addOnShowListener(new ShowPostDelayedWrapper(r, delayMillis)); + } + } + + /** + * 在指定的时间执行 + */ + public final void postAtTime(Runnable r, long uptimeMillis) { + if (isShowing()) { + mDialog.postAtTime(r, uptimeMillis); + } else { + addOnShowListener(new ShowPostAtTimeWrapper(r, uptimeMillis)); + } + } + + /** + * 获取 Dialog 的根布局 + */ + public View getContentView() { + return mContentView; + } + + /** + * 根据 id 查找 View + */ + @Override + public V findViewById(@IdRes int id) { + if (mContentView == null) { + // 没有 setContentView 就想 findViewById ? + throw new IllegalStateException("are you ok?"); + } + return mContentView.findViewById(id); + } + + /** + * 获取当前 Dialog 对象 + */ + @Nullable + public BaseDialog getDialog() { + return mDialog; + } + + @Nullable + @Override + public Lifecycle getLifecycle() { + if (mDialog != null) { + return mDialog.getLifecycle(); + } + return null; + } + } + + /** + * Dialog 生命周期管理 + */ + private static final class DialogLifecycle implements + Application.ActivityLifecycleCallbacks, + BaseDialog.OnShowListener, + BaseDialog.OnDismissListener { + + private static void with(Activity activity, BaseDialog dialog) { + new DialogLifecycle(activity, dialog); + } + + private BaseDialog mDialog; + private Activity mActivity; + + /** Dialog 动画样式(避免 Dialog 从后台返回到前台后再次触发动画效果) */ + private int mDialogAnim; + + private DialogLifecycle(Activity activity, BaseDialog dialog) { + mActivity = activity; + dialog.addOnShowListener(this); + dialog.addOnDismissListener(this); + } + + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(@NonNull Activity activity) {} + + @Override + public void onActivityResumed(@NonNull Activity activity) { + if (mActivity != activity) { + return; + } + + if (mDialog != null && mDialog.isShowing()) { + // 还原 Dialog 动画样式(这里必须要使用延迟设置,否则还是有一定几率会出现) + mDialog.postDelayed(() -> { + if (mDialog != null && mDialog.isShowing()) { + mDialog.setWindowAnimations(mDialogAnim); + } + }, 100); + } + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + if (mActivity != activity) { + return; + } + + if (mDialog != null && mDialog.isShowing()) { + // 获取 Dialog 动画样式 + mDialogAnim = mDialog.getWindowAnimations(); + // 设置 Dialog 无动画效果 + mDialog.setWindowAnimations(BaseDialog.ANIM_EMPTY); + } + } + + @Override + public void onActivityStopped(@NonNull Activity activity) {} + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + if (mActivity != activity) { + return; + } + + if (mDialog != null) { + mDialog.removeOnShowListener(this); + mDialog.removeOnDismissListener(this); + if (mDialog.isShowing()) { + mDialog.dismiss(); + } + mDialog = null; + } + unregisterActivityLifecycleCallbacks(); + // 释放 Activity 对象 + mActivity = null; + } + + @Override + public void onShow(BaseDialog dialog) { + mDialog = dialog; + registerActivityLifecycleCallbacks(); + } + + @Override + public void onDismiss(BaseDialog dialog) { + mDialog = null; + unregisterActivityLifecycleCallbacks(); + } + + /** + * 注册 Activity 生命周期监听 + */ + private void registerActivityLifecycleCallbacks() { + if (mActivity == null) { + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + mActivity.registerActivityLifecycleCallbacks(this); + } else { + mActivity.getApplication().registerActivityLifecycleCallbacks(this); + } + } + + /** + * 反注册 Activity 生命周期监听 + */ + private void unregisterActivityLifecycleCallbacks() { + if (mActivity == null) { + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + mActivity.unregisterActivityLifecycleCallbacks(this); + } else { + mActivity.getApplication().unregisterActivityLifecycleCallbacks(this); + } + } + } + + /** + * Dialog 监听包装类(修复原生 Dialog 监听器对象导致的内存泄漏) + */ + private static final class ListenersWrapper + extends SoftReference implements DialogInterface.OnShowListener, DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { + + private ListenersWrapper(T referent) { + super(referent); + } + + @Override + public void onShow(DialogInterface dialog) { + if (get() != null) { + get().onShow(dialog); + } + } + + @Override + public void onCancel(DialogInterface dialog) { + if (get() != null) { + get().onCancel(dialog); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + if (get() != null) { + get().onDismiss(dialog); + } + } + } + + /** + * 点击事件包装类 + */ + private static final class ViewClickWrapper + implements View.OnClickListener { + + private final BaseDialog mDialog; + private final BaseDialog.OnClickListener mListener; + + private ViewClickWrapper(BaseDialog dialog, BaseDialog.OnClickListener listener) { + mDialog = dialog; + mListener = listener; + } + + @SuppressWarnings("unchecked") + @Override + public final void onClick(View v) { + mListener.onClick(mDialog, v); + } + } + + /** + * 显示监听包装类 + */ + private static final class ShowListenerWrapper + extends SoftReference + implements BaseDialog.OnShowListener { + + private ShowListenerWrapper(DialogInterface.OnShowListener referent) { + super(referent); + } + + @Override + public void onShow(BaseDialog dialog) { + // 在横竖屏切换后监听对象会为空 + if (get() != null) { + get().onShow(dialog); + } + } + } + + /** + * 取消监听包装类 + */ + private static final class CancelListenerWrapper + extends SoftReference + implements BaseDialog.OnCancelListener { + + private CancelListenerWrapper(DialogInterface.OnCancelListener referent) { + super(referent); + } + + @Override + public void onCancel(BaseDialog dialog) { + // 在横竖屏切换后监听对象会为空 + if (get() != null) { + get().onCancel(dialog); + } + } + } + + /** + * 销毁监听包装类 + */ + private static final class DismissListenerWrapper + extends SoftReference + implements BaseDialog.OnDismissListener { + + private DismissListenerWrapper(DialogInterface.OnDismissListener referent) { + super(referent); + } + + @Override + public void onDismiss(BaseDialog dialog) { + // 在横竖屏切换后监听对象会为空 + if (get() != null) { + get().onDismiss(dialog); + } + } + } + + /** + * 按键监听包装类 + */ + private static final class KeyListenerWrapper + implements DialogInterface.OnKeyListener { + + private final BaseDialog.OnKeyListener mListener; + + private KeyListenerWrapper(BaseDialog.OnKeyListener listener) { + mListener = listener; + } + + @Override + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { + // 在横竖屏切换后监听对象会为空 + if (mListener != null && dialog instanceof BaseDialog) { + mListener.onKey((BaseDialog) dialog, event); + } + return false; + } + } + + /** + * post 任务包装类 + */ + private static final class ShowPostWrapper implements OnShowListener { + + private final Runnable mRunnable; + + private ShowPostWrapper(Runnable r) { + mRunnable = r; + } + + @Override + public void onShow(BaseDialog dialog) { + if (mRunnable != null) { + dialog.removeOnShowListener(this); + dialog.post(mRunnable); + } + } + } + + /** + * postDelayed 任务包装类 + */ + private static final class ShowPostDelayedWrapper implements OnShowListener { + + private final Runnable mRunnable; + private final long mDelayMillis; + + private ShowPostDelayedWrapper(Runnable r, long delayMillis) { + mRunnable = r; + mDelayMillis = delayMillis; + } + + @Override + public void onShow(BaseDialog dialog) { + if (mRunnable != null) { + dialog.removeOnShowListener(this); + dialog.postDelayed(mRunnable, mDelayMillis); + } + } + } + + /** + * postAtTime 任务包装类 + */ + private static final class ShowPostAtTimeWrapper implements OnShowListener { + + private final Runnable mRunnable; + private final long mUptimeMillis; + + private ShowPostAtTimeWrapper(Runnable r, long uptimeMillis) { + mRunnable = r; + mUptimeMillis = uptimeMillis; + } + + @Override + public void onShow(BaseDialog dialog) { + if (mRunnable != null) { + dialog.removeOnShowListener(this); + dialog.postAtTime(mRunnable, mUptimeMillis); + } + } + } + + /** + * 点击监听器 + */ + public interface OnClickListener { + void onClick(BaseDialog dialog, V view); + } + + /** + * 显示监听器 + */ + public interface OnShowListener { + + /** + * Dialog 显示了 + */ + void onShow(BaseDialog dialog); + } + + /** + * 取消监听器 + */ + public interface OnCancelListener { + + /** + * Dialog 取消了 + */ + void onCancel(BaseDialog dialog); + } + + /** + * 销毁监听器 + */ + public interface OnDismissListener { + + /** + * Dialog 销毁了 + */ + void onDismiss(BaseDialog dialog); + } + + /** + * 按键监听器 + */ + public interface OnKeyListener { + + /** + * 触发了按键 + */ + boolean onKey(BaseDialog dialog, KeyEvent event); + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/BaseFragment.java b/base/src/main/java/com/example/base/BaseFragment.java new file mode 100644 index 0000000..619aed3 --- /dev/null +++ b/base/src/main/java/com/example/base/BaseFragment.java @@ -0,0 +1,186 @@ +package com.example.base; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.example.base.action.ActivityAction; +import com.example.base.action.BundleAction; +import com.example.base.action.ClickAction; +import com.example.base.action.HandlerAction; +import com.example.base.action.ResourcesAction; + +import java.util.Random; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : Fragment 基类 + */ +public abstract class BaseFragment extends Fragment implements + ActivityAction, ResourcesAction, HandlerAction, ClickAction, BundleAction { + + /** Activity 对象 */ + private A mActivity; + /** 根布局 */ + private View mRootView; + /** 当前是否加载过 */ + private boolean mLoading; + + @SuppressWarnings("unchecked") + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + // 获得全局的 Activity + mActivity = (A) requireActivity(); + } + + @Override + public void onDetach() { + removeCallbacks(); + mActivity = null; + super.onDetach(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mLoading = false; + if (getLayoutId() > 0) { + return mRootView = inflater.inflate(getLayoutId(), null); + } else { + return null; + } + } + + @Override + public void onDestroyView() { + mLoading = false; + mRootView = null; + super.onDestroyView(); + } + + @Override + public void onResume() { + super.onResume(); + if (!mLoading) { + mLoading = true; + initFragment(); + } + } + + /** + * 这个 Fragment 是否已经加载过了 + */ + public boolean isLoading() { + return mLoading; + } + + @NonNull + @Override + public View getView() { + return mRootView; + } + + /** + * 获取绑定的 Activity,防止出现 getActivity 为空 + */ + public A getAttachActivity() { + return mActivity; + } + + protected void initFragment() { + initView(); + initData(); + } + + /** + * 获取布局 ID + */ + protected abstract int getLayoutId(); + + /** + * 初始化控件 + */ + protected abstract void initView(); + + /** + * 初始化数据 + */ + protected abstract void initData(); + + /** + * 根据资源 id 获取一个 View 对象 + */ + @Override + public V findViewById(@IdRes int id) { + return mRootView.findViewById(id); + } + + @Override + public Bundle getBundle() { + return getArguments(); + } + + /** + * startActivityForResult 方法优化 + */ + + private BaseActivity.OnActivityCallback mActivityCallback; + private int mActivityRequestCode; + + public void startActivityForResult(Class clazz, BaseActivity.OnActivityCallback callback) { + startActivityForResult(new Intent(mActivity, clazz), null, callback); + } + + public void startActivityForResult(Intent intent, BaseActivity.OnActivityCallback callback) { + startActivityForResult(intent, null, callback); + } + + public void startActivityForResult(Intent intent, Bundle options, BaseActivity.OnActivityCallback callback) { + // 回调还没有结束,所以不能再次调用此方法,这个方法只适合一对一回调,其他需求请使用原生的方法实现 + if (mActivityCallback == null) { + mActivityCallback = callback; + // 随机生成请求码,这个请求码必须在 2 的 16 次幂以内,也就是 0 - 65535 + mActivityRequestCode = new Random().nextInt((int) Math.pow(2, 16)); + startActivityForResult(intent, mActivityRequestCode, options); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (mActivityCallback != null && mActivityRequestCode == requestCode) { + mActivityCallback.onActivityResult(resultCode, data); + mActivityCallback = null; + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * 销毁当前 Fragment 所在的 Activity + */ + public void finish() { + if (mActivity != null && !mActivity.isFinishing()) { + mActivity.finish(); + } + } + + /** + * Fragment 返回键被按下时回调 + */ + public boolean onKeyDown(int keyCode, KeyEvent event) { + // 默认不拦截按键事件,回传给 Activity + return false; + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/BaseFragmentAdapter.java b/base/src/main/java/com/example/base/BaseFragmentAdapter.java new file mode 100644 index 0000000..68d346f --- /dev/null +++ b/base/src/main/java/com/example/base/BaseFragmentAdapter.java @@ -0,0 +1,135 @@ +package com.example.base; + +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : FragmentPagerAdapter 基类 + */ +public class BaseFragmentAdapter extends FragmentPagerAdapter { + + /** Fragment 集合 */ + private final List mFragmentSet = new ArrayList<>(); + /** Fragment 标题 */ + private final List mFragmentTitle = new ArrayList<>(); + + /** 当前显示的Fragment */ + private F mShowFragment; + + /** 当前 ViewPager */ + private ViewPager mViewPager; + + /** 设置成懒加载模式 */ + private boolean mLazyMode = true; + + public BaseFragmentAdapter(FragmentActivity activity) { + this(activity.getSupportFragmentManager()); + } + + public BaseFragmentAdapter(Fragment fragment) { + this(fragment.getChildFragmentManager()); + } + + public BaseFragmentAdapter(FragmentManager manager) { + super(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @NonNull + @Override + public F getItem(int position) { + return mFragmentSet.get(position); + } + + @Override + public int getCount() { + return mFragmentSet.size(); + } + + @Nullable + @Override + public CharSequence getPageTitle(int position) { + return mFragmentTitle.get(position); + } + + @SuppressWarnings("unchecked") + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getShowFragment() != object) { + // 记录当前的Fragment对象 + mShowFragment = (F) object; + } + super.setPrimaryItem(container, position, object); + } + + /** + * 添加 Fragment + */ + public void addFragment(F fragment) { + addFragment(fragment, null); + } + + public void addFragment(F fragment, CharSequence title) { + mFragmentSet.add(fragment); + mFragmentTitle.add(title); + if (mViewPager != null) { + notifyDataSetChanged(); + if (mLazyMode) { + mViewPager.setOffscreenPageLimit(getCount()); + } + } + } + + /** + * 获取当前的Fragment + */ + public F getShowFragment() { + return mShowFragment; + } + + @Override + public void startUpdate(@NonNull ViewGroup container) { + super.startUpdate(container); + if (container instanceof ViewPager) { + // 记录绑定 ViewPager + mViewPager = (ViewPager) container; + refreshLazyMode(); + } + } + + /** + * 设置懒加载模式 + */ + public void setLazyMode(boolean lazy) { + mLazyMode = lazy; + refreshLazyMode(); + } + + /** + * 刷新加载模式 + */ + private void refreshLazyMode() { + if (mViewPager == null) { + return; + } + + if (mLazyMode) { + // 设置成懒加载模式(也就是不限制 Fragment 展示的数量) + mViewPager.setOffscreenPageLimit(getCount()); + } else { + mViewPager.setOffscreenPageLimit(1); + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/BasePopupWindow.java b/base/src/main/java/com/example/base/BasePopupWindow.java new file mode 100644 index 0000000..ed5857f --- /dev/null +++ b/base/src/main/java/com/example/base/BasePopupWindow.java @@ -0,0 +1,904 @@ +package com.example.base; + +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.SparseArray; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.example.base.action.ActivityAction; +import com.example.base.action.AnimAction; +import com.example.base.action.ClickAction; +import com.example.base.action.HandlerAction; +import com.example.base.action.ResourcesAction; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.FloatRange; +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.core.content.ContextCompat; +import androidx.core.widget.PopupWindowCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/16 + * desc : PopupWindow 基类 + */ +public class BasePopupWindow extends PopupWindow + implements ActivityAction, HandlerAction, ClickAction, + AnimAction, PopupWindow.OnDismissListener { + + private final Context mContext; + private PopupBackground mPopupBackground; + + private List mShowListeners; + private List mDismissListeners; + + public BasePopupWindow(@NonNull Context context) { + super(context); + mContext = context; + } + + @Override + public Context getContext() { + return mContext; + } + + /** + * 设置一个销毁监听器 + * + * @param listener 销毁监听器对象 + * @deprecated 请使用 {@link #addOnDismissListener(BasePopupWindow.OnDismissListener)} + */ + @Deprecated + @Override + public void setOnDismissListener(PopupWindow.OnDismissListener listener) { + if (listener == null) { + return; + } + addOnDismissListener(new DismissListenerWrapper(listener)); + } + + /** + * 添加一个显示监听器 + * + * @param listener 监听器对象 + */ + public void addOnShowListener(@Nullable BasePopupWindow.OnShowListener listener) { + if (mShowListeners == null) { + mShowListeners = new ArrayList<>(); + } + mShowListeners.add(listener); + } + + /** + * 添加一个销毁监听器 + * + * @param listener 监听器对象 + */ + public void addOnDismissListener(@Nullable BasePopupWindow.OnDismissListener listener) { + if (mDismissListeners == null) { + mDismissListeners = new ArrayList<>(); + super.setOnDismissListener(this); + } + mDismissListeners.add(listener); + } + + /** + * 移除一个显示监听器 + * + * @param listener 监听器对象 + */ + public void removeOnShowListener(@Nullable BasePopupWindow.OnShowListener listener) { + if (mShowListeners != null) { + mShowListeners.remove(listener); + } + } + + /** + * 移除一个销毁监听器 + * + * @param listener 监听器对象 + */ + public void removeOnDismissListener(@Nullable BasePopupWindow.OnDismissListener listener) { + if (mDismissListeners != null) { + mDismissListeners.remove(listener); + } + } + + /** + * 设置显示监听器集合 + */ + private void setOnShowListeners(@Nullable List listeners) { + mShowListeners = listeners; + } + + /** + * 设置销毁监听器集合 + */ + private void setOnDismissListeners(@Nullable List listeners) { + super.setOnDismissListener(this); + mDismissListeners = listeners; + } + + /** + * {@link PopupWindow.OnDismissListener} + */ + @Override + public void onDismiss() { + if (mDismissListeners != null) { + for (BasePopupWindow.OnDismissListener listener : mDismissListeners) { + listener.onDismiss(this); + } + } + } + + @Override + public void showAsDropDown(View anchor, int xOff, int yOff, int gravity) { + if (isShowing() || getContentView() == null) { + return; + } + + if (mShowListeners != null) { + for (BasePopupWindow.OnShowListener listener : mShowListeners) { + listener.onShow(this); + } + } + super.showAsDropDown(anchor, xOff, yOff, gravity); + } + + @Override + public void showAtLocation(View parent, int gravity, int x, int y) { + if (isShowing() || getContentView() == null) { + return; + } + + if (mShowListeners != null) { + for (BasePopupWindow.OnShowListener listener : mShowListeners) { + listener.onShow(this); + } + } + super.showAtLocation(parent, gravity, x, y); + } + + @Override + public void dismiss() { + removeCallbacks(); + super.dismiss(); + } + + @Override + public V findViewById(@IdRes int id) { + return getContentView().findViewById(id); + } + + @Override + public void setWindowLayoutType(int type) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + super.setWindowLayoutType(type); + } else { + PopupWindowCompat.setWindowLayoutType(this, type); + } + } + + @Override + public int getWindowLayoutType() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return super.getWindowLayoutType(); + } else { + return PopupWindowCompat.getWindowLayoutType(this); + } + } + + @Override + public void setOverlapAnchor(boolean overlapAnchor) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + super.setOverlapAnchor(overlapAnchor); + } else { + PopupWindowCompat.setOverlapAnchor(this, overlapAnchor); + } + } + + /** + * 设置背景遮盖层的透明度 + */ + public void setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) { + float alpha = 1 - dimAmount; + if (isShowing()) { + setActivityAlpha(alpha); + } + if (mPopupBackground == null && alpha != 1) { + mPopupBackground = new PopupBackground(); + addOnShowListener(mPopupBackground); + addOnDismissListener(mPopupBackground); + } + if (mPopupBackground != null) { + mPopupBackground.setAlpha(alpha); + } + } + + /** + * 设置 Activity 窗口透明度 + */ + private void setActivityAlpha(float alpha) { + if (mContext instanceof Activity) { + Activity activity = (Activity) mContext; + WindowManager.LayoutParams params = activity.getWindow().getAttributes(); + + final ValueAnimator animator = ValueAnimator.ofFloat(params.alpha, alpha); + animator.setDuration(300); + animator.addUpdateListener(animation -> { + float value = (float) animation.getAnimatedValue(); + if (value != params.alpha) { + params.alpha = value; + activity.getWindow().setAttributes(params); + } + }); + animator.start(); + } + } + + @SuppressWarnings("unchecked") + public static class Builder implements ResourcesAction, ClickAction { + + private static final int DEFAULT_ANCHORED_GRAVITY = Gravity.TOP | Gravity.START; + + /** Context 对象 */ + private final Context mContext; + /** PopupWindow 布局 */ + private View mContentView; + /** PopupWindow 对象 */ + private BasePopupWindow mPopupWindow; + + /** PopupWindow Show 监听 */ + private List mOnShowListeners; + /** PopupWindow Dismiss 监听 */ + private List mOnDismissListeners; + + /** 动画 */ + private int mAnimations = BasePopupWindow.ANIM_DEFAULT; + /** 位置 */ + private int mGravity = DEFAULT_ANCHORED_GRAVITY; + /** 宽度和高度 */ + private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + + /** 是否可触摸 */ + private boolean mTouchable = true; + /** 是否有焦点 */ + private boolean mFocusable = true; + /** 是否外层可触摸 */ + private boolean mOutsideTouchable = false; + + /** 背景遮盖层透明度 */ + private float mBackgroundDimAmount; + + /** X 轴偏移 */ + private int mXOffset; + /** Y 轴偏移 */ + private int mYOffset; + + /** 点击事件集合 */ + private SparseArray mClickArray; + + public Builder(Context context) { + mContext = context; + } + + /** + * 设置布局 + */ + public B setContentView(@LayoutRes int id) { + // 这里解释一下,为什么要传 new FrameLayout,因为如果不传的话,XML 的根布局获取到的 LayoutParams 对象会为空,也就会导致宽高解析不出来 + return setContentView(LayoutInflater.from(mContext).inflate(id, new FrameLayout(mContext), false)); + } + public B setContentView(View view) { + mContentView = view; + + if (isCreated()) { + mPopupWindow.setContentView(view); + } else { + if (mContentView != null) { + ViewGroup.LayoutParams params = mContentView.getLayoutParams(); + if (params != null && mWidth == ViewGroup.LayoutParams.WRAP_CONTENT && mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + // 如果当前 PopupWindow 的宽高设置了自适应,就以布局中设置的宽高为主 + setWidth(params.width); + setHeight(params.height); + } + + // 如果当前没有设置重心,就自动获取布局重心 + if (mGravity == DEFAULT_ANCHORED_GRAVITY) { + if (params instanceof FrameLayout.LayoutParams) { + setGravity(((FrameLayout.LayoutParams) params).gravity); + } else if (params instanceof LinearLayout.LayoutParams) { + setGravity(((LinearLayout.LayoutParams) params).gravity); + } else { + // 默认重心是居中 + setGravity(Gravity.CENTER); + } + } + } + } + return (B) this; + } + + /** + * 设置重心位置 + */ + public B setGravity(int gravity) { + // 适配 Android 4.2 新特性,布局反方向(开发者选项 - 强制使用从右到左的布局方向) + mGravity = Gravity.getAbsoluteGravity(gravity, getResources().getConfiguration().getLayoutDirection()); + return (B) this; + } + + /** + * 设置宽度 + */ + public B setWidth(int width) { + mWidth = width; + if (isCreated()) { + mPopupWindow.setWidth(width); + } else { + ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null; + if (params != null) { + params.width = width; + mContentView.setLayoutParams(params); + } + } + return (B) this; + } + + /** + * 设置高度 + */ + public B setHeight(int height) { + mHeight = height; + if (isCreated()) { + mPopupWindow.setHeight(height); + } else { + // 这里解释一下为什么要重新设置 LayoutParams + // 因为如果不这样设置的话,第一次显示的时候会按照 PopupWindow 宽高显示 + // 但是 Layout 内容变更之后就不会按照之前的设置宽高来显示 + // 所以这里我们需要对 View 的 LayoutParams 也进行设置 + ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null; + if (params != null) { + params.height = height; + mContentView.setLayoutParams(params); + } + } + return (B) this; + } + + /** + * 是否可触摸 + */ + public B setTouchable(boolean touchable) { + mTouchable = touchable; + return (B) this; + } + + /** + * 是否有焦点 + */ + public B setFocusable(boolean focusable) { + mFocusable = focusable; + return (B) this; + } + + /** + * 是否外层可触摸 + */ + public B setOutsideTouchable(boolean touchable) { + mOutsideTouchable = touchable; + return (B) this; + } + + /** + * 设置水平偏移量 + */ + public B setXOffset(int offset) { + mXOffset = offset; + return (B) this; + } + + /** + * 设置垂直偏移量 + */ + public B setYOffset(int offset) { + mYOffset = offset; + return (B) this; + } + + /** + * 设置动画,已经封装好几种样式,具体可见{@link AnimAction}类 + */ + public B setAnimStyle(@StyleRes int id) { + mAnimations = id; + if (isCreated()) { + mPopupWindow.setAnimationStyle(id); + } + return (B) this; + } + + /** + * 设置背景遮盖层的透明度 + */ + public B setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) { + mBackgroundDimAmount = dimAmount; + if (isShowing()) { + mPopupWindow.setBackgroundDimAmount(dimAmount); + } + return (B) this; + } + + /** + * 添加显示监听 + */ + public B addOnShowListener(@NonNull BasePopupWindow.OnShowListener listener) { + if (isCreated()) { + mPopupWindow.addOnShowListener(listener); + } else { + if (mOnShowListeners == null) { + mOnShowListeners = new ArrayList<>(); + } + mOnShowListeners.add(listener); + } + return (B) this; + } + + /** + * 添加销毁监听 + */ + public B addOnDismissListener(@NonNull BasePopupWindow.OnDismissListener listener) { + if (isCreated()) { + mPopupWindow.addOnDismissListener(listener); + } else { + if (mOnDismissListeners == null) { + mOnDismissListeners = new ArrayList<>(); + } + mOnDismissListeners.add(listener); + } + return (B) this; + } + + /** + * 设置文本 + */ + public B setText(@IdRes int viewId, @StringRes int stringId) { + return setText(viewId, getString(stringId)); + } + public B setText(@IdRes int id, CharSequence text) { + ((TextView) findViewById(id)).setText(text); + return (B) this; + } + + /** + * 设置文本颜色 + */ + public B setTextColor(@IdRes int id, @ColorInt int color) { + ((TextView) findViewById(id)).setTextColor(color); + return (B) this; + } + + /** + * 设置提示 + */ + public B setHint(@IdRes int viewId, @StringRes int stringId) { + return setHint(viewId, getString(stringId)); + } + public B setHint(@IdRes int id, CharSequence text) { + ((TextView) findViewById(id)).setHint(text); + return (B) this; + } + + /** + * 设置可见状态 + */ + public B setVisibility(@IdRes int id, int visibility) { + findViewById(id).setVisibility(visibility); + return (B) this; + } + + /** + * 设置背景 + */ + public B setBackground(@IdRes int viewId, @DrawableRes int drawableId) { + return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId)); + } + public B setBackground(@IdRes int id, Drawable drawable) { + findViewById(id).setBackground(drawable); + return (B) this; + } + + /** + * 设置图片 + */ + public B setImageDrawable(@IdRes int viewId, @DrawableRes int drawableId) { + return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId)); + } + public B setImageDrawable(@IdRes int id, Drawable drawable) { + ((ImageView) findViewById(id)).setImageDrawable(drawable); + return (B) this; + } + + /** + * 设置点击事件 + */ + public B setOnClickListener(@IdRes int id, @NonNull BasePopupWindow.OnClickListener listener) { + if (isCreated()) { + View view = mPopupWindow.findViewById(id); + if (view != null) { + view.setOnClickListener(new ViewClickWrapper(mPopupWindow, listener)); + } + } else { + if (mClickArray == null) { + mClickArray = new SparseArray<>(); + } + mClickArray.put(id, listener); + } + return (B) this; + } + + /** + * 创建 + */ + @SuppressLint("RtlHardcoded") + public BasePopupWindow create() { + + // 判断布局是否为空 + if (mContentView == null) { + throw new IllegalArgumentException("are you ok?"); + } + + // 如果当前没有设置重心,就设置一个默认的重心 + if (mGravity == DEFAULT_ANCHORED_GRAVITY) { + mGravity = Gravity.CENTER; + } + + // 如果当前没有设置动画效果,就设置一个默认的动画效果 + if (mAnimations == BasePopupWindow.ANIM_DEFAULT) { + switch (mGravity) { + case Gravity.TOP: + mAnimations = BasePopupWindow.ANIM_TOP; + break; + case Gravity.BOTTOM: + mAnimations = BasePopupWindow.ANIM_BOTTOM; + break; + case Gravity.LEFT: + mAnimations = BasePopupWindow.ANIM_LEFT; + break; + case Gravity.RIGHT: + mAnimations = BasePopupWindow.ANIM_RIGHT; + break; + default: + mAnimations = BasePopupWindow.ANIM_DEFAULT; + break; + } + } + + mPopupWindow = createPopupWindow(mContext); + mPopupWindow.setContentView(mContentView); + mPopupWindow.setWidth(mWidth); + mPopupWindow.setHeight(mHeight); + mPopupWindow.setAnimationStyle(mAnimations); + mPopupWindow.setTouchable(mTouchable); + mPopupWindow.setFocusable(mFocusable); + mPopupWindow.setOutsideTouchable(mOutsideTouchable); + mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + + if (mOnShowListeners != null) { + mPopupWindow.setOnShowListeners(mOnShowListeners); + } + + if (mOnDismissListeners != null) { + mPopupWindow.setOnDismissListeners(mOnDismissListeners); + } + + mPopupWindow.setBackgroundDimAmount(mBackgroundDimAmount); + + for (int i = 0; mClickArray != null && i < mClickArray.size(); i++) { + mContentView.findViewById(mClickArray.keyAt(i)).setOnClickListener(new BasePopupWindow.ViewClickWrapper(mPopupWindow, mClickArray.valueAt(i))); + } + return mPopupWindow; + } + + /** + * 显示为下拉 + */ + public BasePopupWindow showAsDropDown(View anchor) { + if (!isCreated()) { + create(); + } + mPopupWindow.showAsDropDown(anchor, mXOffset, mYOffset, mGravity); + return mPopupWindow; + } + + /** + * 显示在指定位置 + */ + public BasePopupWindow showAtLocation(View parent) { + if (!isCreated()) { + create(); + } + mPopupWindow.showAtLocation(parent, mGravity, mXOffset, mYOffset); + return mPopupWindow; + } + + @Override + public Context getContext() { + return mContext; + } + + /** + * 当前 PopupWindow 是否创建了 + */ + public boolean isCreated() { + return mPopupWindow != null; + } + + /** + * 当前 PopupWindow 是否显示了 + */ + public boolean isShowing() { + return mPopupWindow != null && mPopupWindow.isShowing(); + } + + /** + * 销毁当前 PopupWindow + */ + public void dismiss() { + if (mPopupWindow != null) { + mPopupWindow.dismiss(); + } + } + + /** + * 创建 PopupWindow 对象(子类可以重写此方法来改变 PopupWindow 类型) + */ + protected BasePopupWindow createPopupWindow(Context context) { + return new BasePopupWindow(context); + } + + /** + * 获取 PopupWindow 的根布局 + */ + public View getContentView() { + return mContentView; + } + + /** + * 根据 id 查找 View + */ + @Override + public V findViewById(@IdRes int id) { + if (mContentView == null) { + // 没有 setContentView 就想 findViewById ? + throw new IllegalStateException("are you ok?"); + } + return mContentView.findViewById(id); + } + + /** + * 获取当前 PopupWindow 对象 + */ + @Nullable + public BasePopupWindow getPopupWindow() { + return mPopupWindow; + } + + /** + * 延迟执行 + */ + public final void post(Runnable r) { + if (isShowing()) { + mPopupWindow.post(r); + } else { + addOnShowListener(new ShowPostWrapper(r)); + } + } + + /** + * 延迟一段时间执行 + */ + public final void postDelayed(Runnable r, long delayMillis) { + if (isShowing()) { + mPopupWindow.postDelayed(r, delayMillis); + } else { + addOnShowListener(new ShowPostDelayedWrapper(r, delayMillis)); + } + } + + /** + * 在指定的时间执行 + */ + public final void postAtTime(Runnable r, long uptimeMillis) { + if (isShowing()) { + mPopupWindow.postAtTime(r, uptimeMillis); + } else { + addOnShowListener(new ShowPostAtTimeWrapper(r, uptimeMillis)); + } + } + } + + /** + * PopupWindow 背景遮盖层实现类 + */ + private static class PopupBackground implements + BasePopupWindow.OnShowListener, + BasePopupWindow.OnDismissListener { + + private float mAlpha; + + private void setAlpha(float alpha) { + mAlpha = alpha; + } + + @Override + public void onShow(BasePopupWindow popupWindow) { + popupWindow.setActivityAlpha(mAlpha); + } + + @Override + public void onDismiss(BasePopupWindow popupWindow) { + popupWindow.setActivityAlpha(1); + } + } + + /** + * 销毁监听包装类 + */ + private static final class DismissListenerWrapper + extends SoftReference + implements BasePopupWindow.OnDismissListener { + + private DismissListenerWrapper(PopupWindow.OnDismissListener referent) { + super(referent); + } + + @Override + public void onDismiss(BasePopupWindow popupWindow) { + // 在横竖屏切换后监听对象会为空 + if (get() != null) { + get().onDismiss(); + } + } + } + + /** + * 点击事件包装类 + */ + private static final class ViewClickWrapper + implements View.OnClickListener { + + private final BasePopupWindow mBasePopupWindow; + private final BasePopupWindow.OnClickListener mListener; + + private ViewClickWrapper(BasePopupWindow popupWindow, BasePopupWindow.OnClickListener listener) { + mBasePopupWindow = popupWindow; + mListener = listener; + } + + @SuppressWarnings("unchecked") + @Override + public final void onClick(View v) { + mListener.onClick(mBasePopupWindow, v); + } + } + + /** + * post 任务包装类 + */ + private static final class ShowPostWrapper implements OnShowListener { + + private final Runnable mRunnable; + + private ShowPostWrapper(Runnable r) { + mRunnable = r; + } + + @Override + public void onShow(BasePopupWindow dialog) { + if (mRunnable != null) { + dialog.removeOnShowListener(this); + dialog.post(mRunnable); + } + } + } + + /** + * postDelayed 任务包装类 + */ + private static final class ShowPostDelayedWrapper implements OnShowListener { + + private final Runnable mRunnable; + private final long mDelayMillis; + + private ShowPostDelayedWrapper(Runnable r, long delayMillis) { + mRunnable = r; + mDelayMillis = delayMillis; + } + + @Override + public void onShow(BasePopupWindow dialog) { + if (mRunnable != null) { + dialog.removeOnShowListener(this); + dialog.postDelayed(mRunnable, mDelayMillis); + } + } + } + + /** + * postAtTime 任务包装类 + */ + private static final class ShowPostAtTimeWrapper implements OnShowListener { + + private final Runnable mRunnable; + private final long mUptimeMillis; + + private ShowPostAtTimeWrapper(Runnable r, long uptimeMillis) { + mRunnable = r; + mUptimeMillis = uptimeMillis; + } + + @Override + public void onShow(BasePopupWindow dialog) { + if (mRunnable != null) { + dialog.removeOnShowListener(this); + dialog.postAtTime(mRunnable, mUptimeMillis); + } + } + } + + /** + * 点击监听器 + */ + public interface OnClickListener { + void onClick(BasePopupWindow popupWindow, V view); + } + + /** + * 显示监听器 + */ + public interface OnShowListener { + + /** + * PopupWindow 显示了 + */ + void onShow(BasePopupWindow popupWindow); + } + + /** + * 销毁监听器 + */ + public interface OnDismissListener { + + /** + * PopupWindow 销毁了 + */ + void onDismiss(BasePopupWindow popupWindow); + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/action/ActivityAction.java b/base/src/main/java/com/example/base/action/ActivityAction.java new file mode 100644 index 0000000..832865f --- /dev/null +++ b/base/src/main/java/com/example/base/action/ActivityAction.java @@ -0,0 +1,55 @@ +package com.example.base.action; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/03/08 + * desc : Activity 相关意图 + */ +public interface ActivityAction { + + /** + * 获取 Context + */ + Context getContext(); + + /** + * 获取 Activity + */ + default Activity getActivity() { + Context context = getContext(); + do { + if (context instanceof Activity) { + return (Activity) context; + } else if (context instanceof ContextWrapper){ + context = ((ContextWrapper) context).getBaseContext(); + } else { + return null; + } + } while (context != null); + return null; + } + + /** + * 启动一个 Activity(简化版) + */ + default void startActivity(Class clazz) { + startActivity(new Intent(getContext(), clazz)); + } + + /** + * 启动一个 Activity + */ + default void startActivity(Intent intent) { + if (!(getContext() instanceof Activity)) { + // 如果当前的上下文不是 Activity,调用 startActivity 必须加入新任务栈的标记 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + getContext().startActivity(intent); + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/action/AnimAction.java b/base/src/main/java/com/example/base/action/AnimAction.java new file mode 100644 index 0000000..3daa49c --- /dev/null +++ b/base/src/main/java/com/example/base/action/AnimAction.java @@ -0,0 +1,40 @@ +package com.example.base.action; + + +import com.example.base.R; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/21 + * desc : 动画样式 + */ +public interface AnimAction { + + /** 默认动画效果 */ + int ANIM_DEFAULT = -1; + + /** 没有动画效果 */ + int ANIM_EMPTY = 0; + + /** 缩放动画 */ + int ANIM_SCALE = R.style.ScaleAnimStyle; + + /** IOS 动画 */ + int ANIM_IOS = R.style.IOSAnimStyle; + + /** 吐司动画 */ + int ANIM_TOAST = android.R.style.Animation_Toast; + + /** 顶部弹出动画 */ + int ANIM_TOP = R.style.TopAnimStyle; + + /** 底部弹出动画 */ + int ANIM_BOTTOM = R.style.BottomAnimStyle; + + /** 左边弹出动画 */ + int ANIM_LEFT = R.style.LeftAnimStyle; + + /** 右边弹出动画 */ + int ANIM_RIGHT = R.style.RightAnimStyle; +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/action/BundleAction.java b/base/src/main/java/com/example/base/action/BundleAction.java new file mode 100644 index 0000000..139120d --- /dev/null +++ b/base/src/main/java/com/example/base/action/BundleAction.java @@ -0,0 +1,82 @@ +package com.example.base.action; + +import android.os.Bundle; +import android.os.Parcelable; + +import java.io.Serializable; +import java.util.ArrayList; + +import androidx.annotation.Nullable; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/10/23 + * desc : 参数意图 + */ +public interface BundleAction { + + @Nullable + Bundle getBundle(); + + default int getInt(String name) { + return getInt(name, 0); + } + + default int getInt(String name, int defaultValue) { + return getBundle() == null ? defaultValue : getBundle().getInt(name, defaultValue); + } + + default long getLong(String name) { + return getLong(name, 0); + } + + default long getLong(String name, int defaultValue) { + return getBundle() == null ? defaultValue : getBundle().getLong(name, defaultValue); + } + + default float getFloat(String name) { + return getFloat(name, 0); + } + + default float getFloat(String name, int defaultValue) { + return getBundle() == null ? defaultValue : getBundle().getFloat(name, defaultValue); + } + + default double getDouble(String name) { + return getDouble(name, 0); + } + + default double getDouble(String name, int defaultValue) { + return getBundle() == null ? defaultValue : getBundle().getDouble(name, defaultValue); + } + + default boolean getBoolean(String name) { + return getBoolean(name, false); + } + + default boolean getBoolean(String name, boolean defaultValue) { + return getBundle() == null ? defaultValue : getBundle().getBoolean(name, defaultValue); + } + + default String getString(String name) { + return getBundle() == null ? null : getBundle().getString(name); + } + + default

P getParcelable(String name) { + return getBundle() == null ? null : getBundle().getParcelable(name); + } + + @SuppressWarnings("unchecked") + default S getSerializable(String name) { + return (S) (getBundle() == null ? null : getBundle().getSerializable(name)); + } + + default ArrayList getStringArrayList(String name) { + return getBundle() == null ? null : getBundle().getStringArrayList(name); + } + + default ArrayList getIntegerArrayList(String name) { + return getBundle() == null ? null : getBundle().getIntegerArrayList(name); + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/action/ClickAction.java b/base/src/main/java/com/example/base/action/ClickAction.java new file mode 100644 index 0000000..67e738e --- /dev/null +++ b/base/src/main/java/com/example/base/action/ClickAction.java @@ -0,0 +1,33 @@ +package com.example.base.action; + +import android.view.View; + +import androidx.annotation.IdRes; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/15 + * desc : 点击事件意图 + */ +public interface ClickAction extends View.OnClickListener { + + V findViewById(@IdRes int id); + + @Override + default void onClick(View v) { + // 默认不实现,让子类实现 + } + + default void setOnClickListener(@IdRes int... ids) { + for (int id : ids) { + findViewById(id).setOnClickListener(this); + } + } + + default void setOnClickListener(View... views) { + for (View view : views) { + view.setOnClickListener(this); + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/action/HandlerAction.java b/base/src/main/java/com/example/base/action/HandlerAction.java new file mode 100644 index 0000000..000b867 --- /dev/null +++ b/base/src/main/java/com/example/base/action/HandlerAction.java @@ -0,0 +1,62 @@ +package com.example.base.action; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/15 + * desc : Handler 意图处理 + */ +public interface HandlerAction { + + Handler HANDLER = new Handler(Looper.getMainLooper()); + + /** + * 获取 Handler + */ + default Handler getHandler() { + return HANDLER; + } + + /** + * 延迟执行 + */ + default boolean post(Runnable r) { + return postDelayed(r, 0); + } + + /** + * 延迟一段时间执行 + */ + default boolean postDelayed(Runnable r, long delayMillis) { + if (delayMillis < 0) { + delayMillis = 0; + } + return postAtTime(r, SystemClock.uptimeMillis() + delayMillis); + } + + /** + * 在指定的时间执行 + */ + default boolean postAtTime(Runnable r, long uptimeMillis) { + // 发送和这个 Activity 相关的消息回调 + return HANDLER.postAtTime(r, this, uptimeMillis); + } + + /** + * 移除单个消息回调 + */ + default void removeCallbacks(Runnable r) { + HANDLER.removeCallbacks(r); + } + + /** + * 移除全部消息回调 + */ + default void removeCallbacks() { + HANDLER.removeCallbacksAndMessages(this); + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/action/ResourcesAction.java b/base/src/main/java/com/example/base/action/ResourcesAction.java new file mode 100644 index 0000000..2c87c0c --- /dev/null +++ b/base/src/main/java/com/example/base/action/ResourcesAction.java @@ -0,0 +1,66 @@ +package com.example.base.action; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/15 + * desc : Context 意图处理(扩展非 Context 类的方法,禁止 Context 类实现此接口) + */ +public interface ResourcesAction { + + /** + * 获取 Context + */ + Context getContext(); + + /** + * 获取资源对象(仅供子类调用) + */ + default Resources getResources() { + return getContext().getResources(); + } + + /** + * 根据 id 获取一个文本 + */ + default String getString(@StringRes int id) { + return getContext().getString(id); + } + + default String getString(@StringRes int id, Object... formatArgs) { + return getResources().getString(id, formatArgs); + } + + /** + * 根据 id 获取一个 Drawable + */ + default Drawable getDrawable(@DrawableRes int id) { + return ContextCompat.getDrawable(getContext(), id); + } + + /** + * 根据 id 获取一个颜色 + */ + @ColorInt + default int getColor(@ColorRes int id) { + return ContextCompat.getColor(getContext(), id); + } + + /** + * 获取系统服务 + */ + default S getSystemService(@NonNull Class serviceClass) { + return ContextCompat.getSystemService(getContext(), serviceClass); + } +} \ No newline at end of file diff --git a/base/src/main/res/anim/bottom_in_window.xml b/base/src/main/res/anim/bottom_in_window.xml new file mode 100644 index 0000000..416cc3e --- /dev/null +++ b/base/src/main/res/anim/bottom_in_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/bottom_out_window.xml b/base/src/main/res/anim/bottom_out_window.xml new file mode 100644 index 0000000..b62f8c5 --- /dev/null +++ b/base/src/main/res/anim/bottom_out_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/fall_down_item.xml b/base/src/main/res/anim/fall_down_item.xml new file mode 100644 index 0000000..a13e21e --- /dev/null +++ b/base/src/main/res/anim/fall_down_item.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/fall_down_layout.xml b/base/src/main/res/anim/fall_down_layout.xml new file mode 100644 index 0000000..32cb14b --- /dev/null +++ b/base/src/main/res/anim/fall_down_layout.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/base/src/main/res/anim/from_bottom_item.xml b/base/src/main/res/anim/from_bottom_item.xml new file mode 100644 index 0000000..f6563f1 --- /dev/null +++ b/base/src/main/res/anim/from_bottom_item.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/from_bottom_layout.xml b/base/src/main/res/anim/from_bottom_layout.xml new file mode 100644 index 0000000..8b905e7 --- /dev/null +++ b/base/src/main/res/anim/from_bottom_layout.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/base/src/main/res/anim/from_right_item.xml b/base/src/main/res/anim/from_right_item.xml new file mode 100644 index 0000000..9fdc6b4 --- /dev/null +++ b/base/src/main/res/anim/from_right_item.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/from_right_layout.xml b/base/src/main/res/anim/from_right_layout.xml new file mode 100644 index 0000000..02ef45c --- /dev/null +++ b/base/src/main/res/anim/from_right_layout.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/base/src/main/res/anim/ios_in_window.xml b/base/src/main/res/anim/ios_in_window.xml new file mode 100644 index 0000000..3aae57e --- /dev/null +++ b/base/src/main/res/anim/ios_in_window.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/ios_out_window.xml b/base/src/main/res/anim/ios_out_window.xml new file mode 100644 index 0000000..4333adc --- /dev/null +++ b/base/src/main/res/anim/ios_out_window.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/left_in_window.xml b/base/src/main/res/anim/left_in_window.xml new file mode 100644 index 0000000..42aa534 --- /dev/null +++ b/base/src/main/res/anim/left_in_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/left_out_window.xml b/base/src/main/res/anim/left_out_window.xml new file mode 100644 index 0000000..a646ba9 --- /dev/null +++ b/base/src/main/res/anim/left_out_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/right_in_window.xml b/base/src/main/res/anim/right_in_window.xml new file mode 100644 index 0000000..5b07f5a --- /dev/null +++ b/base/src/main/res/anim/right_in_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/right_out_window.xml b/base/src/main/res/anim/right_out_window.xml new file mode 100644 index 0000000..ed38c36 --- /dev/null +++ b/base/src/main/res/anim/right_out_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/scale_in_window.xml b/base/src/main/res/anim/scale_in_window.xml new file mode 100644 index 0000000..ef519b3 --- /dev/null +++ b/base/src/main/res/anim/scale_in_window.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/scale_out_window.xml b/base/src/main/res/anim/scale_out_window.xml new file mode 100644 index 0000000..005c749 --- /dev/null +++ b/base/src/main/res/anim/scale_out_window.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/top_in_window.xml b/base/src/main/res/anim/top_in_window.xml new file mode 100644 index 0000000..23a7998 --- /dev/null +++ b/base/src/main/res/anim/top_in_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/anim/top_out_window.xml b/base/src/main/res/anim/top_out_window.xml new file mode 100644 index 0000000..1d5bd0b --- /dev/null +++ b/base/src/main/res/anim/top_out_window.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/values/colors.xml b/base/src/main/res/values/colors.xml new file mode 100644 index 0000000..14140de --- /dev/null +++ b/base/src/main/res/values/colors.xml @@ -0,0 +1,70 @@ + + + + + #00000000 + + + #FFFFFFFF + #F2FFFFFF + #E6FFFFFF + #D9FFFFFF + #CCFFFFFF + #BFFFFFFF + #B3FFFFFF + #A6FFFFFF + #99FFFFFF + #8CFFFFFF + #80FFFFFF + #73FFFFFF + #66FFFFFF + #59FFFFFF + #4DFFFFFF + #40FFFFFF + #33FFFFFF + #26FFFFFF + #1AFFFFFF + #0DFFFFFF + + + #FF000000 + #F2000000 + #E6000000 + #D9000000 + #CC000000 + #B000000F + #B3000000 + #A6000000 + #99000000 + #8C000000 + #80000000 + #73000000 + #66000000 + #59000000 + #4D000000 + #40000000 + #33000000 + #26000000 + #1A000000 + #0D000000 + + + #FF808080 + + #FFFF0000 + + #FFFFD700 + + #FFFFFF00 + + #FF008000 + + #FF0000FF + + #FF800080 + + #FFFFC0CB + + #FFFFA500 + + \ No newline at end of file diff --git a/base/src/main/res/values/integers.xml b/base/src/main/res/values/integers.xml new file mode 100644 index 0000000..3616732 --- /dev/null +++ b/base/src/main/res/values/integers.xml @@ -0,0 +1,5 @@ + + + 300 + 400 + \ No newline at end of file diff --git a/base/src/main/res/values/styles.xml b/base/src/main/res/values/styles.xml new file mode 100644 index 0000000..dcb3781 --- /dev/null +++ b/base/src/main/res/values/styles.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/test/java/com/example/base/ExampleUnitTest.java b/base/src/test/java/com/example/base/ExampleUnitTest.java new file mode 100644 index 0000000..f6c4d6f --- /dev/null +++ b/base/src/test/java/com/example/base/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.base; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..ad6d153 --- /dev/null +++ b/build.gradle @@ -0,0 +1,38 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { + url 'https://maven.google.com/' + name 'Google' + } + maven { url "https://oss.jfrog.org/libs-snapshot" } + } + dependencies { + classpath "com.android.tools.build:gradle:4.0.1" + classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' // add plugin + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { + url 'https://maven.google.com/' + name 'Google' + } + maven { url "https://oss.jfrog.org/libs-snapshot" } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/config.gradle b/config.gradle new file mode 100644 index 0000000..bd9ad68 --- /dev/null +++ b/config.gradle @@ -0,0 +1,43 @@ +// 通用配置 +android { + + compileSdkVersion 29 + defaultConfig { + minSdkVersion 24 + targetSdkVersion 29 + versionName '1.0' + versionCode 10 + } + + // 支持 Java JDK 8 + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + checkReleaseBuilds false + abortOnError false + } + + // 设置存放 so 文件的目录 + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } +} + +dependencies { + // 依赖 libs 目录下所有 jar 包 + implementation fileTree(include: ['*.jar'], dir: 'libs') + // 依赖 libs 目录下所有 aar 包 + implementation fileTree(include: ['*.aar'], dir: 'libs') + + // 谷歌兼容库:https://developer.android.google.cn/jetpack/androidx/releases/appcompat?hl=zh-cn + implementation 'androidx.appcompat:appcompat:1.3.0-alpha01' + implementation 'com.google.android.material:material:1.3.0-alpha01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.2' + + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c52ac9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3a5f9ed --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Nov 13 10:29:28 CST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..7ce3b55 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +include ':widget' +include ':base' +include ':app' +rootProject.name = "Alcoholic" \ No newline at end of file diff --git a/up_load_txt.json b/up_load_txt.json new file mode 100644 index 0000000..1cc02f3 --- /dev/null +++ b/up_load_txt.json @@ -0,0 +1,7 @@ +{ + "minMustUpCode":1, + "vAPKDownUrl":"https://github.com/ymwm-lxl/Alcoholic/raw/master/app/release/Alcoholic_v1.0_release_1123.apk", + "vCode":2, + "vName":"2.0", + "vUpContent":"更新的一个版本" +} \ No newline at end of file diff --git a/widget/.gitignore b/widget/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/widget/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/widget/build.gradle b/widget/build.gradle new file mode 100644 index 0000000..376897c --- /dev/null +++ b/widget/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'com.android.library' +apply from: '../config.gradle' + +dependencies { + // 基础库(不包任何第三方框架) + implementation project(':base') +} \ No newline at end of file diff --git a/widget/consumer-rules.pro b/widget/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/widget/proguard-rules.pro b/widget/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/widget/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/widget/src/androidTest/java/com/example/widget/ExampleInstrumentedTest.java b/widget/src/androidTest/java/com/example/widget/ExampleInstrumentedTest.java new file mode 100644 index 0000000..a471e66 --- /dev/null +++ b/widget/src/androidTest/java/com/example/widget/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.widget; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.widget.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/widget/src/main/AndroidManifest.xml b/widget/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6466e2b --- /dev/null +++ b/widget/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/layout/CustomViewStub.java b/widget/src/main/java/com/example/widget/layout/CustomViewStub.java new file mode 100644 index 0000000..8779ef3 --- /dev/null +++ b/widget/src/main/java/com/example/widget/layout/CustomViewStub.java @@ -0,0 +1,117 @@ +package com.example.widget.layout; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; + +import com.example.widget.R; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/07/06 + * desc : 自定义 ViewStub(原生 ViewStub 的缺点:继承至 View,不支持 findViewById、动态添加和移除 View、监听显示隐藏) + */ +public final class CustomViewStub extends FrameLayout { + + private OnViewStubListener mListener; + + private final int mLayoutResource; + + private View mInflateView; + + public CustomViewStub(Context context) { + this(context, null); + } + + public CustomViewStub(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomViewStub(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CustomViewStub(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStub); + mLayoutResource = array.getResourceId(R.styleable.CustomViewStub_android_layout, 0); + array.recycle(); + + // 隐藏自己 + setVisibility(GONE); + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (mInflateView == null && visibility != GONE) { + + mInflateView = LayoutInflater.from(getContext()).inflate(mLayoutResource, this, false); + LayoutParams layoutParams = (LayoutParams) mInflateView.getLayoutParams(); + layoutParams.width = getLayoutParams().width; + layoutParams.height = getLayoutParams().height; + if (layoutParams.gravity == LayoutParams.UNSPECIFIED_GRAVITY) { + layoutParams.gravity = Gravity.CENTER; + } + mInflateView.setLayoutParams(layoutParams); + addView(mInflateView); + + if (mListener != null) { + mListener.onInflate(this, mInflateView); + } + } + + if (mListener != null) { + mListener.onVisibility(this, visibility); + } + } + + /** + * 设置显示状态(避免 setVisibility 导致的无限递归) + */ + public void setCustomVisibility(int visibility) { + super.setVisibility(visibility); + } + + /** + * 获取填充的 View + */ + public View getInflateView() { + return mInflateView; + } + + /** + * 设置监听器 + */ + public void setOnViewStubListener(OnViewStubListener listener) { + mListener = listener; + } + + public interface OnViewStubListener { + + /** + * 布局填充回调(可在此中做 View 初始化) + * + * @param stub 当前 ViewStub 对象 + * @param inflatedView 填充布局对象 + */ + void onInflate(CustomViewStub stub, View inflatedView); + + /** + * 可见状态改变(可在此中做 View 更新) + * + * @param stub 当前 ViewStub 对象 + * @param visibility 可见状态参数改变 + */ + void onVisibility(CustomViewStub stub, int visibility); + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/layout/NoScrollViewPager.java b/widget/src/main/java/com/example/widget/layout/NoScrollViewPager.java new file mode 100644 index 0000000..72b9f8c --- /dev/null +++ b/widget/src/main/java/com/example/widget/layout/NoScrollViewPager.java @@ -0,0 +1,69 @@ +package com.example.widget.layout; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 禁用水平滑动的ViewPager(一般用于 APP 主页的 ViewPager + Fragment) + */ +public final class NoScrollViewPager extends ViewPager { + + public NoScrollViewPager(Context context) { + super(context); + } + + public NoScrollViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // 不拦截这个事件 + return false; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + // 不处理这个事件 + return false; + } + + @Override + public boolean executeKeyEvent(@NonNull KeyEvent event) { + // 不响应按键事件 + return false; + } + + @Override + public void setCurrentItem(int item) { + boolean smoothScroll; + int currentItem = getCurrentItem(); + if (currentItem == 0) { + // 如果当前是第一页,只有第二页才会有动画 + smoothScroll = item == currentItem + 1; + } else if (currentItem == getCount() - 1) { + // 如果当前是最后一页,只有最后第二页才会有动画 + smoothScroll = item == currentItem - 1; + } else { + // 如果当前是中间页,只有相邻页才会有动画 + smoothScroll = Math.abs(currentItem - item) == 1; + } + super.setCurrentItem(item, smoothScroll); + } + + public int getCount() { + PagerAdapter adapter = getAdapter(); + return adapter != null ? adapter.getCount() : 0; + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/layout/RatioFrameLayout.java b/widget/src/main/java/com/example/widget/layout/RatioFrameLayout.java new file mode 100644 index 0000000..de123a4 --- /dev/null +++ b/widget/src/main/java/com/example/widget/layout/RatioFrameLayout.java @@ -0,0 +1,72 @@ +package com.example.widget.layout; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.example.widget.R; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/08/23 + * desc : 按照比例显示的 FrameLayout + */ +public final class RatioFrameLayout extends FrameLayout { + + /** 宽高比 */ + private final float mSizeRatio; + + public RatioFrameLayout(Context context) { + this(context, null); + } + + public RatioFrameLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatioFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RatioFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RatioFrameLayout); + mSizeRatio = array.getFloat(R.styleable.RatioFrameLayout_sizeRatio, 0); + array.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mSizeRatio != 0) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { + // 如果当前宽度和高度都是写死的 + if (widthSpecSize / mSizeRatio <= heightSpecSize) { + // 如果宽度经过比例换算不超过原有的高度 + heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (widthSpecSize / mSizeRatio), MeasureSpec.EXACTLY); + } else if (heightSpecSize * mSizeRatio <= widthSpecSize) { + // 如果高度经过比例换算不超过原有的宽度 + widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (heightSpecSize * mSizeRatio), MeasureSpec.EXACTLY); + } + } else if (widthSpecMode == MeasureSpec.EXACTLY) { + // 如果当前宽度是写死的,但是高度不写死 + heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (widthSpecSize / mSizeRatio), MeasureSpec.EXACTLY); + } else if (heightSpecMode == MeasureSpec.EXACTLY) { + // 如果当前高度是写死的,但是宽度不写死 + widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (heightSpecSize * mSizeRatio), MeasureSpec.EXACTLY); + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/layout/SettingBar.java b/widget/src/main/java/com/example/widget/layout/SettingBar.java new file mode 100644 index 0000000..a43eb11 --- /dev/null +++ b/widget/src/main/java/com/example/widget/layout/SettingBar.java @@ -0,0 +1,350 @@ +package com.example.widget.layout; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.example.widget.R; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/01/23 + * desc : 设置条自定义控件 + */ +public final class SettingBar extends FrameLayout { + + private final LinearLayout mMainLayout; + private final TextView mLeftView; + private final TextView mRightView; + private final View mLineView; + + public SettingBar(Context context) { + this(context, null); + } + + public SettingBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SettingBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SettingBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + mMainLayout = new LinearLayout(getContext()); + mLeftView = new TextView(getContext()); + mRightView = new TextView(getContext()); + mLineView = new View(getContext()); + + mRightView.setGravity(Gravity.END | Gravity.CENTER_VERTICAL); + + mLeftView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mLeftView.getLineSpacingMultiplier()); + mRightView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mRightView.getLineSpacingMultiplier()); + + mLeftView.setPaddingRelative((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics())); + mRightView.setPaddingRelative((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics())); + + mLeftView.setCompoundDrawablePadding((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics())); + mRightView.setCompoundDrawablePadding((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics())); + + LinearLayout.LayoutParams leftParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + leftParams.gravity = Gravity.CENTER_VERTICAL; + mMainLayout.addView(mLeftView, leftParams); + + LinearLayout.LayoutParams rightParams = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT); + rightParams.gravity = Gravity.CENTER_VERTICAL; + rightParams.weight = 1; + mMainLayout.addView(mRightView, rightParams); + + addView(mMainLayout, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_VERTICAL)); + addView(mLineView, 1, new LayoutParams(LayoutParams.MATCH_PARENT, 1, Gravity.BOTTOM)); + + final TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.SettingBar); + + // 文本设置 + if (array.hasValue(R.styleable.SettingBar_bar_leftText)) { + setLeftText(array.getString(R.styleable.SettingBar_bar_leftText)); + } + + if (array.hasValue(R.styleable.SettingBar_bar_rightText)) { + setRightText(array.getString(R.styleable.SettingBar_bar_rightText)); + } + + // 提示设置 + if (array.hasValue(R.styleable.SettingBar_bar_leftHint)) { + setLeftHint(array.getString(R.styleable.SettingBar_bar_leftHint)); + } + + if (array.hasValue(R.styleable.SettingBar_bar_rightHint)) { + setRightHint(array.getString(R.styleable.SettingBar_bar_rightHint)); + } + + // 图标设置 + if (array.hasValue(R.styleable.SettingBar_bar_leftIcon)) { + setLeftIcon(array.getDrawable(R.styleable.SettingBar_bar_leftIcon)); + } + + if (array.hasValue(R.styleable.SettingBar_bar_rightIcon)) { + setRightIcon(array.getDrawable(R.styleable.SettingBar_bar_rightIcon)); + } + + // 文字颜色设置 + setLeftColor(array.getColor(R.styleable.SettingBar_bar_leftColor, ContextCompat.getColor(getContext(), R.color.black80))); + setRightColor(array.getColor(R.styleable.SettingBar_bar_rightColor, ContextCompat.getColor(getContext(), R.color.black60))); + + // 文字大小设置 + setLeftSize(TypedValue.COMPLEX_UNIT_SP, array.getDimensionPixelSize(R.styleable.SettingBar_bar_leftSize, 15)); + setRightSize(TypedValue.COMPLEX_UNIT_SP, array.getDimensionPixelSize(R.styleable.SettingBar_bar_rightSize, 14)); + + // 分割线设置 + if (array.hasValue(R.styleable.SettingBar_bar_lineColor)) { + setLineDrawable(array.getDrawable(R.styleable.SettingBar_bar_lineColor)); + } else { + setLineDrawable(new ColorDrawable(0xFFECECEC)); + } + + if (array.hasValue(R.styleable.SettingBar_bar_lineVisible)) { + setLineVisible(array.getBoolean(R.styleable.SettingBar_bar_lineVisible, true)); + } + + if (array.hasValue(R.styleable.SettingBar_bar_lineSize)) { + setLineSize(array.getDimensionPixelSize(R.styleable.SettingBar_bar_lineSize, 0)); + } + + if (array.hasValue(R.styleable.SettingBar_bar_lineMargin)) { + setLineMargin(array.getDimensionPixelSize(R.styleable.SettingBar_bar_lineMargin, 0)); + } + + if (getBackground() == null) { + StateListDrawable drawable = new StateListDrawable(); + drawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.black5))); + drawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.black5))); + drawable.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.black5))); + drawable.addState(new int[]{}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.white))); + setBackground(drawable); + + // 必须要设置可点击,否则点击屏幕任何角落都会触发按压事件 + setFocusable(true); + setClickable(true); + } + + array.recycle(); + } + + /** + * 设置左边的标题 + */ + public SettingBar setLeftText(@StringRes int id) { + return setLeftText(getResources().getString(id)); + } + + public SettingBar setLeftText(CharSequence text) { + mLeftView.setText(text); + return this; + } + + public CharSequence getLeftText() { + return mLeftView.getText(); + } + + /** + * 设置左边的提示 + */ + public SettingBar setLeftHint(@StringRes int id) { + return setLeftHint(getResources().getString(id)); + } + + public SettingBar setLeftHint(CharSequence hint) { + mLeftView.setHint(hint); + return this; + } + + /** + * 设置右边的标题 + */ + public SettingBar setRightText(@StringRes int id) { + setRightText(getResources().getString(id)); + return this; + } + + public SettingBar setRightText(CharSequence text) { + mRightView.setText(text); + return this; + } + + public CharSequence getRightText() { + return mRightView.getText(); + } + + /** + * 设置右边的提示 + */ + public SettingBar setRightHint(@StringRes int id) { + return setRightHint(getResources().getString(id)); + } + + public SettingBar setRightHint(CharSequence hint) { + mRightView.setHint(hint); + return this; + } + + /** + * 设置左边的图标 + */ + public SettingBar setLeftIcon(@DrawableRes int id) { + setLeftIcon(ContextCompat.getDrawable(getContext(), id)); + return this; + } + + public SettingBar setLeftIcon(Drawable drawable) { + mLeftView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); + return this; + } + + public Drawable getLeftIcon() { + return mLeftView.getCompoundDrawables()[0]; + } + + /** + * 设置右边的图标 + */ + public SettingBar setRightIcon(@DrawableRes int id) { + setRightIcon(ContextCompat.getDrawable(getContext(), id)); + return this; + } + + public SettingBar setRightIcon(Drawable drawable) { + mRightView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); + return this; + } + + public Drawable getRightIcon() { + return mRightView.getCompoundDrawables()[2]; + } + + /** + * 设置左标题颜色 + */ + public SettingBar setLeftColor(@ColorInt int color) { + mLeftView.setTextColor(color); + return this; + } + + /** + * 设置右标题颜色 + */ + public SettingBar setRightColor(@ColorInt int color) { + mRightView.setTextColor(color); + return this; + } + + /** + * 设置左标题的文本大小 + */ + public SettingBar setLeftSize(int unit, float size) { + mLeftView.setTextSize(unit, size); + return this; + } + + /** + * 设置右标题的文本大小 + */ + public SettingBar setRightSize(int unit, float size) { + mRightView.setTextSize(unit, size); + return this; + } + + /** + * 设置分割线是否显示 + */ + public SettingBar setLineVisible(boolean visible) { + mLineView.setVisibility(visible ? VISIBLE : GONE); + return this; + } + + /** + * 设置分割线的颜色 + */ + public SettingBar setLineColor(@ColorInt int color) { + return setLineDrawable(new ColorDrawable(color)); + } + public SettingBar setLineDrawable(Drawable drawable) { + mLineView.setBackground(drawable); + return this; + } + + /** + * 设置分割线的大小 + */ + public SettingBar setLineSize(int size) { + ViewGroup.LayoutParams layoutParams = mLineView.getLayoutParams(); + layoutParams.height = size; + mLineView.setLayoutParams(layoutParams); + return this; + } + + /** + * 设置分割线边界 + */ + public SettingBar setLineMargin(int margin) { + LayoutParams params = (LayoutParams) mLineView.getLayoutParams(); + params.leftMargin = margin; + params.rightMargin = margin; + mLineView.setLayoutParams(params); + return this; + } + + /** + * 获取主布局 + */ + public LinearLayout getMainLayout() { + return mMainLayout; + } + + /** + * 获取左标题 + */ + public TextView getLeftView() { + return mLeftView; + } + + /** + * 获取右标题 + */ + public TextView getRightView() { + return mRightView; + } + + /** + * 获取分割线 + */ + public View getLineView() { + return mLineView; + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/layout/SimpleLayout.java b/widget/src/main/java/com/example/widget/layout/SimpleLayout.java new file mode 100644 index 0000000..3bf317a --- /dev/null +++ b/widget/src/main/java/com/example/widget/layout/SimpleLayout.java @@ -0,0 +1,104 @@ +package com.example.widget.layout; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 简单的 Layout(常用于自定义组合控件继承的基类,可以起到性能优化的作用) + */ +public class SimpleLayout extends ViewGroup { + + public SimpleLayout(Context context) { + super(context); + } + + public SimpleLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr, 0); + } + + public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int count = getChildCount(); + int maxHeight = 0; + int maxWidth = 0; + int childState = 0; + + // 测量子 View + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + // 被测量的子 View 不能是隐藏的 + if (child.getVisibility() != GONE) { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + final MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); + maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + params.leftMargin + params.rightMargin); + maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + params.topMargin + params.bottomMargin); + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + } + + maxWidth += (getPaddingLeft() + getPaddingRight()); + maxHeight += (getPaddingTop() + getPaddingBottom()); + + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + + // 测量自身 + setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), + resolveSizeAndState(maxHeight, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // 遍历子 View + int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); + int left = params.leftMargin + getPaddingLeft(); + int top = params.topMargin + getPaddingTop(); + int right = left + child.getMeasuredWidth() + getPaddingRight() + params.rightMargin; + int bottom = top + child.getMeasuredHeight() + getPaddingBottom() + params.bottomMargin; + // 将子 View 放置到左上角的位置 + child.layout(left, top, right, bottom); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams params) { + return new MarginLayoutParams(params); + } + + @Override + protected boolean checkLayoutParams(LayoutParams params) { + return params instanceof MarginLayoutParams; + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/layout/WrapRecyclerView.java b/widget/src/main/java/com/example/widget/layout/WrapRecyclerView.java new file mode 100644 index 0000000..32483fd --- /dev/null +++ b/widget/src/main/java/com/example/widget/layout/WrapRecyclerView.java @@ -0,0 +1,468 @@ +package com.example.widget.layout; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/09/21 + * desc : 支持添加底部和头部的 RecyclerView + */ +public final class WrapRecyclerView extends RecyclerView { + + /** 原有的适配器 */ + private RecyclerView.Adapter mRealAdapter; + + /** 支持添加头部和底部的适配器 */ + private final WrapRecyclerAdapter mWrapAdapter = new WrapRecyclerAdapter(); + + public WrapRecyclerView(Context context) { + super(context); + } + + public WrapRecyclerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public WrapRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setAdapter(Adapter adapter) { + mRealAdapter = adapter; + // 偷梁换柱 + mWrapAdapter.setRealAdapter(mRealAdapter); + // 禁用条目动画 + setItemAnimator(null); + super.setAdapter(mWrapAdapter); + } + + @Override + public Adapter getAdapter() { + return mRealAdapter; + } + + /** + * 添加头部View + */ + public void addHeaderView(View view) { + mWrapAdapter.addHeaderView(view); + } + + @SuppressWarnings("unchecked") + public V addHeaderView(@LayoutRes int id) { + View headerView = LayoutInflater.from(getContext()).inflate(id, this, false); + addHeaderView(headerView); + return (V) headerView; + } + + /** + * 移除头部View + */ + public void removeHeaderView(View view) { + mWrapAdapter.removeHeaderView(view); + } + + /** + * 添加底部View + */ + public void addFooterView(View view) { + mWrapAdapter.addFooterView(view); + } + + @SuppressWarnings("unchecked") + public V addFooterView(@LayoutRes int id) { + View footerView = LayoutInflater.from(getContext()).inflate(id, this, false); + addFooterView(footerView); + return (V) footerView; + } + + /** + * 移除底部View + */ + public void removeFooterView(View view) { + mWrapAdapter.removeFooterView(view); + } + + /** + * 获取头部View总数 + */ + public int getHeaderViewsCount() { + return mWrapAdapter.getHeaderViewsCount(); + } + + /** + * 获取底部View总数 + */ + public int getFooterViewsCount() { + return mWrapAdapter.getFooterViewsCount(); + } + + /** + * 获取头部View集合 + */ + public List getHeaderViews() { + return mWrapAdapter.getHeaderViews(); + } + + /** + * 获取底部View集合 + */ + public List getFooterViews() { + return mWrapAdapter.getFooterViews(); + } + + /** + * 刷新头部和底部布局所有的 View 的状态 + */ + public void refreshHeaderFooterViews() { + mWrapAdapter.notifyDataSetChanged(); + } + + /** + * 设置在 GridLayoutManager 模式下头部和尾部都是独占一行的效果 + */ + public void adjustSpanSize() { + + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + + @Override + public int getSpanSize(int position) { + return (position < mWrapAdapter.getHeaderViewsCount() + || position >= mWrapAdapter.getHeaderViewsCount() + (mRealAdapter == null ? 0 : mRealAdapter.getItemCount())) + ? ((GridLayoutManager) layoutManager).getSpanCount() : 1; + } + }); + } + } + + /** + * 采用装饰设计模式,将原有的适配器包装起来 + */ + private static final class WrapRecyclerAdapter extends RecyclerView.Adapter { + + /** 头部条目类型 */ + private static final int HEADER_VIEW_TYPE = Integer.MIN_VALUE >> 1; + /** 底部条目类型 */ + private static final int FOOTER_VIEW_TYPE = Integer.MAX_VALUE >> 1; + + /** 原有的适配器 */ + private RecyclerView.Adapter mRealAdapter; + /** 头部View集合 */ + private final List mHeaderViews = new ArrayList<>(); + /** 底部View集合 */ + private final List mFooterViews = new ArrayList<>(); + /** 当前调用的位置 */ + private int mCurrentPosition; + + /** RecyclerView对象 */ + private RecyclerView mRecyclerView; + + /** 数据观察者对象 */ + private WrapAdapterDataObserver mObserver; + + private void setRealAdapter(RecyclerView.Adapter adapter) { + if (mRealAdapter != adapter) { + + if (mRealAdapter != null) { + if (mObserver != null) { + // 为原有的RecyclerAdapter移除数据监听对象 + mRealAdapter.unregisterAdapterDataObserver(mObserver); + } + } + + mRealAdapter = adapter; + if (mRealAdapter != null) { + if (mObserver == null) { + mObserver = new WrapAdapterDataObserver(this); + } + // 为原有的RecyclerAdapter添加数据监听对象 + mRealAdapter.registerAdapterDataObserver(mObserver); + // 适配器不是第一次被绑定到RecyclerView上需要发送通知,因为第一次绑定会自动通知 + if (mRecyclerView != null) { + notifyDataSetChanged(); + } + } + } + } + + @Override + public int getItemCount() { + if (mRealAdapter != null) { + return getHeaderViewsCount() + mRealAdapter.getItemCount() + getFooterViewsCount(); + } else { + return getHeaderViewsCount() + getFooterViewsCount(); + } + } + + @SuppressWarnings("all") + @Override + public int getItemViewType(int position) { + mCurrentPosition = position; + // 获取头部布局的总数 + int headerCount = getHeaderViewsCount(); + // 获取原有适配器的总数 + int adapterCount = mRealAdapter != null ? mRealAdapter.getItemCount() : 0; + // 获取在原有适配器上的位置 + int adjPosition = position - headerCount; + if (position < headerCount) { + return HEADER_VIEW_TYPE; + } else if (adjPosition < adapterCount) { + return mRealAdapter.getItemViewType(adjPosition); + } else { + return FOOTER_VIEW_TYPE; + } + } + + public int getPosition() { + return mCurrentPosition; + } + + @SuppressWarnings("all") + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case HEADER_VIEW_TYPE: + return newWrapViewHolder(mHeaderViews.get(getPosition())); + case FOOTER_VIEW_TYPE: + return newWrapViewHolder(mFooterViews.get(getPosition() - getHeaderViewsCount() - (mRealAdapter != null ? mRealAdapter.getItemCount() : 0))); + default: + int itemViewType = mRealAdapter.getItemViewType(getPosition() - getHeaderViewsCount()); + if (itemViewType == HEADER_VIEW_TYPE || itemViewType == FOOTER_VIEW_TYPE) { + throw new IllegalStateException("Please do not use this type as itemType"); + } + if (mRealAdapter != null) { + return mRealAdapter.onCreateViewHolder(parent, itemViewType); + } else { + return null; + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + int viewType = getItemViewType(position); + switch (viewType) { + case HEADER_VIEW_TYPE: + case FOOTER_VIEW_TYPE: + break; + default: + if (mRealAdapter != null) { + mRealAdapter.onBindViewHolder(holder, getPosition() - getHeaderViewsCount()); + } + break; + } + } + + private WrapViewHolder newWrapViewHolder(View view) { + ViewParent parent = view.getParent(); + if (parent instanceof ViewGroup) { + // IllegalStateException: ViewHolder views must not be attached when created. + // Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot) + ((ViewGroup) parent).removeView(view); + } + return new WrapViewHolder(view); + } + + @Override + public long getItemId(int position) { + if (mRealAdapter != null && position > getHeaderViewsCount() - 1 && position < getHeaderViewsCount() + mRealAdapter.getItemCount()) { + return mRealAdapter.getItemId(position - getHeaderViewsCount()); + } else { + return super.getItemId(position); + } + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + mRecyclerView = recyclerView; + if (mRealAdapter != null) { + mRealAdapter.onAttachedToRecyclerView(recyclerView); + } + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + mRecyclerView = null; + if (mRealAdapter != null) { + mRealAdapter.onDetachedFromRecyclerView(recyclerView); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onViewRecycled(@NonNull ViewHolder holder) { + if (holder instanceof WrapViewHolder) { + // 防止这个 ViewHolder 被 RecyclerView 拿去复用 + holder.setIsRecyclable(false); + return; + } + if (mRealAdapter != null) { + mRealAdapter.onViewRecycled(holder); + } + } + + @SuppressWarnings("unchecked") + @Override + public boolean onFailedToRecycleView(@NonNull ViewHolder holder) { + if (mRealAdapter != null) { + return mRealAdapter.onFailedToRecycleView(holder); + } + return super.onFailedToRecycleView(holder); + } + + @SuppressWarnings("unchecked") + @Override + public void onViewAttachedToWindow(@NonNull ViewHolder holder) { + if (mRealAdapter != null) { + mRealAdapter.onViewAttachedToWindow(holder); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onViewDetachedFromWindow(@NonNull ViewHolder holder) { + if (mRealAdapter != null) { + mRealAdapter.onViewDetachedFromWindow(holder); + } + } + + /** + * 添加头部View + */ + private void addHeaderView(View view) { + // 不能添加同一个View对象,否则会导致RecyclerView复用异常 + if (!mHeaderViews.contains(view) && !mFooterViews.contains(view)) { + mHeaderViews.add(view); + notifyDataSetChanged(); + } + } + + /** + * 移除头部View + */ + private void removeHeaderView(View view) { + if (mHeaderViews.remove(view)) { + notifyDataSetChanged(); + } + } + + /** + * 添加底部View + */ + private void addFooterView(View view) { + // 不能添加同一个View对象,否则会导致RecyclerView复用异常 + if (!mFooterViews.contains(view) && !mHeaderViews.contains(view)) { + mFooterViews.add(view); + notifyDataSetChanged(); + } + } + + /** + * 移除底部View + */ + private void removeFooterView(View view) { + if (mFooterViews.remove(view)) { + notifyDataSetChanged(); + } + } + + /** + * 获取头部View总数 + */ + private int getHeaderViewsCount() { + return mHeaderViews.size(); + } + + /** + * 获取底部View总数 + */ + private int getFooterViewsCount() { + return mFooterViews.size(); + } + + /** + * 获取头部View集合 + */ + private List getHeaderViews() { + return mHeaderViews; + } + + /** + * 获取底部View集合 + */ + private List getFooterViews() { + return mFooterViews; + } + } + + /** + * 头部和底部通用的ViewHolder对象 + */ + private static final class WrapViewHolder extends RecyclerView.ViewHolder { + + private WrapViewHolder(View itemView) { + super(itemView); + } + } + + /** + * 数据改变监听器 + */ + private static final class WrapAdapterDataObserver extends RecyclerView.AdapterDataObserver { + + private final WrapRecyclerAdapter mWrapAdapter; + + private WrapAdapterDataObserver(WrapRecyclerAdapter adapter) { + mWrapAdapter = adapter; + } + + @Override + public void onChanged() { + mWrapAdapter.notifyDataSetChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + onItemRangeChanged(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeChanged(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeInserted(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeRemoved(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + mWrapAdapter.notifyItemMoved(mWrapAdapter.getHeaderViewsCount() + fromPosition, toPosition); + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/view/ClearEditText.java b/widget/src/main/java/com/example/widget/view/ClearEditText.java new file mode 100644 index 0000000..4480559 --- /dev/null +++ b/widget/src/main/java/com/example/widget/view/ClearEditText.java @@ -0,0 +1,126 @@ +package com.example.widget.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.example.widget.R; + +import java.util.Objects; + +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 带清除按钮的 EditText + */ +public final class ClearEditText extends RegexEditText + implements View.OnTouchListener, + View.OnFocusChangeListener, TextWatcher { + + private Drawable mClearDrawable; + + private OnTouchListener mOnTouchListener; + private OnFocusChangeListener mOnFocusChangeListener; + + public ClearEditText(Context context) { + this(context, null); + } + + public ClearEditText(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.editTextStyle); + } + + @SuppressLint("ClickableViewAccessibility") + public ClearEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mClearDrawable = DrawableCompat.wrap(Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.input_delete_ic))); + mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight()); + setDrawableVisible(false); + super.setOnTouchListener(this); + super.setOnFocusChangeListener(this); + super.addTextChangedListener(this); + } + + private void setDrawableVisible(final boolean visible) { + if (mClearDrawable.isVisible() == visible) { + return; + } + + mClearDrawable.setVisible(visible, false); + final Drawable[] drawables = getCompoundDrawables(); + setCompoundDrawables( + drawables[0], + drawables[1], + visible ? mClearDrawable : null, + drawables[3]); + } + + @Override + public void setOnFocusChangeListener(final OnFocusChangeListener onFocusChangeListener) { + mOnFocusChangeListener = onFocusChangeListener; + } + + @Override + public void setOnTouchListener(final OnTouchListener onTouchListener) { + mOnTouchListener = onTouchListener; + } + + /** + * {@link OnFocusChangeListener} + */ + + @Override + public void onFocusChange(final View view, final boolean hasFocus) { + if (hasFocus && getText() != null) { + setDrawableVisible(getText().length() > 0); + } else { + setDrawableVisible(false); + } + if (mOnFocusChangeListener != null) { + mOnFocusChangeListener.onFocusChange(view, hasFocus); + } + } + + /** + * {@link OnTouchListener} + */ + + @Override + public boolean onTouch(final View view, final MotionEvent motionEvent) { + final int x = (int) motionEvent.getX(); + if (mClearDrawable.isVisible() && x > getWidth() - getPaddingRight() - mClearDrawable.getIntrinsicWidth()) { + if (motionEvent.getAction() == MotionEvent.ACTION_UP) { + setText(""); + } + return true; + } + return mOnTouchListener != null && mOnTouchListener.onTouch(view, motionEvent); + } + + /** + * {@link TextWatcher} + */ + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + if (isFocused()) { + setDrawableVisible(s.length() > 0); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void afterTextChanged(Editable s) {} +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/view/CountdownView.java b/widget/src/main/java/com/example/widget/view/CountdownView.java new file mode 100644 index 0000000..dc9aa29 --- /dev/null +++ b/widget/src/main/java/com/example/widget/view/CountdownView.java @@ -0,0 +1,90 @@ +package com.example.widget.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2018/10/18 + * desc : 验证码倒计时 + */ +public final class CountdownView extends AppCompatTextView implements Runnable { + + /** 倒计时秒数 */ + private int mTotalSecond = 60; + /** 秒数单位文本 */ + private static final String TIME_UNIT = "S"; + + /** 当前秒数 */ + private int mCurrentSecond; + /** 记录原有的文本 */ + private CharSequence mRecordText; + + public CountdownView(Context context) { + super(context); + } + + public CountdownView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CountdownView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * 设置倒计时总秒数 + */ + public void setTotalTime(int totalTime) { + this.mTotalSecond = totalTime; + } + + /** + * 开始倒计时 + */ + public void start() { + mRecordText = getText(); + setEnabled(false); + mCurrentSecond = mTotalSecond; + post(this); + } + + /** + * 结束倒计时 + */ + public void stop() { + setText(mRecordText); + setEnabled(true); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + // 设置点击的属性 + setClickable(true); + } + + @Override + protected void onDetachedFromWindow() { + // 移除延迟任务,避免内存泄露 + removeCallbacks(this); + super.onDetachedFromWindow(); + } + + @SuppressLint("SetTextI18n") + @Override + public void run() { + if (mCurrentSecond == 0) { + stop(); + } else { + mCurrentSecond--; + setText(mCurrentSecond + " " + TIME_UNIT); + postDelayed(this, 1000); + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/view/PasswordEditText.java b/widget/src/main/java/com/example/widget/view/PasswordEditText.java new file mode 100644 index 0000000..dccde24 --- /dev/null +++ b/widget/src/main/java/com/example/widget/view/PasswordEditText.java @@ -0,0 +1,166 @@ +package com.example.widget.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.example.widget.R; + +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/08/25 + * desc : 密码隐藏显示 EditText + */ +public final class PasswordEditText extends RegexEditText + implements View.OnTouchListener, + View.OnFocusChangeListener, TextWatcher { + + private Drawable mCurrentDrawable; + private final Drawable mVisibleDrawable; + private final Drawable mInvisibleDrawable; + + private View.OnTouchListener mOnTouchListener; + private View.OnFocusChangeListener mOnFocusChangeListener; + + public PasswordEditText(Context context) { + this(context, null); + } + + public PasswordEditText(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.editTextStyle); + } + + @SuppressWarnings("all") + @SuppressLint("ClickableViewAccessibility") + public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mVisibleDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.password_off_ic)); + mVisibleDrawable.setBounds(0, 0, mVisibleDrawable.getIntrinsicWidth(), mVisibleDrawable.getIntrinsicHeight()); + + mInvisibleDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.password_on_ic)); + mInvisibleDrawable.setBounds(0, 0, mInvisibleDrawable.getIntrinsicWidth(), mInvisibleDrawable.getIntrinsicHeight()); + + mCurrentDrawable = mVisibleDrawable; + + // 密码不可见 + addInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); + if (getInputRegex() == null) { + // 密码输入规则 + setInputRegex(REGEX_NONNULL); + } + + setDrawableVisible(false); + super.setOnTouchListener(this); + super.setOnFocusChangeListener(this); + super.addTextChangedListener(this); + } + + private void setDrawableVisible(boolean visible) { + if (mCurrentDrawable.isVisible() == visible) { + return; + } + + mCurrentDrawable.setVisible(visible, false); + Drawable[] drawables = getCompoundDrawablesRelative(); + setCompoundDrawablesRelative( + drawables[0], + drawables[1], + visible ? mCurrentDrawable : null, + drawables[3]); + } + + private void refreshDrawableStatus() { + Drawable[] drawables = getCompoundDrawablesRelative(); + setCompoundDrawablesRelative( + drawables[0], + drawables[1], + mCurrentDrawable, + drawables[3]); + } + + @Override + public void setOnFocusChangeListener(View.OnFocusChangeListener onFocusChangeListener) { + mOnFocusChangeListener = onFocusChangeListener; + } + + @Override + public void setOnTouchListener(View.OnTouchListener onTouchListener) { + mOnTouchListener = onTouchListener; + } + + /** + * {@link View.OnFocusChangeListener} + */ + + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (hasFocus && getText() != null) { + setDrawableVisible(getText().length() > 0); + } else { + setDrawableVisible(false); + } + if (mOnFocusChangeListener != null) { + mOnFocusChangeListener.onFocusChange(view, hasFocus); + } + } + + /** + * {@link View.OnTouchListener} + */ + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + int x = (int) motionEvent.getX(); + if (mCurrentDrawable.isVisible() && x > getWidth() - getPaddingRight() - mCurrentDrawable.getIntrinsicWidth()) { + if (motionEvent.getAction() == MotionEvent.ACTION_UP) { + if (mCurrentDrawable == mVisibleDrawable) { + mCurrentDrawable = mInvisibleDrawable; + // 密码可见 + setTransformationMethod(HideReturnsTransformationMethod.getInstance()); + refreshDrawableStatus(); + } else if (mCurrentDrawable == mInvisibleDrawable) { + mCurrentDrawable = mVisibleDrawable; + // 密码不可见 + setTransformationMethod(PasswordTransformationMethod.getInstance()); + refreshDrawableStatus(); + } + Editable editable = getText(); + if (editable != null) { + setSelection(editable.toString().length()); + } + } + return true; + } + return mOnTouchListener != null && mOnTouchListener.onTouch(view, motionEvent); + } + + /** + * {@link TextWatcher} + */ + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (isFocused()) { + setDrawableVisible(s.length() > 0); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void afterTextChanged(Editable s) {} +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/view/RegexEditText.java b/widget/src/main/java/com/example/widget/view/RegexEditText.java new file mode 100644 index 0000000..c94ad60 --- /dev/null +++ b/widget/src/main/java/com/example/widget/view/RegexEditText.java @@ -0,0 +1,191 @@ +package com.example.widget.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.InputFilter; +import android.text.Spanned; +import android.text.TextUtils; +import android.util.AttributeSet; + +import com.example.widget.R; + +import java.util.regex.Pattern; + +import androidx.appcompat.widget.AppCompatEditText; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/06/29 + * desc : 正则输入限制编辑框 + */ +public class RegexEditText extends AppCompatEditText implements InputFilter { + + /** 手机号(只能以 1 开头) */ + public static final String REGEX_MOBILE = "[1]\\d{0,10}"; + /** 中文(普通的中文字符) */ + public static final String REGEX_CHINESE = "[\\u4e00-\\u9fa5]*"; + /** 英文(大写和小写的英文) */ + public static final String REGEX_ENGLISH = "[a-zA-Z]*"; + /** 计数(非 0 开头的数字) */ + public static final String REGEX_COUNT = "[1-9]\\d*"; + /** 用户名(中文、英文、数字) */ + public static final String REGEX_NAME = "[[\\u4e00-\\u9fa5]|[a-zA-Z]|\\d]*"; + /** 非空格的字符(不能输入空格) */ + public static final String REGEX_NONNULL = "\\S+"; + + /** 正则表达式规则 */ + private Pattern mPattern; + + public RegexEditText(Context context) { + this(context, null); + } + + public RegexEditText(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.editTextStyle); + } + + public RegexEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RegexEditText); + + if (array.hasValue(R.styleable.RegexEditText_inputRegex)) { + setInputRegex(array.getString(R.styleable.RegexEditText_inputRegex)); + } else { + if (array.hasValue(R.styleable.RegexEditText_regexType)) { + int regexType = array.getInt(R.styleable.RegexEditText_regexType, 0); + switch (regexType) { + case 0x01: + setInputRegex(REGEX_MOBILE); + break; + case 0x02: + setInputRegex(REGEX_CHINESE); + break; + case 0x03: + setInputRegex(REGEX_ENGLISH); + break; + case 0x04: + setInputRegex(REGEX_COUNT); + break; + case 0x05: + setInputRegex(REGEX_NAME); + break; + case 0x06: + setInputRegex(REGEX_NONNULL); + break; + default: + break; + } + } + } + + array.recycle(); + } + + /** + * 是否有这个输入标记 + */ + public boolean hasInputType(int type) { + return (getInputType() & type) != 0; + } + + /** + * 添加一个输入标记 + */ + public void addInputType(int type) { + setInputType(getInputType() | type); + } + + /** + * 移除一个输入标记 + */ + public void removeInputType(int type) { + setInputType(getInputType() & ~type); + } + + /** + * 设置输入正则 + */ + public void setInputRegex(String regex) { + if (TextUtils.isEmpty(regex)) { + return; + } + + mPattern = Pattern.compile(regex); + addFilters(this); + } + + /** + * 获取输入正则 + */ + public String getInputRegex() { + if (mPattern == null) { + return null; + } + return mPattern.pattern(); + } + + /** + * 添加筛选规则 + */ + public void addFilters(InputFilter filter) { + if (filter == null) { + return; + } + + final InputFilter[] newFilters; + final InputFilter[] oldFilters = getFilters(); + if (oldFilters != null && oldFilters.length > 0) { + newFilters = new InputFilter[oldFilters.length + 1]; + // 复制旧数组的元素到新数组中 + System.arraycopy(oldFilters, 0, newFilters, 0, oldFilters.length); + newFilters[oldFilters.length] = filter; + } else { + newFilters = new InputFilter[1]; + newFilters[0] = filter; + } + super.setFilters(newFilters); + } + + /** + * {@link InputFilter} + * + * @param source 新输入的字符串 + * @param start 新输入的字符串起始下标,一般为0 + * @param end 新输入的字符串终点下标,一般为source长度-1 + * @param dest 输入之前文本框内容 + * @param destStart 原内容起始坐标,一般为0 + * @param destEnd 原内容终点坐标,一般为dest长度-1 + * @return 返回字符串将会加入到内容中 + */ + @Override + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int destStart, int destEnd) { + if (mPattern != null) { + // 拼接出最终的字符串 + String begin = dest.toString().substring(0, destStart); + String over = dest.toString().substring(destStart + (destEnd - destStart), destStart + (dest.toString().length() - begin.length())); + String result = begin + source + over; + + // 判断是插入还是删除 + if (destStart > destEnd - 1) { + // 如果是插入字符 + if (!mPattern.matcher(result).matches()) { + // 如果不匹配就不让这个字符输入 + return ""; + } + } else { + // 如果是删除字符 + if (!mPattern.matcher(result).matches()) { + // 如果不匹配则不让删除(删空操作除外) + if (!"".equals(result)) { + return dest.toString().substring(destStart, destEnd); + } + } + } + } + + // 不做任何修改 + return source; + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/view/ScaleImageView.java b/widget/src/main/java/com/example/widget/view/ScaleImageView.java new file mode 100644 index 0000000..ac19a30 --- /dev/null +++ b/widget/src/main/java/com/example/widget/view/ScaleImageView.java @@ -0,0 +1,53 @@ +package com.example.widget.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; + + +import com.example.widget.R; + +import androidx.appcompat.widget.AppCompatImageView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/08/02 + * desc : 长按缩放松手恢复的 ImageView + */ +public final class ScaleImageView extends AppCompatImageView { + + private float mScaleSize = 1.2f; + + public ScaleImageView(Context context) { + this(context, null); + } + + public ScaleImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ScaleImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ScaleImageView); + setScaleSize(array.getFloat(R.styleable.ScaleImageView_scaleRatio, mScaleSize)); + array.recycle(); + } + + @Override + protected void dispatchSetPressed(boolean pressed) { + // 判断当前手指是否按下了 + if (pressed) { + setScaleX(mScaleSize); + setScaleY(mScaleSize); + } else { + setScaleX(1); + setScaleY(1); + } + } + + public void setScaleSize(float size) { + mScaleSize = size; + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/view/SmartTextView.java b/widget/src/main/java/com/example/widget/view/SmartTextView.java new file mode 100644 index 0000000..01fbb81 --- /dev/null +++ b/widget/src/main/java/com/example/widget/view/SmartTextView.java @@ -0,0 +1,58 @@ +package com.example.widget.view; + +import android.content.Context; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; + +import androidx.appcompat.widget.AppCompatTextView; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/08/18 + * desc : 智能显示的 TextView + */ +public final class SmartTextView extends AppCompatTextView implements TextWatcher { + + public SmartTextView(Context context) { + this(context, null); + } + + public SmartTextView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + public SmartTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + addTextChangedListener(this); + // 触发一次监听 + afterTextChanged(null); + } + + /** + * {@link TextWatcher} + */ + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + // 判断当前有没有设置文本达到自动隐藏和显示的效果 + if (TextUtils.isEmpty(getText().toString())) { + if (getVisibility() != GONE) { + setVisibility(GONE); + } + } else { + if (getVisibility() != VISIBLE) { + setVisibility(VISIBLE); + } + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/example/widget/view/SwitchButton.java b/widget/src/main/java/com/example/widget/view/SwitchButton.java new file mode 100644 index 0000000..63019cc --- /dev/null +++ b/widget/src/main/java/com/example/widget/view/SwitchButton.java @@ -0,0 +1,532 @@ +package com.example.widget.view; + +import android.annotation.SuppressLint; +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.graphics.RadialGradient; +import android.graphics.RectF; +import android.graphics.Shader; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AccelerateInterpolator; + +import com.example.widget.R; + +import androidx.annotation.Nullable; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/02/20 + * desc : 高仿 ios 开关按钮 + */ +public final class SwitchButton extends View { + + private static final int STATE_SWITCH_OFF = 1; + private static final int STATE_SWITCH_OFF2 = 2; + private static final int STATE_SWITCH_ON = 3; + private static final int STATE_SWITCH_ON2 = 4; + + private final AccelerateInterpolator mInterpolator = new AccelerateInterpolator(2); + private final Paint mPaint = new Paint(); + private final Path mBackgroundPath = new Path(); + private final Path mBarPath = new Path(); + private final RectF mBound = new RectF(); + + private float mAnim1, mAnim2; + private RadialGradient mShadowGradient; + + /** 按钮宽高形状比率(0,1] 不推荐大幅度调整 */ + protected final float mAspectRatio = 0.68f; + /** (0,1] */ + protected final float mAnimationSpeed = 0.1f; + + /** 上一个选中状态 */ + private int mLastCheckedState; + /** 当前的选中状态 */ + private int mCheckedState; + + private boolean isCanVisibleDrawing = false; + + /** 是否显示按钮阴影 */ + protected boolean isShadow; + /** 是否选中 */ + protected boolean mChecked; + + /** 开启状态背景色 */ + protected int mAccentColor = 0xFF4BD763; + /** 开启状态按钮描边色 */ + protected int mPrimaryDarkColor = 0xFF3AC652; + /** 关闭状态描边色 */ + protected int mOffColor = 0xFFE3E3E3; + /** 关闭状态按钮描边色 */ + protected int mOffDarkColor = 0xFFBFBFBF; + /** 按钮阴影色 */ + protected int mShadowColor = 0xFF333333; + /** 监听器 */ + private OnCheckedChangeListener mListener; + + private float mRight; + private float mCenterX, mCenterY; + private float mScale; + + private float mOffset; + private float mRadius, mStrokeWidth; + private float mWidth; + private float mLeft; + private float bRight; + private float mOnLeftX, mOn2LeftX, mOff2LeftX, mOffLeftX; + + private float mShadowReservedHeight; + + public SwitchButton(Context context) { + this(context, null); + } + + public SwitchButton(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + setLayerType(LAYER_TYPE_SOFTWARE, null); + + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton); + mChecked = array.getBoolean(R.styleable.SwitchButton_android_checked, mChecked); + setEnabled(array.getBoolean(R.styleable.SwitchButton_android_enabled, isEnabled())); + mLastCheckedState = mCheckedState = mChecked ? STATE_SWITCH_ON : STATE_SWITCH_OFF; + + array.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + switch (MeasureSpec.getMode(widthMeasureSpec)) { + case MeasureSpec.AT_MOST: + case MeasureSpec.UNSPECIFIED: + widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 56, getResources().getDisplayMetrics()) + + getPaddingLeft() + getPaddingRight()), MeasureSpec.EXACTLY); + break; + case MeasureSpec.EXACTLY: + default: + break; + } + switch (MeasureSpec.getMode(heightMeasureSpec)) { + case MeasureSpec.AT_MOST: + case MeasureSpec.UNSPECIFIED: + heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (MeasureSpec.getSize(widthMeasureSpec) * mAspectRatio) + + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY); + break; + case MeasureSpec.EXACTLY: + default: + break; + } + setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + isCanVisibleDrawing = width > getPaddingLeft() + getPaddingRight() && height > getPaddingTop() + getPaddingBottom(); + + if (isCanVisibleDrawing) { + int actuallyDrawingAreaWidth = width - getPaddingLeft() - getPaddingRight(); + int actuallyDrawingAreaHeight = height - getPaddingTop() - getPaddingBottom(); + + int actuallyDrawingAreaLeft; + int actuallyDrawingAreaRight; + int actuallyDrawingAreaTop; + int actuallyDrawingAreaBottom; + if (actuallyDrawingAreaWidth * mAspectRatio < actuallyDrawingAreaHeight) { + actuallyDrawingAreaLeft = getPaddingLeft(); + actuallyDrawingAreaRight = width - getPaddingRight(); + int heightExtraSize = (int) (actuallyDrawingAreaHeight - actuallyDrawingAreaWidth * mAspectRatio); + actuallyDrawingAreaTop = getPaddingTop() + heightExtraSize / 2; + actuallyDrawingAreaBottom = getHeight() - getPaddingBottom() - heightExtraSize / 2; + } else { + int widthExtraSize = (int) (actuallyDrawingAreaWidth - actuallyDrawingAreaHeight / mAspectRatio); + actuallyDrawingAreaLeft = getPaddingLeft() + widthExtraSize / 2; + actuallyDrawingAreaRight = getWidth() - getPaddingRight() - widthExtraSize / 2; + actuallyDrawingAreaTop = getPaddingTop(); + actuallyDrawingAreaBottom = getHeight() - getPaddingBottom(); + } + + mShadowReservedHeight = (int) ((actuallyDrawingAreaBottom - actuallyDrawingAreaTop) * 0.07f); + float left = actuallyDrawingAreaLeft; + float top = actuallyDrawingAreaTop + mShadowReservedHeight; + mRight = actuallyDrawingAreaRight; + float bottom = actuallyDrawingAreaBottom - mShadowReservedHeight; + + float sHeight = bottom - top; + mCenterX = (mRight + left) / 2; + mCenterY = (bottom + top) / 2; + + mLeft = left; + mWidth = bottom - top; + bRight = left + mWidth; + // OfB + final float halfHeightOfS = mWidth / 2; + mRadius = halfHeightOfS * 0.95f; + // offset of switching + mOffset = mRadius * 0.2f; + mStrokeWidth = (halfHeightOfS - mRadius) * 2; + mOnLeftX = mRight - mWidth; + mOn2LeftX = mOnLeftX - mOffset; + mOffLeftX = left; + mOff2LeftX = mOffLeftX + mOffset; + mScale = 1 - mStrokeWidth / sHeight; + + mBackgroundPath.reset(); + RectF bound = new RectF(); + bound.top = top; + bound.bottom = bottom; + bound.left = left; + bound.right = left + sHeight; + mBackgroundPath.arcTo(bound, 90, 180); + bound.left = mRight - sHeight; + bound.right = mRight; + mBackgroundPath.arcTo(bound, 270, 180); + mBackgroundPath.close(); + + mBound.left = mLeft; + mBound.right = bRight; + // bTop = sTop + mBound.top = top + mStrokeWidth / 2; + // bBottom = sBottom + mBound.bottom = bottom - mStrokeWidth / 2; + float bCenterX = (bRight + mLeft) / 2; + float bCenterY = (bottom + top) / 2; + + int red = mShadowColor >> 16 & 0xFF; + int green = mShadowColor >> 8 & 0xFF; + int blue = mShadowColor & 0xFF; + mShadowGradient = new RadialGradient(bCenterX, bCenterY, mRadius, Color.argb(200, red, green, blue), + Color.argb(25, red, green, blue), Shader.TileMode.CLAMP); + } + } + + private void calcBPath(float percent) { + mBarPath.reset(); + mBound.left = mLeft + mStrokeWidth / 2; + mBound.right = bRight - mStrokeWidth / 2; + mBarPath.arcTo(mBound, 90, 180); + mBound.left = mLeft + percent * mOffset + mStrokeWidth / 2; + mBound.right = bRight + percent * mOffset - mStrokeWidth / 2; + mBarPath.arcTo(mBound, 270, 180); + mBarPath.close(); + } + + private float calcBTranslate(float percent) { + float result = 0; + switch (mCheckedState - mLastCheckedState) { + case 1: + if (mCheckedState == STATE_SWITCH_OFF2) { + // off -> off2 + result = mOffLeftX; + } else if (mCheckedState == STATE_SWITCH_ON) { + // on2 -> on + result = mOnLeftX - (mOnLeftX - mOn2LeftX) * percent; + } + break; + case 2: + if (mCheckedState == STATE_SWITCH_ON) { + // off2 -> on + result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent; + } else if (mCheckedState == STATE_SWITCH_ON2) { + // off -> on2 + result = mOn2LeftX - (mOn2LeftX - mOffLeftX) * percent; + } + break; + case 3: + // off -> on + result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent; + break; + case -1: + if (mCheckedState == STATE_SWITCH_ON2) { + // on -> on2 + result = mOn2LeftX + (mOnLeftX - mOn2LeftX) * percent; + } else if (mCheckedState == STATE_SWITCH_OFF) { + // off2 -> off + result = mOffLeftX; + } + break; + case -2: + if (mCheckedState == STATE_SWITCH_OFF) { + // on2 -> off + result = mOffLeftX + (mOn2LeftX - mOffLeftX) * percent; + } else if (mCheckedState == STATE_SWITCH_OFF2) { + // on -> off2 + result = mOff2LeftX + (mOnLeftX - mOff2LeftX) * percent; + } + break; + case -3: + // on -> off + result = mOffLeftX + (mOnLeftX - mOffLeftX) * percent; + break; + default: // init + case 0: + if (mCheckedState == STATE_SWITCH_OFF) { + // off -> off + result = mOffLeftX; + } else if (mCheckedState == STATE_SWITCH_ON) { + // on -> on + result = mOnLeftX; + } + break; + } + return result - mOffLeftX; + } + + @Override + protected void onDraw(Canvas canvas) { + if (!isCanVisibleDrawing) { + return; + } + + mPaint.setAntiAlias(true); + + final boolean isOn = (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_ON2); + // Draw background + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(isOn ? mAccentColor : mOffColor); + canvas.drawPath(mBackgroundPath, mPaint); + + mAnim1 = mAnim1 - mAnimationSpeed > 0 ? mAnim1 - mAnimationSpeed : 0; + mAnim2 = mAnim2 - mAnimationSpeed > 0 ? mAnim2 - mAnimationSpeed : 0; + + final float dsAnim = mInterpolator.getInterpolation(mAnim1); + final float dbAnim = mInterpolator.getInterpolation(mAnim2); + // Draw background animation + final float scale = mScale * (isOn ? dsAnim : 1 - dsAnim); + final float scaleOffset = (mRight - mCenterX - mRadius) * (isOn ? 1 - dsAnim : dsAnim); + canvas.save(); + canvas.scale(scale, scale, mCenterX + scaleOffset, mCenterY); + mPaint.setColor(0xFFFFFFFF); + canvas.drawPath(mBackgroundPath, mPaint); + canvas.restore(); + // To prepare center bar path + canvas.save(); + canvas.translate(calcBTranslate(dbAnim), mShadowReservedHeight); + final boolean isState2 = (mCheckedState == STATE_SWITCH_ON2 || mCheckedState == STATE_SWITCH_OFF2); + calcBPath(isState2 ? 1 - dbAnim : dbAnim); + // Use center bar path to draw shadow + if (isShadow) { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setShader(mShadowGradient); + canvas.drawPath(mBarPath, mPaint); + mPaint.setShader(null); + } + canvas.translate(0, -mShadowReservedHeight); + // draw bar + canvas.scale(0.98f, 0.98f, mWidth / 2, mWidth / 2); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(0xFFFFFFFF); + canvas.drawPath(mBarPath, mPaint); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mStrokeWidth * 0.5f); + mPaint.setColor(isOn ? mPrimaryDarkColor : mOffDarkColor); + canvas.drawPath(mBarPath, mPaint); + canvas.restore(); + + mPaint.reset(); + if (mAnim1 > 0 || mAnim2 > 0) { + invalidate(); + } + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + if (isEnabled() + && (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_OFF) + && (mAnim1 * mAnim2 == 0)) { + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + mLastCheckedState = mCheckedState; + mAnim2 = 1; + + switch (mCheckedState) { + case STATE_SWITCH_OFF: + setChecked(true, false); + if (mListener != null) { + mListener.onCheckedChanged(this, true); + } + break; + case STATE_SWITCH_ON: + setChecked(false, false); + if (mListener != null) { + mListener.onCheckedChanged(this, false); + } + break; + default: + break; + } + break; + case MotionEvent.ACTION_DOWN: + default: + break; + } + } + return true; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState state = new SavedState(superState); + state.checked = mChecked; + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + mChecked = savedState.checked; + mCheckedState = mChecked ? STATE_SWITCH_ON : STATE_SWITCH_OFF; + invalidate(); + } + + public void setColor(int newColorPrimary, int newColorPrimaryDark) { + setColor(newColorPrimary, newColorPrimaryDark, mOffColor, mOffDarkColor); + } + + public void setColor(int newColorPrimary, int newColorPrimaryDark, int newColorOff, int newColorOffDark) { + setColor(newColorPrimary, newColorPrimaryDark, newColorOff, newColorOffDark, mShadowColor); + } + + public void setColor(int newColorPrimary, int newColorPrimaryDark, int newColorOff, int newColorOffDark, int newColorShadow) { + mAccentColor = newColorPrimary; + mPrimaryDarkColor = newColorPrimaryDark; + mOffColor = newColorOff; + mOffDarkColor = newColorOffDark; + mShadowColor = newColorShadow; + invalidate(); + } + + /** + * 设置按钮阴影开关 + */ + public void setShadow(boolean shadow) { + isShadow = shadow; + invalidate(); + } + + /** + * 当前状态是否选中 + */ + public boolean isChecked() { + return mChecked; + } + + /** + * 设置选择状态(默认会回调监听器) + */ + public void setChecked(boolean checked) { + // 回调监听器 + setChecked(checked, true); + } + + /** + * 设置选择状态 + */ + public void setChecked(boolean checked, boolean callback) { + int newState = checked ? STATE_SWITCH_ON : STATE_SWITCH_OFF; + if (newState == mCheckedState) { + return; + } + if ((newState == STATE_SWITCH_ON && (mCheckedState == STATE_SWITCH_OFF || mCheckedState == STATE_SWITCH_OFF2)) + || (newState == STATE_SWITCH_OFF && (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_ON2))) { + mAnim1 = 1; + } + mAnim2 = 1; + + if (!mChecked && newState == STATE_SWITCH_ON) { + mChecked = true; + } else if (mChecked && newState == STATE_SWITCH_OFF) { + mChecked = false; + } + mLastCheckedState = mCheckedState; + mCheckedState = newState; + postInvalidate(); + + if (callback && mListener != null) { + mListener.onCheckedChanged(this, checked); + } + } + + /** + * 设置选中状态改变监听 + */ + public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { + mListener = listener; + } + + public interface OnCheckedChangeListener { + /** + * 回调监听 + * + * @param button 切换按钮 + * @param isChecked 是否选中 + */ + void onCheckedChanged(SwitchButton button, boolean isChecked); + } + + /** + * 保存开关状态 + */ + private static final class SavedState extends BaseSavedState { + + private boolean checked; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + checked = 1 == in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(checked ? 1 : 0); + } + + /** + * fixed by Night99 https://github.com/g19980115 + */ + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} \ No newline at end of file diff --git a/widget/src/main/res/drawable/icon.png b/widget/src/main/res/drawable/icon.png new file mode 100644 index 0000000..8de003e Binary files /dev/null and b/widget/src/main/res/drawable/icon.png differ diff --git a/widget/src/main/res/drawable/input_delete_ic.xml b/widget/src/main/res/drawable/input_delete_ic.xml new file mode 100644 index 0000000..7f143c4 --- /dev/null +++ b/widget/src/main/res/drawable/input_delete_ic.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/password_off_ic.xml b/widget/src/main/res/drawable/password_off_ic.xml new file mode 100644 index 0000000..cae4597 --- /dev/null +++ b/widget/src/main/res/drawable/password_off_ic.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/password_on_ic.xml b/widget/src/main/res/drawable/password_on_ic.xml new file mode 100644 index 0000000..dbff3e4 --- /dev/null +++ b/widget/src/main/res/drawable/password_on_ic.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/values/attrs.xml b/widget/src/main/res/values/attrs.xml new file mode 100644 index 0000000..8c19df4 --- /dev/null +++ b/widget/src/main/res/values/attrs.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/test/java/com/example/widget/ExampleUnitTest.java b/widget/src/test/java/com/example/widget/ExampleUnitTest.java new file mode 100644 index 0000000..53d2d07 --- /dev/null +++ b/widget/src/test/java/com/example/widget/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.widget; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file