commit a11422076218b5eee8af50fd4edad0edaff43ecd Author: wangyu Date: Sat Dec 27 13:55:48 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ccc4f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/bidinfo.keystore b/app/bidinfo.keystore new file mode 100644 index 0000000..992fdaf Binary files /dev/null and b/app/bidinfo.keystore differ diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..48b9c42 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,184 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) + id 'kotlin-kapt' + id 'kotlin-parcelize' +} + +android { + namespace 'com.cheng.bole' + compileSdk 34 + buildFeatures.buildConfig = true + + lintOptions{ + checkReleaseBuilds false + abortOnError false + } + + defaultConfig { + applicationId "com.cheng.BoLe" + minSdk 26 + targetSdk 34 + versionCode 240 + versionName "2.4.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + ndk { + abiFilters 'arm64-v8a' //'armeabi-v7a' + } + multiDexEnabled true + + flavorDimensions = ["channel"] + + productFlavors { + xiaomi {} + oppo {} + vivo {} + huawei {} + honor {} + baidu {} + yyb {} + upgrade {} + bd_tg {} + dy_tg {} + tx_tg {} + ks_tg {} + dd_tg {} + sm_tg {} + oc_tg {} + } + + productFlavors.configureEach { + dimension "channel" + manifestPlaceholders = [UMENG_CHANNEL: name] + } + + manifestPlaceholders = [ + GETUI_APPID : "ej3hUPd0LR8G1CzkNtyZS3", + GT_INSTALL_CHANNEL: "test", + ] + } + + // 配置签名信息 + signingConfigs { + config { + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + enableV1Signing true + enableV2Signing true + enableV3Signing true + } + } + + buildTypes { + debug { + minifyEnabled false + shrinkResources false + signingConfig signingConfigs.config + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + shrinkResources true + signingConfig signingConfigs.config + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + applicationVariants.configureEach { variant -> + if (variant.buildType.name == "release") { + variant.outputs.forEach { + it.outputFileName = "material_v${defaultConfig.versionName}_${variant.productFlavors[0].name}.apk" + } + + variant.assembleProvider.get().doLast { + copy { + variant.outputs.forEach { file -> + //移动到指定文件夹 + ant.move file: file.outputFile, + todir: "${project.rootDir}/apk" + } + } + } + + variant.assembleProvider.get().doLast { + android.buildTypes.each { buildType -> + file("build/outputs/apk/$buildType").listFiles().each { channelFolder -> + if (channelFolder.isDirectory() && channelFolder.getName() != outputApkFolder) { + delete(channelFolder) + } + } + } + } + + } + + } + + buildFeatures { + viewBinding true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) + implementation project(':base') + + implementation libs.androidx.appcompat + implementation libs.com.google.android.material.material + implementation libs.androidx.constraintlayout.constraintlayout + testImplementation libs.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espresso.core + +// implementation "androidx.core:core-splashscreen:1.0.1" + implementation 'com.jakewharton:disklrucache:2.0.2' // disklrucache + + // tabLayout + implementation 'com.github.angcyo.DslTablayout:TabLayout:3.7.2' + implementation 'com.github.angcyo.DslTablayout:ViewPager1Delegate:3.7.2' + + implementation 'com.github.FlyJingFish:GradientTextView:1.2.5' //渐变文字 + implementation 'com.github.aitsuki:SwipeMenuRecyclerView:2.1.5' // 侧滑菜单 + implementation 'com.github.chrisbanes:PhotoView:2.3.0' //图片浏览 + implementation 'io.github.billywei01:fastaes:1.1.3' //解密 + implementation 'com.github.gzu-liyujiang:Android_CN_OAID:4.2.12' //获取手机设备id + implementation 'com.google.android.flexbox:flexbox:3.0.0' //recyclerview flexbox + + implementation files('libs/channelsdk-0.2.2.aar') //快手分包 + implementation 'com.tencent.vasdolly:helper:3.0.4' //腾讯分包 + implementation files('libs/humesdk-1.0.0.aar') //巨量分包 + + implementation 'com.getui:gysdk:3.1.7.0' //一键认证sdk + implementation 'com.getui:gtc:3.2.16.0' //个推公共库,如已接其他个推sdk则保留一个最高版本即可 + + implementation 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.8.0' //微信 + //友盟 + implementation 'com.umeng.umsdk:common:9.6.3'// 必选 + implementation 'com.umeng.umsdk:asms:1.8.0'// 必选 + implementation 'com.umeng.umsdk:apm:1.9.1' // U-APM包依赖(必选) + implementation 'com.umeng.umsdk:share-core:7.3.2'//分享核心库,必选 + implementation 'com.umeng.umsdk:share-wx:7.3.2' //微信完整版 + + implementation 'top.zibin:Luban:1.1.8' //图片压缩 + implementation 'com.github.Dimezis:BlurView:version-2.0.6' //毛玻璃效果 + implementation 'com.bytedance.ads:AppConvert:2.0.0' //巨量融合 + implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:4.1.14' // 滚轮选择器 + //media3视频播放 + implementation "androidx.media3:media3-exoplayer:1.4.1" + implementation "androidx.media3:media3-ui:1.4.1" + implementation "androidx.media3:media3-common:1.4.1" +} \ No newline at end of file diff --git a/app/libs/alipaysdk-15.8.03.210428205839.aar b/app/libs/alipaysdk-15.8.03.210428205839.aar new file mode 100644 index 0000000..b2cf3f4 Binary files /dev/null and b/app/libs/alipaysdk-15.8.03.210428205839.aar differ diff --git a/app/libs/asr-file-recognize-release.aar b/app/libs/asr-file-recognize-release.aar new file mode 100644 index 0000000..d8d08ee Binary files /dev/null and b/app/libs/asr-file-recognize-release.aar differ diff --git a/app/libs/channelsdk-0.2.2.aar b/app/libs/channelsdk-0.2.2.aar new file mode 100644 index 0000000..93ce247 Binary files /dev/null and b/app/libs/channelsdk-0.2.2.aar differ diff --git a/app/libs/easyPhotos-release.aar b/app/libs/easyPhotos-release.aar new file mode 100644 index 0000000..aa79d8e Binary files /dev/null and b/app/libs/easyPhotos-release.aar differ diff --git a/app/libs/humesdk-1.0.0.aar b/app/libs/humesdk-1.0.0.aar new file mode 100644 index 0000000..2db4091 Binary files /dev/null and b/app/libs/humesdk-1.0.0.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..a900707 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,358 @@ +# 代码混淆压缩比,在0~7之间,默认为5,一般不做修改 +-optimizationpasses 5 +# 混合时不使用大小写混合,混合后的类名为小写 +-dontusemixedcaseclassnames +# 指定不去忽略非公共库的类 +-dontskipnonpubliclibraryclasses +# 这句话能够使我们的项目混淆后产生映射文件 +# 包含有类名->混淆后类名的映射关系 +-verbose +# 指定不去忽略非公共库的类成员 +-dontskipnonpubliclibraryclassmembers +# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。 +-dontpreverify +# 保留Annotation不混淆 +-keepattributes *Annotation*,InnerClasses +# 避免混淆泛型 +-keepattributes Signature +# 抛出异常时保留代码行号 +-keepattributes SourceFile,LineNumberTable +# 指定混淆是采用的算法,后面的参数是一个过滤器 +# 这个过滤器是谷歌推荐的算法,一般不做更改 +-optimizations !code/simplification/cast,!field/*,!class/merging/* + +############################################# +# +# Android开发中一些需要保留的公共部分 +# +############################################# + +# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 +# 因为这些子类都有可能被外部调用 +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Appliction +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class * extends android.view.View +-keep public class com.android.vending.licensing.ILicensingService + +# 保留support下的所有类及其内部类 +-keep class android.support.** {*;} +#kotlin +-keep class kotlin.** { *; } +-keep class kotlin.Metadata { *; } +-dontwarn kotlin.** +-keepclassmembers class **$WhenMappings { + ; +} +-keepclassmembers class kotlin.Metadata { + public ; +} +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); +} + +# 保留继承的 +-keep public class * extends android.support.v4.** +-keep public class * extends android.support.v7.** +-keep public class * extends android.support.annotation.** + +-keep class com.google.android.material.** {*;} +-keep class androidx.** {*;} +-keep public class * extends androidx.** +-keep interface androidx.** {*;} +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** + + +# 保留R下面的资源 +-keep class **.R$* {*;} + +# 保留本地native方法不被混淆 +-keepclasseswithmembernames class * { + native ; +} + +# 保留binding方法不被混淆 +-keepclassmembers class ** implements androidx.viewbinding.ViewBinding { + public static ** bind(***); + public static ** inflate(***); +} + +# 保留在Activity中的方法参数是view的方法, +# 这样以来我们在layout中写的onClick就不会被影响 +-keepclassmembers class * extends android.app.Activity{ + public void *(android.view.View); +} + +# 保留我们自定义控件(继承自View)不被混淆 +-keep public class * extends android.view.View{ + *** get*(); + void set*(***); + public (android.content.Context); + public (android.content.Context, android.util.AttributeSet); + public (android.content.Context, android.util.AttributeSet, int); +} + +# 保留Parcelable序列化类不被混淆 +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +# 保留Serializable序列化的类不被混淆 +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +-keep public class * implements java.io.Serializable {*;} + +# 不混淆实体类 +-keep class com.cheng.bole.bean.** { *; } +-keep class com.cheng.bole.bean.db.** { *; } +-keep class com.cheng.bole.net.model.** { *; } + +# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆 +-keepclassmembers class * { + void *(**On*Event); + void *(**On*Listener); +} + +# 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用 +# 记得proguard-android.txt中一定不要加-dontoptimize才起作用 +# 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制 +-assumenosideeffects class android.util.Log { + public static int v(...); + public static int i(...); + public static int w(...); + public static int d(...); + public static int e(...); +} + +# fastJson +-dontwarn com.alibaba.fastjson.** +-keep class com.alibaba.fastjson.** { *; } +-keepattributes *Annotation* + +# gson +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken +-keepattributes AnnotationDefault,RuntimeVisibleAnnotations + +# 保留Gson核心类和方法,防止它们被混淆 +-dontwarn sun.misc.** +-keep class com.google.gson.** { *; } +-keep class com.google.gson.examples.android.model.** { ; } +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + + +# 保持Gson序列化和反序列化所需的注解不被混淆 +-keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault + +-keepclassmembernames class * { + @com.google.gson.annotations.SerializedName ; +} + +# 保持枚举类型的序列化兼容性 +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# 防止R8或ProGuard移除未使用的元素(如果这些元素是通过反射访问的,比如Gson) +-keep class * { + @com.google.gson.annotations.SerializedName *; +} +-keepclassmembers class com.example.base.extensions.GsonExtensionsKt { + ; +} + +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +# OkHttp3 +-dontwarn com.squareup.okhttp3.** +-keep class com.squareup.okhttp3.** { *;} +-dontwarn okio.** + +# Okio +-dontwarn com.squareup.** +-dontwarn okio.** +-keep public class org.codehaus.* { *; } +-keep public class java.nio.* { *; } + +# Retrofit +-dontwarn retrofit2.** +-keep class retrofit2.** { *; } +-keepattributes Signature +-keepattributes Exceptions + +# liveDataBus +-dontwarn com.jeremyliao.liveeventbus.** +-keep class com.jeremyliao.liveeventbus.** { *; } +-keep class androidx.lifecycle.** { *; } +-keep class androidx.arch.core.** { *; } + +# 微信 +-keep class com.tencent.mm.opensdk.** { *; } +-keep class com.tencent.wxop.** { *; } +-keep class com.tencent.mm.sdk.** { *; } + +# 支付宝 +-libraryjars libs/alipaysdk-15.8.03.210428205839.aar +-keep class com.alipay.android.app.IAlixPay{*;} +-keep class com.alipay.android.app.IAlixPay$Stub{*;} +-keep class com.alipay.android.app.IRemoteServiceCallback{*;} +-keep class com.alipay.android.app.IRemoteServiceCallback$Stub{*;} +-keep class com.alipay.sdk.app.PayTask{ public *;} +-keep class com.alipay.sdk.app.AuthTask{ public *;} + +# 友盟 +-keep class com.umeng.** {*;} +-keep class org.repackage.** {*;} + +-keepclassmembers class * { + public (org.json.JSONObject); +} + +-keep public class com.chat.assistant.ai.R$*{ +public static final int *; +} + +#加载动画 +-keep class com.wang.avi.** { *; } +-keep class com.wang.avi.indicators.** { *; } + +#oss +-keep class com.alibaba.sdk.android.oss.** { *; } +-dontwarn okio.** +-dontwarn org.apache.commons.codec.binary.** +-keep class com.alibaba.idst.nui.*{*;} + +##高德 +#定位 +-keep class com.amap.api.location.**{*;} +-keep class com.amap.api.fence.**{*;} +-keep class com.loc.**{*;} +-keep class com.autonavi.aps.amapapi.model.**{*;} +#地图 +-keep class com.amap.api.maps.**{*;} +-keep class com.autonavi.**{*;} +-keep class com.amap.api.trace.**{*;} +#2D地图 +-keep class com.amap.api.maps2d.**{*;} +-keep class com.amap.api.mapcore2d.**{*;} +#搜索 +-keep class com.amap.api.services.**{*;} + +#room +-keep class androidx.room.** { *; } +-keepclassmembers class * { + @androidx.room.* ; + @androidx.room.* ; +} + +-keep interface com.cheng.bole.manager.db.* { *; } +-keepclassmembers class * extends androidx.room.RoomDatabase { + public (...); +} + +-keep class com.hjq.permissions.** {*;} + +-keep class org.libpag.** {*;} +-keep class androidx.exifinterface.** {*;} + +-dontwarn io.microshow.rxffmpeg.** +-keep class io.microshow.rxffmpeg.**{*;} + +# 华为推送 +-ignorewarnings +-keepattributes *Annotation* +-keepattributes Exceptions +-keepattributes InnerClasses +-keepattributes Signature +-keepattributes SourceFile,LineNumberTable +-keep class com.huawei.hianalytics.**{*;} +-keep class com.huawei.updatesdk.**{*;} +-keep class com.huawei.hms.**{*;} + +#腾讯语音识别 +-keepclasseswithmembernames class * { +native ; +} +-keep public class com.tencent.cloud.qcloudasrsdk.* + +-dontwarn com.bun.miitmdid.interfaces.IIdentifierListener +-dontwarn com.download.library.DownloadTask$DownloadTaskStatus +-dontwarn com.google.j2objc.annotations.Weak +-dontwarn com.oracle.svm.core.annotate.Delete +-dontwarn com.oracle.svm.core.annotate.TargetClass +-dontwarn com.sun.tools.javac.code.Attribute$UnresolvedClass +-dontwarn com.sun.tools.javac.code.Type$ClassType +-dontwarn javax.lang.model.SourceVersion +-dontwarn javax.lang.model.element.AnnotationMirror +-dontwarn javax.lang.model.element.AnnotationValue +-dontwarn javax.lang.model.element.AnnotationValueVisitor +-dontwarn javax.lang.model.element.Element +-dontwarn javax.lang.model.element.ElementKind +-dontwarn javax.lang.model.element.ExecutableElement +-dontwarn javax.lang.model.element.Modifier +-dontwarn javax.lang.model.element.Name +-dontwarn javax.lang.model.element.NestingKind +-dontwarn javax.lang.model.element.PackageElement +-dontwarn javax.lang.model.element.TypeElement +-dontwarn javax.lang.model.element.TypeParameterElement +-dontwarn javax.lang.model.element.VariableElement +-dontwarn javax.lang.model.type.DeclaredType +-dontwarn javax.lang.model.type.TypeKind +-dontwarn javax.lang.model.type.TypeMirror +-dontwarn javax.lang.model.type.TypeVariable +-dontwarn javax.lang.model.type.TypeVisitor +-dontwarn javax.lang.model.util.Elements +-dontwarn javax.lang.model.util.SimpleAnnotationValueVisitor7 +-dontwarn javax.lang.model.util.SimpleTypeVisitor7 +-dontwarn javax.lang.model.util.Types +-dontwarn org.bouncycastle.asn1.gm.GMNamedCurves +-dontwarn org.bouncycastle.asn1.x9.X9ECParameters +-dontwarn org.bouncycastle.crypto.digests.SM3Digest +-dontwarn org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey +-dontwarn org.bouncycastle.jce.provider.BouncyCastleProvider +-dontwarn org.bouncycastle.jce.spec.ECParameterSpec +-dontwarn org.bouncycastle.jce.spec.ECPublicKeySpec +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.bouncycastle.math.ec.ECCurve +-dontwarn org.bouncycastle.math.ec.ECPoint +-dontwarn org.bouncycastle.util.encoders.Hex +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE +-dontwarn com.efs.sdk.base.core.config.GlobalInfo +-dontwarn com.efs.sdk.base.core.config.GlobalInfoManager +-dontwarn coil.compose.EqualityDelegate +-dontwarn coil.compose.SingletonAsyncImageKt +-dontwarn android.os.SystemProperties \ No newline at end of file diff --git a/app/src/androidTest/java/cn/zuom8/phoneloca/ExampleInstrumentedTest.kt b/app/src/androidTest/java/cn/zuom8/phoneloca/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b4500ac --- /dev/null +++ b/app/src/androidTest/java/cn/zuom8/phoneloca/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.cheng.bole + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.cheng.bole", appContext.packageName) + } +} \ 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..95f13f0 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/fonts/Alimama ShuHeiTi.ttf b/app/src/main/assets/fonts/Alimama ShuHeiTi.ttf new file mode 100644 index 0000000..854c844 Binary files /dev/null and b/app/src/main/assets/fonts/Alimama ShuHeiTi.ttf differ diff --git a/app/src/main/assets/fonts/D-DIN-PRO-500-Medium.otf b/app/src/main/assets/fonts/D-DIN-PRO-500-Medium.otf new file mode 100644 index 0000000..d72d454 Binary files /dev/null and b/app/src/main/assets/fonts/D-DIN-PRO-500-Medium.otf differ diff --git a/app/src/main/assets/fonts/YouSheBiaoTiHei.ttf b/app/src/main/assets/fonts/YouSheBiaoTiHei.ttf new file mode 100644 index 0000000..3729151 Binary files /dev/null and b/app/src/main/assets/fonts/YouSheBiaoTiHei.ttf differ diff --git a/app/src/main/java/com/cheng/bole/BaseApplication.kt b/app/src/main/java/com/cheng/bole/BaseApplication.kt new file mode 100644 index 0000000..d38852b --- /dev/null +++ b/app/src/main/java/com/cheng/bole/BaseApplication.kt @@ -0,0 +1,90 @@ +package com.cheng.bole + +import com.cheng.bole.utils.ChannelUtils +import com.cheng.bole.common.Constants +import com.example.base.MvvmApplication +import com.example.base.utils.L +import com.example.base.utils.MMKVUtils +import com.example.base.utils.Utils +import com.g.gysdk.GYManager +import com.g.gysdk.GYResponse +import com.g.gysdk.GyCallBack +import com.g.gysdk.GyConfig +import com.scwang.smart.refresh.header.MaterialHeader +import com.scwang.smart.refresh.layout.SmartRefreshLayout +import com.tencent.mmkv.MMKV +import com.umeng.analytics.MobclickAgent +import com.umeng.commonsdk.UMConfigure +import com.umeng.socialize.PlatformConfig + + +class BaseApplication : MvvmApplication() { + + override fun onCreate() { + super.onCreate() + Utils.init(this) + MMKV.initialize(this) + preInitUM() + preInitGT() + initRefreshLayout() + } + + /** + * 初始化友盟 + */ + private fun preInitUM() { + UMConfigure.setLogEnabled(true) + + PlatformConfig.setFileProvider(Constants.AppFilter) + PlatformConfig.setWeixin(Constants.WechatAppId, Constants.WechatAppSecret) + MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO) + + UMConfigure.setProcessEvent(true) + } + + /** + * 个推初始化 + */ + private fun preInitGT() { + if (MMKVUtils.getBoolean("isAgree")) { + try { + GYManager.getInstance().preInit(this) //个验SDK初始化 + GYManager.getInstance().init( + GyConfig.with(this) + .preLoginUseCache(true)//预取号使用缓存,可以提高预取号的成功率,建议设置为true + .channel(ChannelUtils.getChannel())//应用渠道 + .eLoginDebug(BuildConfig.DEBUG)//运营商debug调试模式 + .debug(BuildConfig.DEBUG)//个验debug调试模式 + .callBack(object : GyCallBack { + override fun onSuccess(gyResponse: GYResponse) { + L.d("TAG-->>GYManager init onSuccess =${gyResponse.code}") + } + + override fun onFailed(gyResponse: GYResponse) { + L.d("TAG-->>GYManager init onFailed =${gyResponse.code}") + } + }).build() + ) + //预登录 + GYManager.getInstance().ePreLogin(8000, object : GyCallBack { + override fun onSuccess(gyResponse: GYResponse) { + L.d("TAG-->>GYManager ePreLogin onSuccess =${gyResponse.code}") + } + + override fun onFailed(gyResponse: GYResponse) { + L.d("TAG-->>GYManager ePreLogin onFailed =${gyResponse.code}") + } + }) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun initRefreshLayout() { + //设置全局的Header构建器 + SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, _ -> + MaterialHeader(context) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/AccountEntity.kt b/app/src/main/java/com/cheng/bole/bean/AccountEntity.kt new file mode 100644 index 0000000..c25a3cd --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/AccountEntity.kt @@ -0,0 +1,16 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class AccountEntity( + val avater: String, + val bind: List, + val create_time: String, + val name: String, + val phone: String, + val role: Int, + val temp: Boolean, + val user_id: String, + val vip_name: String, + val vip_type: String +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/AdIncentiveEntity.kt b/app/src/main/java/com/cheng/bole/bean/AdIncentiveEntity.kt new file mode 100644 index 0000000..f5f8371 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/AdIncentiveEntity.kt @@ -0,0 +1,13 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class AdIncentiveEntity( + val ad_code: String, + val ad_count: Int, + val ad_height: Int, + val ad_width: Int, + val is_visible: Boolean, + val reward_count: Int, + val reward_name: String +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/BannerEntity.kt b/app/src/main/java/com/cheng/bole/bean/BannerEntity.kt new file mode 100644 index 0000000..9f04f69 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/BannerEntity.kt @@ -0,0 +1,9 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class BannerEntity( + val image: String, + val page: String, + val type: String +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/ConfigEntity.kt b/app/src/main/java/com/cheng/bole/bean/ConfigEntity.kt new file mode 100644 index 0000000..f750bea --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/ConfigEntity.kt @@ -0,0 +1,39 @@ +package com.cheng.bole.bean + +import com.google.gson.annotations.SerializedName + +class ConfigEntity { + + @SerializedName("client.version.upgrade") + var versionEntity: com.cheng.bole.bean.VersionEntity? = null + + @SerializedName("client.weixin.share") //微信分享 + var wxShareEntity: com.cheng.bole.bean.WxShareEntity? = null + + @SerializedName("client.guide.pay.enable") + var guidePayEnable: Boolean? = true //引导页是否开启支付,默认可以 + + @SerializedName("client.guide.enable") + var guideEnable: Boolean? = true //是否开启引导页 + + @SerializedName("client.start.function.hint") + var guideHint: String? = "" + + @SerializedName("client.nologin.pay.enable") + var noLoginPayEnable: Boolean? = false + + @SerializedName("client.pay.agreement") //是否显示支付协议 + var payAgreementEnable: Boolean? = true + + @SerializedName("client.login.type") //登录方式 + var loginType: List? = emptyList() + + @SerializedName("client.banner.urls") //首页banner + var banners: List? = emptyList() + + @SerializedName("client.ad.switch") //广告总开关 + var adSwitch: Boolean = false + + @SerializedName("client.service.phone") //客服电话 + var servicePhoneList: List = emptyList() +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/CouponActivityEntity.kt b/app/src/main/java/com/cheng/bole/bean/CouponActivityEntity.kt new file mode 100644 index 0000000..958a8c1 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/CouponActivityEntity.kt @@ -0,0 +1,14 @@ +package com.ylqh.cube.bean + +import java.io.Serializable + +data class CouponActivityEntity( + val id: String = "", + val activity_desc: String = "", + val activity_name: String = "", + val activity_threshold: String = "", + val activity_type: String = "", + val activity_type_name: String = "", + val activity_value: String = "", + var enableAnim: Boolean = true +): Serializable \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/CouponEntity.kt b/app/src/main/java/com/cheng/bole/bean/CouponEntity.kt new file mode 100644 index 0000000..db29052 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/CouponEntity.kt @@ -0,0 +1,25 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class CouponEntity( + val id: String, + val status: String, + val status_name: String, + val threshold: String, + val coupon_desc: String, + val coupon_name: String, + val coupon_type: String, + val coupon_type_name: String, + val coupon_value: String, + val coupon_value_name: String, + val create_time: String, + val expire_time: String, + val expire_timestamp: String, + var isChecked: Boolean = false +) : Serializable { + companion object { + const val TYPE_CASH = "1" + const val TYPE_DISCOUNT = "2" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/LoginEntity.kt b/app/src/main/java/com/cheng/bole/bean/LoginEntity.kt new file mode 100644 index 0000000..914664e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/LoginEntity.kt @@ -0,0 +1,8 @@ +package com.cheng.bole.bean + +class LoginEntity { + val user_id: String = "" + val name: String = "" + val avater: String = "" + val token: String = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/NoticeEntity.kt b/app/src/main/java/com/cheng/bole/bean/NoticeEntity.kt new file mode 100644 index 0000000..8035b05 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/NoticeEntity.kt @@ -0,0 +1,11 @@ +package com.cheng.bole.bean + +data class NoticeEntity( + val loop: Boolean, + val notice: List? +) { + data class NoticeItem( + val message: String, + var loop: Boolean = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/OrderPayEntity.kt b/app/src/main/java/com/cheng/bole/bean/OrderPayEntity.kt new file mode 100644 index 0000000..d6e6453 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/OrderPayEntity.kt @@ -0,0 +1,16 @@ +package com.cheng.bole.bean + +class OrderPayEntity { + var appId = "" + var orderId = "" + var outTradeNo = "" + var payParam = "" + var payType = "" + + var nonceStr = "" + var `package` = "" + var partnerId = "" + var prepayId = "" + var sign = "" + var timeStamp = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/PushPayloadEntity.kt b/app/src/main/java/com/cheng/bole/bean/PushPayloadEntity.kt new file mode 100644 index 0000000..99241b0 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/PushPayloadEntity.kt @@ -0,0 +1,9 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class PushPayloadEntity( + val msgId: String, + val page: String, + val params: String +): Serializable \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/SendCodeEntity.kt b/app/src/main/java/com/cheng/bole/bean/SendCodeEntity.kt new file mode 100644 index 0000000..c879890 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/SendCodeEntity.kt @@ -0,0 +1,5 @@ +package com.cheng.bole.bean + +class SendCodeEntity { + val timestamp: String = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/UploadImgEntity.kt b/app/src/main/java/com/cheng/bole/bean/UploadImgEntity.kt new file mode 100644 index 0000000..111338b --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/UploadImgEntity.kt @@ -0,0 +1,8 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +class UploadImgEntity : Serializable { + var id = "" + var url = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/UserConfigEntity.kt b/app/src/main/java/com/cheng/bole/bean/UserConfigEntity.kt new file mode 100644 index 0000000..5edd7d1 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/UserConfigEntity.kt @@ -0,0 +1,11 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +class UserConfigEntity : Serializable{ + var token = "" + var temp = false + var name = "" + var user_id = "" + var config: com.cheng.bole.bean.ConfigEntity? = null +} diff --git a/app/src/main/java/com/cheng/bole/bean/UserEntity.kt b/app/src/main/java/com/cheng/bole/bean/UserEntity.kt new file mode 100644 index 0000000..fbe2f2a --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/UserEntity.kt @@ -0,0 +1,34 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class UserEntity( + val appleid: String, + val avater: String, + val balance: String, + val city: String, + val client_cid: String, + val country: String, + val coupon_count: Int, + val imei: String, + val month_download_count: String, + val month_download_size: String, + val name: String, + val oaid: String, + val os_version: String, + val phone: String, + val province: String, + val role: String, + val sex: Int, + val show_contact_menu: Boolean, + val show_masonry_menu: Boolean, + val temp: Boolean, + val unionid: String, + val user_id: String, + val vip: String, // 1非会员 2会员 3终生会员 + val vip_expire: String, + val vip_expire_time: String, + val vip_name: String, + val weixinAppOpenId: String, + val ip_area: String +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/VersionEntity.kt b/app/src/main/java/com/cheng/bole/bean/VersionEntity.kt new file mode 100644 index 0000000..b7348fc --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/VersionEntity.kt @@ -0,0 +1,15 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +class VersionEntity : Serializable { + + var description = "" + var force = false + var last_version_force = "" + var title = "" + var url = "" + var app_size = "" + var version = "" + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/VipGoodsEntity.kt b/app/src/main/java/com/cheng/bole/bean/VipGoodsEntity.kt new file mode 100644 index 0000000..3eaa5e7 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/VipGoodsEntity.kt @@ -0,0 +1,14 @@ +package com.cheng.bole.bean + +class VipGoodsEntity { + var checked: Boolean = false + var goods_id: String = "" + var goods_name: String = "" + var origin_price: String = "" + var pay_type: String = "" + var price: String = "" + var single_pay_price: String = "" + var tips: String = "" + var sign_value = "" + var value: String = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/VipPermissionEntity.kt b/app/src/main/java/com/cheng/bole/bean/VipPermissionEntity.kt new file mode 100644 index 0000000..5a3cea9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/VipPermissionEntity.kt @@ -0,0 +1,23 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class VipPermissionEntity( + val auth: Boolean, + val auth_ad: Boolean, + val scene: String, + val user_id: Int, + val vip: Int, + val vip_expire: String, + val vip_expire_time: String, + val vip_goods_type: String, + val vip_message: String, + val vip_name: String, + var type: Int +) : Serializable { + + companion object { + const val TYPE_SHARE = 1 + const val TYPE_DOWNLOAD = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/VipTipItemEntity.kt b/app/src/main/java/com/cheng/bole/bean/VipTipItemEntity.kt new file mode 100644 index 0000000..77d43d4 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/VipTipItemEntity.kt @@ -0,0 +1,24 @@ +package com.cheng.bole.bean + +import com.cheng.bole.R + +class VipTipItemEntity(var icon: Int, var name: String) { + companion object { + fun getVipTipList(): List { + val list = ArrayList() + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item1, "素材图库")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item2, "魔方工具")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item3, "高清图片")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item4, "AI创作")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item5, "视频提取")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item6, "音频提取")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item7, "文案提取")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item8, "无广告")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item9, "海量次数")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item10, "海量流量")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item11, "专属客服")) + list.add(com.cheng.bole.bean.VipTipItemEntity(R.mipmap.ic_vip_item12, "在线浏览")) + return list + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/WxServiceEntity.kt b/app/src/main/java/com/cheng/bole/bean/WxServiceEntity.kt new file mode 100644 index 0000000..cfb6698 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/WxServiceEntity.kt @@ -0,0 +1,10 @@ +package com.cheng.bole.bean + +import com.google.gson.annotations.SerializedName + +class WxServiceEntity { + var corpid = "" + + @SerializedName("kf.address") + var address = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/bean/WxShareEntity.kt b/app/src/main/java/com/cheng/bole/bean/WxShareEntity.kt new file mode 100644 index 0000000..f5bb3d9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/bean/WxShareEntity.kt @@ -0,0 +1,10 @@ +package com.cheng.bole.bean + +import java.io.Serializable + +data class WxShareEntity( + val content: String = "", + val image: String = "", + val link: String = "", + val title: String = "" +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/common/AppUpdate.kt b/app/src/main/java/com/cheng/bole/common/AppUpdate.kt new file mode 100644 index 0000000..42012ab --- /dev/null +++ b/app/src/main/java/com/cheng/bole/common/AppUpdate.kt @@ -0,0 +1,75 @@ +package com.cheng.bole.common + +import android.app.Activity +import android.graphics.Color +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.azhon.appupdate.manager.DownloadManager +import com.cheng.bole.BuildConfig +import com.cheng.bole.R +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.dialog.UpdateVersionDialog +import com.cheng.bole.utils.UIUtils +import com.example.base.extensions.toast + +object AppUpdate { + + private val apkName = "appupdate.apk" + + fun checkUpdate(fragment: Fragment, isManual: Boolean, clickFun: () -> Unit?) { + + if (isManual) { + toast(fragment.getString(R.string.check_version_ing)) + } + + val result: com.cheng.bole.bean.VersionEntity? = UserConfigManager.userConfig?.config?.versionEntity + result?.apply { + if (UIUtils.checkVersion(version, BuildConfig.VERSION_NAME)) { + if (last_version_force == BuildConfig.VERSION_NAME) { + force = true + } else if (UIUtils.checkVersion(last_version_force, BuildConfig.VERSION_NAME)) { + force = true + } + clickFun.invoke() + com.cheng.bole.common.AppUpdate.startUpdate(result, fragment) + } else { + if (isManual) { + toast(fragment.getString(R.string.curr_new_version)) + } + } + } + } + + private fun startUpdate(versionEntity: com.cheng.bole.bean.VersionEntity, fragment: Fragment) { + UpdateVersionDialog.newInstance(versionEntity).show(fragment.childFragmentManager, null) + } + + private fun startUpdateDialog(result: com.cheng.bole.bean.VersionEntity?, activity: Activity) { + if (result == null) return + val manager = DownloadManager.Builder(activity).run { + apkUrl(result.url) + apkName(com.cheng.bole.common.AppUpdate.apkName) + smallIcon(R.mipmap.ic_launcher_icon) + dialogImage(R.mipmap.ic_update_version_bg_z) + showNewerToast(true) + apkVersionName(result.version) + apkSize(result.app_size) + apkDescription(result.description) + enableLog(true) + jumpInstallPage(true) + dialogButtonTextColor(Color.WHITE) + showNotification(true) + showBgdToast(false) + when (result.force) { + true -> forcedUpgrade(true) + else -> forcedUpgrade(false) + } + dialogButtonColor(ContextCompat.getColor(activity, R.color.colorAccent)) + dialogProgressBarColor(ContextCompat.getColor(activity, R.color.colorAccent)) + notifyId(1011) + build() + } + manager.download() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/common/Constants.kt b/app/src/main/java/com/cheng/bole/common/Constants.kt new file mode 100644 index 0000000..2cc160d --- /dev/null +++ b/app/src/main/java/com/cheng/bole/common/Constants.kt @@ -0,0 +1,34 @@ +package com.cheng.bole.common + +import android.graphics.Typeface +import com.cheng.bole.BuildConfig +import com.example.base.utils.Utils + +object Constants { + + const val BaseUrl = "http://bid7.yua8.cn"//正式地址 + const val TestUrl = "http://bid7.yua8.cn"//测试地址 + + const val APP_ID = "10055" + + const val AppFilter = "${BuildConfig.APPLICATION_ID}.fileprovider" + + const val WechatAppId = ""//微信APPID + const val WechatAppSecret = ""//微信secret + const val UmengAppkey = "692528cc8560e34872f36551"//友盟appKey + + const val userAgreement = "$BaseUrl/static/new5/user.html"//用户协议 + const val privacyPolicy = "$BaseUrl/static/new5/provacy.html"//隐私政策 + const val renewAgreement = "$BaseUrl/static/new5/renew.html"//自动续费协议 + const val permissionList: String = "${BaseUrl}/static/new5/limits.html"//权限说明 + const val shareList: String = "${BaseUrl}/static/new5/shareList.html"//第三方共享清单 + const val sdkList: String = "${BaseUrl}/static/new5/sdkList.html"//第三方SDK目录 + + const val Encrypt = "zpzkfp72v3hgatzg5w7pyg86x5342kxt" + const val Signature = "ckBHUSWBx3TqwNT2kxMrsXyXFuA3PW" + + val almmsht = Typeface.createFromAsset(Utils.getApp().assets, "fonts/Alimama ShuHeiTi.ttf") + val dDIN_PRO_M = Typeface.createFromAsset(Utils.getApp().assets, "fonts/D-DIN-PRO-500-Medium.otf") + val youSheBiaoTiHei = Typeface.createFromAsset(Utils.getApp().assets, "fonts/YouSheBiaoTiHei.ttf") + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/common/EventConstants.kt b/app/src/main/java/com/cheng/bole/common/EventConstants.kt new file mode 100644 index 0000000..270bc86 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/common/EventConstants.kt @@ -0,0 +1,270 @@ +package com.cheng.bole.common + +object EventConstants { + const val APP_LAUNCH = "client.launch" //app启动 + + const val GUIDE_LAUNCH = "client.guide.launch" //引导页启动 + + const val GUIDE_OPPORTUNITY_SCROLL = "client.guide.content.scroll" //滑动切换引导页内容 + + const val GUIDE_SKIP = "client.guide.pay.skip" //跳过引导页支付 + + const val HOME_BOTTOM_TAB_CHECK = "client.main.bottom.tab.check" //底部tab切换 + + const val HOME_NOTICE_CHECK = "client.main.notice.check" //首页通知点击 + + const val GOODS_SELECT = "client.goods.select" //点击切换支付的会员类型 + + const val PAY_SELECT = "client.pay.select" //点击切换支付类型 + + const val PAY_PAY = "client.pay.pay" //支付按钮点击 + + const val CHALLENGE_TASK_PAY_PAY = "client.challenge.task.pay.pay" //0元挑战支付按钮点击 + + const val PAY_SUCCESS = "client.pay.success" //支付成功 + + const val PAY_CANCEL = "client.pay.cancel" //支付取消 + + const val ERROR_CLIENT_WXPAY_ERR = "client.wxpay.err" //微信支付失败 + + const val ERROR_CLIENT_ALIPAY_ERR = "client.alipay.err" //支付宝支付失败 + + const val ERROR_CLIENT_DOWNLOAD_IMG = "client.download.img.err" //图片下载失败 + + const val ERROR_CLIENT_DOWNLOAD_VIDEO = "client.download.video.err" //视频下载失败 + + const val ERROR_CLIENT_DOWNLOAD_AUDIO = "client.download.audio.err" //音频下载失败 + + const val CANCEL_DOWNLOAD_VIDEO = "client.download.video.cancel" //取消视频下载 + + const val PAUSE_DOWNLOAD_VIDEO = "client.download.video.pause" //暂停视频下载 + + const val CONTINUE_DOWNLOAD_VIDEO = "client.download.video.continue" //继续视频下载 + + const val RESTART_DOWNLOAD_VIDEO = "client.download.video.restart" //重新视频下载 + + const val SPEED_UP_DOWNLOAD_VIDEO = "client.download.video.speed.up" //加速视频下载 + + const val BACKGROUND_CLIENT_DOWNLOAD = "client.download.background" //后台下载 + + const val FLOAT_WINDOW_CLICK = "client.float.window.click" //点击悬浮窗 + + const val SAVE_AI_MEDIA = "client.ai.media.save" //ai生成文件保存 + + const val RECOGNIZE_AUDIO_TO_TEXT = "client.audio.text.recognize" //音频转文字 + + const val PKG_UPDATE = "client.pkg.update" //升级弹窗点击更新 + + const val PKG_CANCEL = "client.pkg.cancel" //升级弹窗点击取消 + + const val GET_MATERIAL = "client.get.material" //获取素材 + + const val GET_MATERIAL_CANCEL = "client.get.material.cancel" //取消获取素材 + + const val DIALOG_CONFIRM_SAVE_FILE = "client.dialog.confirm.save.file" //保存文件地址弹框确认 + + const val DIALOG_GO_TO_VIEW = "client.dialog.go.to.view" //前往保存文件的地址查看 + + const val JUMP_TO_ABOUT_US = "client.jump.to.about.us" //界面跳转 + + const val JUMP_TO_LINK_EXTRACT = "client.jump.to.link.extract" //跳转链接提取 + + const val JUMP_TO_WECHAT_VIDEO = "client.jump.to.wechat.video" //跳转视频号 + + const val JUMP_TO_WECHAT_PLAYBACK = "client.jump.to.wechat.video.playback" //跳转直播回放 + + const val JUMP_TO_COURSE_WX_VIDEO = "client.course.wechat.video" //视频号视频教程 + + const val JUMP_TO_COURSE_PLAYBACK = "client.course.playback" //直播回放视频教程 + + const val JUMP_TO_TOOL = "client.jump.to.home.tool" //跳转工具 + + const val MAIN_CENTER_ENABLE = "client.main.center.enable" //首页跳转个人中心 + + const val JUMP_TO_MEMBER_RECHARGE = "client.jump.to.member.recharge" //跳转到充值页 + + const val JUMP_TO_LOGIN = "client.jump.to.login" //跳转到登录页 + + const val JUMP_TO_SYSTEM_SETTING = "client.jump.to.system.setting" //跳转到系统设置 + + const val JUMP_TO_USER_SETTING = "client.jump.to.user.setting" //跳转到用户设置 + + const val JUMP_TO_FEEDBACK = "client.jump.to.feedback" //跳转到意见反馈 + + const val JUMP_TO_ACCOUNT_BIND = "client.jump.to.account.bind" //跳转到账号绑定 + + const val JUMP_TO_ACCOUNT_MANAGE = "client.jump.to.account.manage" //跳转到账号管理 + + const val JUMP_TO_DOWNLOAD_HISTORY = "client.jump.to.download.history" //跳转到下载记录 + + const val JUMP_TO_DOWNLOAD_TASK_LIST = "client.jump.to.download.task.list" //跳转到下载任务列表 + + const val JUMP_TO_RECHARGE_DIAMOND = "client.jump.to.recharge.diamond" //跳转到M币充值 + + const val JUMP_TO_COUPON_LIST = "client.jump.to.coupon.list" //跳转优惠券列表 + + const val JUMP_TO_CHALLENGE_TASK = "client.jump.to.challenge.task" //跳转到0元挑战 + + const val JUMP_TO_SHARE_WX_VIDEO = "client.jump.to.wechat.share.video" //跳转到视频号分享 + + const val JUMP_TO_SHARE_WX_PLAYBACK = "client.jump.to.wechat.share.playback" //跳转到直播回放分享 + + const val JUMP_TO_COURSE = "client.jump.to.course" //跳转到指导教程 + + const val DOWNLOAD_FILE = "client.download.file" //下载文件 + + const val DOWNLOAD_FILE_SUCCESS = "client.download.file.success" //下载文件成功 + + const val DOWNLOAD_FILE_ERROR = "client.download.file.error" //下载文件失败 + + const val TRANSPOND_FILE = "client.transpond.file" //转发文件 + + const val MATERIAL_COPY_TEXT = "client.material.copy.text" //复制文字 + + const val MATERIAL_TYPE_CHECK = "client.material.type.check" //素材切换 + + const val MATERIAL_ALL_SELECT = "client.material.all.select" //全部选中素材 + + const val MATERIAL_SELECT = "client.material.select" //选择素材 + + const val TOOLS_VIDEO_EXTRACT_AUDIO = "client.tools.video.audio" //提取音频 + + const val MATERIAL_PLAY_VIDEO = "client.material.play.video" //播发视频 + + const val GET_CODE = "client.get.code" //获取验证码 + + const val LOGIN = "client.login" //登录 + + const val SWITCH_ACCOUNT = "client.switch.account" //切换账户 + + const val ACCOUNT_BIND = "client.account.bind" //绑定账号 + + const val ACCOUNT_BIND_CANCEL = "client.account.bind.cancel" //取消绑定账号 + + const val CHECK_AGREEMENT = "client.check.agreement" //切换协议状态 + + const val VIEW_AGREEMENT = "client.view.agreement" //查看用户协议 + + const val PRIVACY_POLICY_CLICK_OK = "client.privacy.policy.click.ok" //同意隐私协议 + + const val SAVE_USER_CONFIG_ERROR = "client.save.user.config.error" //保存用户配置错误 + + const val SHARE_APP = "client.share.app" //分享app + + const val CLEAR_CACHE = "client.clear.cache" //清除缓存 + + const val CONTACT_SERVICE = "client.contact.service" //联系客服 + + const val EXIT_LOGIN = "client.exit.login" //退出登录 + + const val CANCEL_ACCOUNT = "client.cancel.account" //注销账户 + + const val MEMBER_FORCE_LOGIN = "client.member.force.login" //会员强制登录 + + const val GET_MATERIAL_TIMES_USE_UP = "client.times.use.up.get.material" //获取素材次数已用完 + + const val PICTURE_HANDLE_TIMES_USE_UP = "client.times.use.up.picture.handle" //图片处理次数已用完 + + const val CHECK_LOGIN_TYPE = "client.check.login.type" //切换登录方式 + + const val OPEN_SCREEN_AD_SHOW = "client.ad.open.screen.show" //开屏广告展示 + + const val OPEN_SCREEN_AD_SKIP = "client.ad.open.screen.skip" //开屏广告跳过 + + const val OPEN_SCREEN_AD_CLICK = "client.ad.open.screen.click" //开屏广告点击 + + const val BANNER_AD_SHOW = "client.ad.banner.show" //banner广告展示 + + const val BANNER_AD_CLOSE = "client.ad.banner.close" //banner广告关闭 + + const val BANNER_AD_CLICK = "client.ad.banner.click" //banner广告点击 + + const val INSERT_SCREEN_AD_SHOW = "client.ad.insert.screen.show" //插屏广告展示 + + const val INSERT_SCREEN_AD_CLOSE = "client.ad.insert.screen.close" //插屏广告关闭 + + const val INSERT_SCREEN_AD_CLICK = "client.ad.insert.screen.click" //插屏广告点击 + + const val INSERT_SCREEN_AD_SKIP_VIDEO = "client.ad.insert.screen.skip.video" //跳过插屏广告 + + const val INCENTIVE_AD_SHOW = "client.ad.incentive.show" //激励广告展示 + + const val INCENTIVE_AD_CLOSE = "client.ad.incentive.close" //激励广告关闭 + + const val INCENTIVE_AD_REWARD = "client.ad.incentive.reward" //激励广告已获取到奖励 + + const val INCENTIVE_AD_SKIP_VIDEO = "client.ad.incentive.skip.video" //跳过激励广告 + + const val ACCOUNT_UNBIND = "client.account.unbind" //解除绑定账号 + + const val HISTORY_RECORD_TYPE_CHECK = "client.history.record.type.check" //历史记录切换 + + const val CLOSE_FREE_TIME_USES_UP_DIALOG = + "client.free.time.uses.up.dialog.close" //关闭免费次数用完的提示框 + + const val CHECK_FREE_TIME_USES_UP_DIALOG = + "client.free.time.uses.up.dialog.check" //免费次数用完的提示框切换充值类型 + + const val CONFIRM_FREE_TIME_USES_UP_DIALOG = + "client.free.time.uses.up.dialog.confirm" //确认免费次数用完的提示框 + + const val MULTI_DELETE_FILE = "client.multi.delete.file" //批量删除文件 + + const val MULTI_TRANSMIT_FILE = "client.multi.transmit.file" //批量转发文件 + + const val PREVIEW_DELETE_FILE = "client.preview.delete.file" //预览时删除文件 + + const val PREVIEW_TRANSMIT_FILE = "client.preview.transmit.file" //预览时删除文件 + + const val PREVIEW_HANDLE_IMAGE = "client.preview.handle.image" //预览时进行图片处理 + + const val TOOLS_HANDLE_IMAGE_START = "client.tools.handle.image.start" //图片处理开始 + + const val TOOLS_HANDLE_SAVE_IMAGE = "client.tools.handle.save.image" //保存已处理过的图片至相册 + + const val AUTO_SWITCH_DOWNLOAD_URL = "client.auto.switch.download.url" //自动切换下载链接 + + const val HAND_SWITCH_DOWNLOAD_URL = "client.hand.switch.download.url" //手动切换下载链接立即加速 + + const val HOME_BANNER_CLICK = "client.home.banner.click" + + const val COUPON_ANIMATION_PLAY = "client.coupon.animation.play" //播放优惠券动画 + + const val COUPON_ANIMATION_CLOSE = "client.coupon.animation.close" //关闭优惠券动画 + + const val COUPON_RECEIVE = "client.coupon.receive" //领取优惠券 + + const val COUPON_REDEEM_ENABLE = "client.coupon.redeem.enable" //优惠券兑换按钮点击 + + const val COUPON_REDEEM_INFO = "client.coupon.redeem.info" //优惠券兑换详情 + + const val COUPON_REDEEM = "client.coupon.redeem" //优惠券兑换 + + const val COUPON_REDEEM_SUCCESS = "client.coupon.redeem.success" //优惠券兑换成功 + + const val COUPON_REDEEM_SUCCESS_CONFIRM = "client.coupon.redeem.success.confirm" //优惠券兑换成功 + + const val COUPON_VIEW = "client.coupon.view" //查看优惠券 + + const val COUPON_DIALOG_CHECK = "client.coupon.dialog.check" //切换优惠券 + + const val COUPON_DIALOG_CLOSE = "client.coupon.dialog.close" //关闭优惠券 + + const val COUPON_DIALOG_CONFIRM = "client.coupon.dialog.confirm" //确认优惠券 + + const val COPY_USER_ID = "client.copy_user_id" //复制用户id + + const val SHOW_PALYBACK_HINT_DIALOG = "client.show.playback.hint.dialog" + + const val EXIT_APP = "client.exit.app" //退出APP + + const val SHOW_DIALOG = "client.show.dialog" //弹出退出app的弹框 + + const val START_COUPON_ANIMATION = "client.start.coupon.animation" + + const val CHALLENGE_TASK_SIGN_IN = "client.challenge.tasK.sign.in" //签到 + + const val CHALLENGE_TASK_SIGN_IN_SUCCESS = "client.challenge.tasK.sign.in.success" //签到成功 + + const val CHALLENGE_TASK_SIGN_IN_FAIL = "client.challenge.tasK.sign.in.fail" //签到失败 +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/ClipboardEvent.kt b/app/src/main/java/com/cheng/bole/event/ClipboardEvent.kt new file mode 100644 index 0000000..7890992 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/ClipboardEvent.kt @@ -0,0 +1,3 @@ +package com.cheng.bole.event + +class ClipboardEvent(var text: String) \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/CouponActivityEvent.kt b/app/src/main/java/com/cheng/bole/event/CouponActivityEvent.kt new file mode 100644 index 0000000..59ffff9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/CouponActivityEvent.kt @@ -0,0 +1,5 @@ +package com.cheng.bole.event + +import com.ylqh.cube.bean.CouponActivityEntity + +class CouponActivityEvent(var list: List, var type: Int)// 0 首页, 1 enter, 2 exit \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/CouponRefreshEvent.kt b/app/src/main/java/com/cheng/bole/event/CouponRefreshEvent.kt new file mode 100644 index 0000000..e182f8f --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/CouponRefreshEvent.kt @@ -0,0 +1,4 @@ +package com.cheng.bole.event + +class CouponRefreshEvent { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/HomeRefreshEvent.kt b/app/src/main/java/com/cheng/bole/event/HomeRefreshEvent.kt new file mode 100644 index 0000000..9bcaf3e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/HomeRefreshEvent.kt @@ -0,0 +1,4 @@ +package com.cheng.bole.event + +class HomeRefreshEvent { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/LoginSuccessEvent.kt b/app/src/main/java/com/cheng/bole/event/LoginSuccessEvent.kt new file mode 100644 index 0000000..43da29b --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/LoginSuccessEvent.kt @@ -0,0 +1,4 @@ +package com.cheng.bole.event + +class LoginSuccessEvent { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/LogoutSuccessEvent.kt b/app/src/main/java/com/cheng/bole/event/LogoutSuccessEvent.kt new file mode 100644 index 0000000..22e2a69 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/LogoutSuccessEvent.kt @@ -0,0 +1,4 @@ +package com.cheng.bole.event + +class LogoutSuccessEvent { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/MineRefreshEvent.kt b/app/src/main/java/com/cheng/bole/event/MineRefreshEvent.kt new file mode 100644 index 0000000..3554a15 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/MineRefreshEvent.kt @@ -0,0 +1,4 @@ +package com.cheng.bole.event + +class MineRefreshEvent { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/PayStatusEnum.kt b/app/src/main/java/com/cheng/bole/event/PayStatusEnum.kt new file mode 100644 index 0000000..8076949 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/PayStatusEnum.kt @@ -0,0 +1,5 @@ +package com.cheng.bole.event + +enum class PayStatusEnum { + PAY_SUCCESS, PAY_ERROR, PAY_CANCEL +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/PayStatusEvent.kt b/app/src/main/java/com/cheng/bole/event/PayStatusEvent.kt new file mode 100644 index 0000000..a8709d7 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/PayStatusEvent.kt @@ -0,0 +1,3 @@ +package com.cheng.bole.event + +class PayStatusEvent(var payStatus: com.cheng.bole.event.PayStatusEnum, var message: String = "") \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/PushMessageEvent.kt b/app/src/main/java/com/cheng/bole/event/PushMessageEvent.kt new file mode 100644 index 0000000..096ecda --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/PushMessageEvent.kt @@ -0,0 +1,6 @@ +package com.cheng.bole.event + +class PushMessageEvent( + var payload: String, + var type: Int //2展示通知 3点击通知 +) \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/UserInfoEvent.kt b/app/src/main/java/com/cheng/bole/event/UserInfoEvent.kt new file mode 100644 index 0000000..6233323 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/UserInfoEvent.kt @@ -0,0 +1,4 @@ +package com.cheng.bole.event + +class UserInfoEvent { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/VipPaySuccessEvent.kt b/app/src/main/java/com/cheng/bole/event/VipPaySuccessEvent.kt new file mode 100644 index 0000000..cb111fa --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/VipPaySuccessEvent.kt @@ -0,0 +1,4 @@ +package com.cheng.bole.event + +class VipPaySuccessEvent { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/event/WxLoginEvent.kt b/app/src/main/java/com/cheng/bole/event/WxLoginEvent.kt new file mode 100644 index 0000000..b4db26d --- /dev/null +++ b/app/src/main/java/com/cheng/bole/event/WxLoginEvent.kt @@ -0,0 +1,3 @@ +package com.cheng.bole.event + +class WxLoginEvent(var code: String, var state: String) \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ext/NumberExt.kt b/app/src/main/java/com/cheng/bole/ext/NumberExt.kt new file mode 100644 index 0000000..7e2acd2 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ext/NumberExt.kt @@ -0,0 +1,7 @@ +package com.cheng.bole.ext + +import java.text.DecimalFormat + +fun Number.format(pattern: String = "0.##"): String { + return DecimalFormat(pattern).format(this) +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ext/StringExt.kt b/app/src/main/java/com/cheng/bole/ext/StringExt.kt new file mode 100644 index 0000000..d2da7c9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ext/StringExt.kt @@ -0,0 +1,9 @@ +package com.cheng.bole.ext + +import java.math.BigDecimal +import java.math.RoundingMode + +fun String.format(newScale: Int = 2, roundingMode: RoundingMode = RoundingMode.HALF_UP): BigDecimal { + val bd = BigDecimal(this) + return bd.setScale(newScale, roundingMode) +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ext/ViewPager2Ext.kt b/app/src/main/java/com/cheng/bole/ext/ViewPager2Ext.kt new file mode 100644 index 0000000..9d8e6b3 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ext/ViewPager2Ext.kt @@ -0,0 +1,34 @@ +package com.cheng.bole.ext + +import android.animation.Animator +import android.animation.TimeInterpolator +import android.animation.ValueAnimator +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.viewpager2.widget.ViewPager2 + +fun ViewPager2.setCurrentItem( + item: Int, + duration: Long, + interpolator: TimeInterpolator = AccelerateDecelerateInterpolator(), + pagePxWidth: Int = width, + pagePxHeight: Int = height +) { + val pxToDrag: Int = if (orientation == ViewPager2.ORIENTATION_HORIZONTAL) pagePxWidth * (item - currentItem) else pagePxHeight * (item - currentItem) + val animator = ValueAnimator.ofInt(0, pxToDrag) + var previousValue = 0 + animator.addUpdateListener { valueAnimator -> + val currentValue = valueAnimator.animatedValue as Int + val currentPxToDrag = (currentValue - previousValue).toFloat() + fakeDragBy(-currentPxToDrag) + previousValue = currentValue + } + animator.addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) { beginFakeDrag() } + override fun onAnimationEnd(animation: Animator) { endFakeDrag() } + override fun onAnimationCancel(animation: Animator) { /* Ignored */ } + override fun onAnimationRepeat(animation: Animator) { /* Ignored */ } + }) + animator.interpolator = interpolator + animator.duration = duration + animator.start() +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/impl/OnSeekBarChangeListenerImpl.kt b/app/src/main/java/com/cheng/bole/impl/OnSeekBarChangeListenerImpl.kt new file mode 100644 index 0000000..2d5f91a --- /dev/null +++ b/app/src/main/java/com/cheng/bole/impl/OnSeekBarChangeListenerImpl.kt @@ -0,0 +1,19 @@ +package com.cheng.bole.impl + +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener + +abstract class OnSeekBarChangeListenerImpl: OnSeekBarChangeListener { + + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/impl/TextWatcherImpl.kt b/app/src/main/java/com/cheng/bole/impl/TextWatcherImpl.kt new file mode 100644 index 0000000..c6824a9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/impl/TextWatcherImpl.kt @@ -0,0 +1,19 @@ +package com.cheng.bole.impl + +import android.text.Editable +import android.text.TextWatcher + +abstract class TextWatcherImpl: TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + + } + + override fun afterTextChanged(s: Editable?) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/manager/DialogEnum.kt b/app/src/main/java/com/cheng/bole/manager/DialogEnum.kt new file mode 100644 index 0000000..395a488 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/manager/DialogEnum.kt @@ -0,0 +1,6 @@ +package com.cheng.bole.manager + +enum class DialogEnum { + CLICK_OK, + CLICK_CANCEL +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/manager/EventReportManager.kt b/app/src/main/java/com/cheng/bole/manager/EventReportManager.kt new file mode 100644 index 0000000..e0de4c3 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/manager/EventReportManager.kt @@ -0,0 +1,31 @@ +package com.cheng.bole.manager + +import com.alibaba.fastjson.JSONObject +import com.cheng.bole.net.ApiFactory +import com.example.base.utils.L +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +object EventReportManager { + + fun eventReport(key: String?, value: String?, extra: String) { + GlobalScope.launch { + try { + val jsonObject = JSONObject() + jsonObject["source"] = "android" + jsonObject["type"] = "click" + jsonObject["key"] = key//点击的地方 + jsonObject["value"] = value + jsonObject["extra"] = extra//附加参数 + L.d("TAG-->>${jsonObject}") + ApiFactory.apiService.eventReport(jsonObject.toString().toRequestBody("application/json".toMediaType())) + } catch (e: Exception) { + e.printStackTrace() + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/manager/LoginManager.kt b/app/src/main/java/com/cheng/bole/manager/LoginManager.kt new file mode 100644 index 0000000..995cf1e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/manager/LoginManager.kt @@ -0,0 +1,39 @@ +package com.cheng.bole.manager + +import com.example.base.utils.MMKVUtils + +object LoginManager { + + fun saveToken(token: String) { + MMKVUtils.put("x-token", token) + } + + fun getToken(): String { + return MMKVUtils.getString("x-token") ?: "" + } + + fun isLogin(): Boolean { + return !UserConfigManager.getIsTemp() + } + + fun saveLastLoginType(type: String) { + MMKVUtils.put("last_login_type", type) + } + + fun getLastLoginType(): String? { + return MMKVUtils.getString("last_login_type") + } + + /** + * 退出登录 + */ + fun logout() { + // 1. 清除用户数据 + MMKVUtils.put("x-role", true) + MMKVUtils.removeKey("userConfig") + MMKVUtils.removeKey("x-token") + MMKVUtils.removeKey("guide_pay") + MMKVUtils.removeKey("guide") + MMKVUtils.removeKey("nologin_pay") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/manager/NotificationHelper.kt b/app/src/main/java/com/cheng/bole/manager/NotificationHelper.kt new file mode 100644 index 0000000..4c78f0b --- /dev/null +++ b/app/src/main/java/com/cheng/bole/manager/NotificationHelper.kt @@ -0,0 +1,117 @@ +package com.cheng.bole.manager + +import android.app.Activity +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.fragment.app.FragmentManager +import com.cheng.bole.R +import com.cheng.bole.ui.dialog.TipDialog +import com.example.base.extensions.toast +import com.example.base.utils.Utils + + +object NotificationHelper { + private val notificationManager by lazy { Utils.getApp().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } + + private const val DEFAULT_CHANNEL_ID = "default" + private const val DOWNLOAD_CHANNEL_ID = "download" + private const val FOREGROUND_CHANNEL_ID = "foreground" + + private const val DOWNLOAD_GROUP = "download_group" + + private const val DOWNLOAD_CANCEL_CODE = 2000 + private const val DOWNLOAD_RETRY_CODE = 3000 + + private var _notificationId = 1000 + + /** + * 创建默认通知通道 + */ + fun createDefaultChannel() { + createNotificationChannel(DEFAULT_CHANNEL_ID, "Default", NotificationManager.IMPORTANCE_DEFAULT) + } + + /** + * 前台通知 + */ + fun startForeground(service: Service) { + val channelId = createNotificationChannel(FOREGROUND_CHANNEL_ID, "Foreground", NotificationManager.IMPORTANCE_LOW) + val builder = NotificationCompat.Builder(service, channelId) + val notification = builder + .setContentText("文件下载服务运行中") + .setSmallIcon(R.mipmap.ic_launcher_icon) + .setCategory(Notification.CATEGORY_SERVICE) + .build() + service.startForeground(1, notification) + } + + /** + * 取消通知 + */ + fun cancelNotification(id: Int) { + notificationManager.cancel(id) + } + + /** + * 创建通知通道 + * + * @param channelId + * @param channelName + * @return + */ + private fun createNotificationChannel(channelId: String, channelName: String, importance: Int): String { + val channel = NotificationChannel(channelId, channelName, importance) + channel.setSound(null, null) + notificationManager.createNotificationChannel(channel) + return channelId + } + + fun getNotificationId(): Int { + return ++_notificationId + } + + /** + * 判断通知权限是否开启 + */ + fun isNotificationEnabled(context: Context): Boolean { + return NotificationManagerCompat.from(context).areNotificationsEnabled() + } + + /** + * 通知被禁用时显示提示 + */ + fun showDisabledTip(activity: Activity, fm: FragmentManager, isHomePage: Boolean = false) { + if (isHomePage) { + toast("平台推送消息需要通知权限,检测到您已关闭相关权限,请前往设置手动打开") + } else { + val f = TipDialog.newInstance("请开启通知权限", "为方便查看下载进度,请在设置中打开【允许通知】") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + try { + //8.0及以上 + val intent = Intent() + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName) + activity.startActivity(intent) + } catch (e: Exception) { + val intent = Intent() + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.setData(Uri.fromParts("package", activity.packageName, null)) + activity.startActivity(intent) + } + } + } + f.show(fm, TipDialog::class.java.simpleName) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/manager/ShareManager.kt b/app/src/main/java/com/cheng/bole/manager/ShareManager.kt new file mode 100644 index 0000000..8713524 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/manager/ShareManager.kt @@ -0,0 +1,166 @@ +package com.cheng.bole.manager + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.text.TextUtils +import androidx.core.content.FileProvider +import androidx.fragment.app.FragmentActivity +import com.cheng.bole.R +import com.example.base.extensions.toast +import com.umeng.socialize.ShareAction +import com.umeng.socialize.UMShareListener +import com.umeng.socialize.bean.SHARE_MEDIA +import com.umeng.socialize.media.UMImage +import com.umeng.socialize.media.UMVideo +import com.umeng.socialize.media.UMWeb +import java.io.File + + +object ShareManager { + + /** + * 分享媒体文件 + */ + fun shareFile(context: Context, file: File) { + val uri = FileProvider.getUriForFile(context,"${context.packageName}.fileprovider", file) + val intent = Intent(Intent.ACTION_SEND) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.setType(if (file.extension == "mp4") "video/mp4" else if (file.extension == "mp3") "audio/x-mpeg" else "image/jpeg") //分享文件类型 + context.startActivity(Intent.createChooser(intent, "分享")) + } + + /** + * 分享 + */ + fun shareUrl( + activity: FragmentActivity, + shareMedia: SHARE_MEDIA, + shareBean: com.cheng.bole.bean.WxShareEntity, + clickFun: () -> Unit, + ) { + if (!TextUtils.isEmpty(shareBean.link)) { + val web = UMWeb(shareBean.link) + web.title = shareBean.title + web.setThumb(UMImage(activity, R.mipmap.ic_launcher_icon)) + web.description = shareBean.content + + ShareAction(activity).withMedia(web).setPlatform(shareMedia) + .setCallback(object : UMShareListener { + /** + * 分享成功的回调 + * @param platform 平台类型 + */ + override fun onResult(platform: SHARE_MEDIA) { + clickFun.invoke() + } + + /** + * 分享取消的回调 + * @param platform 平台类型 + */ + override fun onCancel(platform: SHARE_MEDIA) { + toast("取消分享") + } + + /** + * 分享失败的回调 + * @param platform 平台类型 + * @param t 错误原因 + */ + override fun onError(platform: SHARE_MEDIA, t: Throwable) { + toast("分享失败,请重试") + } + + /** + * 分享开始的回调 + * @param platform 平台类型 + */ + override fun onStart(platform: SHARE_MEDIA) {} + + }).share() + + } + } + + fun shareImageOnWeChat(activity: Activity, image: UMImage) { + image.title = "" + image.description = "" + image.setThumb(image) + ShareAction(activity).withMedia(image).withText("") + .setPlatform(SHARE_MEDIA.WEIXIN) + .setCallback(object : UMShareListener { + /** + * 分享成功的回调 + * @param platform 平台类型 + */ + override fun onResult(platform: SHARE_MEDIA) { + toast("分享成功") + } + + /** + * 分享取消的回调 + * @param platform 平台类型 + */ + override fun onCancel(platform: SHARE_MEDIA) { + toast("取消分享") + } + + /** + * 分享失败的回调 + * @param platform 平台类型 + * @param t 错误原因 + */ + override fun onError(platform: SHARE_MEDIA, t: Throwable) { + toast("分享失败,请重试") + } + + /** + * 分享开始的回调 + * @param platform 平台类型 + */ + override fun onStart(platform: SHARE_MEDIA) {} + + }).share() + } + + fun shareFileOnWeChat(activity: Activity, file: File) { + val video = UMVideo(file) + ShareAction(activity).withMedia(video) + .setPlatform(SHARE_MEDIA.WEIXIN) + .setCallback(object : UMShareListener { + /** + * 分享成功的回调 + * @param platform 平台类型 + */ + override fun onResult(platform: SHARE_MEDIA) { + toast("分享成功") + } + + /** + * 分享取消的回调 + * @param platform 平台类型 + */ + override fun onCancel(platform: SHARE_MEDIA) { + toast("取消分享") + } + + /** + * 分享失败的回调 + * @param platform 平台类型 + * @param t 错误原因 + */ + override fun onError(platform: SHARE_MEDIA, t: Throwable) { + toast("分享失败,请重试") + } + + /** + * 分享开始的回调 + * @param platform 平台类型 + */ + override fun onStart(platform: SHARE_MEDIA) {} + + }).share() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/manager/UserConfigManager.kt b/app/src/main/java/com/cheng/bole/manager/UserConfigManager.kt new file mode 100644 index 0000000..256fabd --- /dev/null +++ b/app/src/main/java/com/cheng/bole/manager/UserConfigManager.kt @@ -0,0 +1,273 @@ +package com.cheng.bole.manager + +import android.os.Build +import android.text.TextUtils +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.MMKVUtils +import com.example.base.utils.Utils +import com.github.gzuliyujiang.oaid.DeviceID +import com.github.gzuliyujiang.oaid.DeviceIdentifier +import com.github.gzuliyujiang.oaid.IGetter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.cheng.bole.net.ApiFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject + +object UserConfigManager { + val _userConfigLiveData = MutableLiveData() + + val userConfig: com.cheng.bole.bean.UserConfigEntity? get() = _userConfigLiveData.value + + val userInfoLiveData = MutableLiveData() + + val userInfo: com.cheng.bole.bean.UserEntity? get() = userInfoLiveData.value + + fun getUserConfig(clickFun: () -> Unit?) { + if (!TextUtils.isEmpty(MMKVUtils.getString("userConfig"))) { + val data = Gson().fromJson(MMKVUtils.getString("userConfig"), com.cheng.bole.bean.UserConfigEntity::class.java) + _userConfigLiveData.postValue(data) + + getOaId() + saveUserConfig(data) + clickFun.invoke() + return + } + getOaId { + userConfig { clickFun.invoke() } + } + } + + fun userConfig(clickFun: () -> Unit?) { + GlobalScope.launch { + try { + val oaid = MMKVUtils.getString("oaid") ?: "" + val response = ApiFactory.apiService.getUserConfig(oaid, Build.VERSION.SDK_INT, "", DeviceIdentifier.getAndroidID(Utils.getApp()), getGTCid()) + if (response.status) { + _userConfigLiveData.postValue(response.data) + MMKVUtils.put("userConfig", Gson().toJson(response.data)) + + saveUserConfig(response.data) + + withContext(Dispatchers.Main) { + clickFun.invoke() + } + } else { + withContext(Dispatchers.Main) { + toast(response.message, true) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun saveUserConfig(data: com.cheng.bole.bean.UserConfigEntity) { + try { + LoginManager.saveToken(data.token) + saveIsTemp(data.temp) + if (data.config != null){ + saveGuidePayEnable(data.config!!.guidePayEnable!!) + saveGuideEnable(data.config!!.guideEnable!!) + saveGuideHint(data.config!!.guideHint) + saveNoLoginPay(data.config!!.noLoginPayEnable!!) + savePayAgreementEnable(data.config!!.payAgreementEnable!!) + saveLoginType(data.config!!.loginType!!) + saveBanners(data.config!!.banners!!) + saveShareEntity(data.config!!.wxShareEntity) + saveAdSwitch(data.config!!.adSwitch) + saveServicePhoneList(data.config!!.servicePhoneList) + } + } catch (e : Exception) { + e.printStackTrace() + } + } + + fun saveLastUserinfo(userinfo: com.cheng.bole.bean.UserEntity?) { + MMKVUtils.put("last_userinfo", Gson().toJson(userinfo)) + } + + fun getLastUserinfo(): com.cheng.bole.bean.UserEntity? { + val str = MMKVUtils.getString("last_userinfo") + if (!TextUtils.isEmpty(str)) { + return Gson().fromJson(str, com.cheng.bole.bean.UserEntity::class.java) + } + return null + } + + private fun saveGuidePayEnable(temp: Boolean) { + MMKVUtils.put("guide_pay", temp)//引导页是否可以支付 + } + + fun getGuidePayEnable(): Boolean { + return MMKVUtils.getBoolean("guide_pay", true) + } + + private fun saveGuideEnable(temp: Boolean) { + MMKVUtils.put("guide", temp)//是否开启引导页 + } + + fun getGuideEnable(): Boolean { + return MMKVUtils.getBoolean("guide", true) + } + + private fun saveGuideHint(hint: String?) { + MMKVUtils.put("guide_hint", hint) + } + + fun getGuideHint(): String { + return MMKVUtils.getString("guide_hint") ?: "" + } + + fun saveIsTemp(temp: Boolean) { + MMKVUtils.put("x-role", temp)//true临时用户 ,false登录用户 + } + + fun getIsTemp(): Boolean { + return MMKVUtils.getBoolean("x-role", true) + } + + fun isFirstUseApp(): Boolean { + return MMKVUtils.getBoolean("isFirstUse", true) + } + + fun saveFirstUseApp(isFirst: Boolean) { + MMKVUtils.put("isFirstUse", isFirst) + } + + private fun saveNoLoginPay(noLoginPay: Boolean) { + MMKVUtils.put("nologin_pay", noLoginPay) + } + + fun getNoLoginPay(): Boolean { + return MMKVUtils.getBoolean("nologin_pay", true) + } + + private fun savePayAgreementEnable(payAgreement: Boolean) { + MMKVUtils.put("pay_agreement", payAgreement) + } + + fun isPayAgreementEnable(): Boolean { + return MMKVUtils.getBoolean("pay_agreement", true) + } + + private fun saveLoginType(list: List) { + MMKVUtils.put("login_type", Gson().toJson(list)) + } + + fun getLoginType(): List { + val s = MMKVUtils.getString("login_type") + if (!TextUtils.isEmpty(s)) { + return Gson().fromJson(s, object : TypeToken>() {}.type) + } + return emptyList() + } + + private fun saveBanners(list: List) { + MMKVUtils.put("home_banner", Gson().toJson(list)) + } + + fun getBanners(): List { + val s = MMKVUtils.getString("home_banner") + if (!TextUtils.isEmpty(s)) { + return Gson().fromJson(s, object : TypeToken>() {}.type) + } + return emptyList() + } + + private fun saveShareEntity(entity: com.cheng.bole.bean.WxShareEntity?) { + MMKVUtils.put("weixin_share", Gson().toJson(entity)) + } + + fun getShareEntity(): com.cheng.bole.bean.WxShareEntity? { + val s = MMKVUtils.getString("weixin_share") + if (!TextUtils.isEmpty(s)) { + return Gson().fromJson(s, com.cheng.bole.bean.WxShareEntity::class.java) + } + return null + } + + private fun saveAdSwitch(switch: Boolean) { + MMKVUtils.put("ad_switch", switch) + } + + fun isAdSwitch(): Boolean { + return MMKVUtils.getBoolean("ad_switch") + } + + private fun saveServicePhoneList(list: List) { + MMKVUtils.put("service_phone_list", Gson().toJson(list)) + } + + fun getServicePhoneList(): List { + val str = MMKVUtils.getString("service_phone_list") + if (!TextUtils.isEmpty(str)) { + return Gson().fromJson(str, object : TypeToken>() {}.type) + } + return emptyList() + } + + /** + * 保存个推cid + */ + fun saveGTCid(cid: String) { + MMKVUtils.put("gt_cid", cid) + } + + /** + * 获取个推cid + */ + fun getGTCid(): String { + return MMKVUtils.getString("gt_cid") ?: "" + } + + private fun getOaId(callback: ((String) -> Unit)? = null) { + val oaid = DeviceIdentifier.getOAID(Utils.getApp()) + if (TextUtils.isEmpty(oaid)) { + DeviceID.getOAID(Utils.getApp(), object : IGetter { + override fun onOAIDGetComplete(result: String) { + MMKVUtils.put("oaid", result) + callback?.invoke(result) + } + + override fun onOAIDGetError(error: Exception) { + callback?.invoke("") + } + }) + } else { + MMKVUtils.put("oaid", oaid) + callback?.invoke(oaid) + } + } + + /** + * 保存百度归因bd_vid + */ + fun saveBDVID() { + try { + val str = com.cheng.bole.utils.appwalle.ChannelReader.get(Utils.getApp()) + if (!TextUtils.isEmpty(str)) { + val jsonObj = JSONObject(str) + jsonObj.getString("bd_vid").let { + MMKVUtils.put("bd_vid", it) + } + } else { + MMKVUtils.put("bd_vid", "") + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * 获取百度归因bd_vid + */ + fun getBDVID(): String { + return MMKVUtils.getString("bd_vid") ?: "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/net/ApiFactory.kt b/app/src/main/java/com/cheng/bole/net/ApiFactory.kt new file mode 100644 index 0000000..4d2064e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/ApiFactory.kt @@ -0,0 +1,8 @@ +package com.cheng.bole.net + + +object ApiFactory { + val apiService: ApiService by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + HttpRetrofit().apiService + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/net/ApiService.kt b/app/src/main/java/com/cheng/bole/net/ApiService.kt new file mode 100644 index 0000000..150f204 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/ApiService.kt @@ -0,0 +1,158 @@ +package com.cheng.bole.net + +import com.cheng.bole.net.model.HttpBaseResult +import com.cheng.bole.net.model.HttpListResult +import com.ylqh.cube.bean.CouponActivityEntity +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Part +import retrofit2.http.Query +import retrofit2.http.QueryMap + +interface ApiService { + + /** + * 获取客户端配置 + */ + @GET("/api/user/config") + suspend fun getUserConfig( + @Query("oaid") oaid: String, + @Query("os_version") osVersion: Int, + @Query("ua") ua: String, + @Query("imei") imei: String, + @Query("cid") cid: String, + ): HttpBaseResult + + /** + * 发送验证码 + */ + @POST("/api/user/code") + suspend fun sendCode(@Body params: Map): HttpBaseResult + + /** + * 登录 + */ + @POST("/api/user/login") + suspend fun login(@Body requestBody: RequestBody): HttpBaseResult + + /** + * 退出登录 + */ + @POST("/api/user/logout") + suspend fun logout(): HttpBaseResult + + /** + *更新用户信息 + */ + @PUT("/api/user") + suspend fun changUser(@Body requestBody: RequestBody): HttpBaseResult + + /** + * 注销 + */ + @POST("/api/user/destroy") + suspend fun userDestroy(): HttpBaseResult + + /** + * 获取用户信息 + */ + @GET("/api/user") + suspend fun userInfo(): HttpBaseResult + + /** + * 获取账号列表 + */ + @GET("/api/user/account") + suspend fun accountList(@Query("scene") scene: String): HttpBaseResult> + + /** + * 上传图片 + */ + @Multipart + @POST("/api/user/upload") + suspend fun upload(@Part part: MultipartBody.Part, @Query("scene") scene: String): HttpBaseResult + + /** + *删除用户文件 + */ + @DELETE("/api/user/upload") + suspend fun delUserFile(@Query("id") id: String): HttpBaseResult + + /** + * 联系客服 + */ + @GET("/api/weixin/service") + suspend fun wxService(): HttpBaseResult + + /** + * 事件上报 + */ + @POST("/api/user/event") + suspend fun eventReport(@Body requestBody: RequestBody): HttpBaseResult + + /** + * 推送回执接口 + */ + @PUT("/api/push/receipt") + suspend fun reportPushReceipt(@Body requestBody: RequestBody): HttpBaseResult + + /** + * 意见反馈 + */ + @POST("/api/user/feedback") + suspend fun feedback(@Body requestBody: RequestBody): HttpBaseResult + + /** + * 用户公告 + */ + @GET("/api/user/notice") + suspend fun notice(): HttpBaseResult + + /** + * 获取VIP套餐列表 + */ + @GET("/api/order/goods") + suspend fun getGoodsList(@Query("type") type: String = "member"): HttpBaseResult> + + /** + * 优惠券列表 + */ + @GET("/api/activity/coupon") + suspend fun couponList(@QueryMap params: Map): HttpBaseResult> + + /** + * 获取优惠活动列表 + */ + @GET("/api/activity") + suspend fun couponActivityList(@Query("scene") scene: String): HttpBaseResult> + + /** + * 领取活动优惠券 + */ + @POST("/api/activity") + suspend fun getActivityCoupons(@Query("activity_id") ids: String): HttpBaseResult + + /** + * 创建支付订单 + */ + @POST("/api/order") + suspend fun payCreateOrder(@Body requestBody: RequestBody): HttpBaseResult + + /** + * 权限验证 + */ + @GET("/api/user/auth") + suspend fun checkPermission(@Query("scene") info: String?): HttpBaseResult + + /** + * 权限上报 + */ + @POST("/api/user/auth") + suspend fun sendCheckPermission(@Body requestBody: RequestBody): HttpBaseResult +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/net/ContentTypeInterceptor.kt b/app/src/main/java/com/cheng/bole/net/ContentTypeInterceptor.kt new file mode 100644 index 0000000..429ae31 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/ContentTypeInterceptor.kt @@ -0,0 +1,24 @@ +package com.cheng.bole.net + +import okhttp3.Interceptor +import okhttp3.Response +import java.util.Locale + +class ContentTypeInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + val url = String.format("%s", originalRequest.url) + val method = originalRequest.method.lowercase(Locale.getDefault()).trim { it <= ' ' } + + if (!url.contains("user/upload")) { + if(method == "post" || method == "put"){ + val newRequest = originalRequest.newBuilder() + .header("Content-Type", "application/json; charset=utf-8") + .method(originalRequest.method, originalRequest.body) + .build() + return chain.proceed(newRequest) + } + } + return chain.proceed(originalRequest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/net/HttpRetrofit.kt b/app/src/main/java/com/cheng/bole/net/HttpRetrofit.kt new file mode 100644 index 0000000..5ee2e90 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/HttpRetrofit.kt @@ -0,0 +1,48 @@ +package com.cheng.bole.net + +import com.cheng.bole.BuildConfig +import com.cheng.bole.common.Constants +import com.google.gson.GsonBuilder +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + + +/** + * 网络请求方法 结合了rxJava + */ +class HttpRetrofit internal constructor() { + companion object { + private const val DEFAULT_TIMEOUT = 20L + } + + val apiService: ApiService + + init { + //手动创建一个OkHttpClient并设置超时时间 + val httpClient = OkHttpClient.Builder() + httpClient.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) + //header拦截 + httpClient.addInterceptor(RequestHeaderInterceptor()) + httpClient.addInterceptor(ContentTypeInterceptor()) + //请求拦截 + httpClient.addInterceptor(RequestInterceptor()) + //响应拦截 + httpClient.addInterceptor(ResponseInterceptor()) + + + val gson = GsonBuilder().setLenient().registerTypeAdapterFactory(MyTypeAdapterFactory()).create() + val retrofit = Retrofit.Builder() + .client(httpClient.build()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .baseUrl(if (BuildConfig.DEBUG) Constants.TestUrl else Constants.BaseUrl) + .build() + + apiService = retrofit.create(ApiService::class.java) + } + + +} diff --git a/app/src/main/java/com/cheng/bole/net/MyTypeAdapterFactory.kt b/app/src/main/java/com/cheng/bole/net/MyTypeAdapterFactory.kt new file mode 100644 index 0000000..ef4b8ac --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/MyTypeAdapterFactory.kt @@ -0,0 +1,128 @@ +package com.cheng.bole.net + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import java.io.IOException + +/** + * @date 2020-06-05 13:53 + * @desc gson数据类型转换处理 + */ +class MyTypeAdapterFactory : TypeAdapterFactory { + + @Suppress("UNCHECKED_CAST") + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + //type.rawType 这个类型是实体的类型 不是服务器返回的数据类型 + return when (type.rawType) { + String::class.java -> StringNullAdapter() as TypeAdapter + Int::class.java -> IntNullAdapter() as TypeAdapter + Long::class.java -> LongNullAdapter() as TypeAdapter + Double::class.java -> DoubleNullAdapter() as TypeAdapter + else -> null + } + } + + //字符串处理 + class StringNullAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun read(reader: JsonReader): String { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return "" + } + return reader.nextString() + } + + @Throws(IOException::class) + override fun write(writer: JsonWriter, value: String?) { + if (value == null) { + writer.nullValue() + return + } + writer.value(value) + } + } + + //Int处理 + inner class IntNullAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun read(reader: JsonReader): Int { + val jt = reader.peek() + if (jt == JsonToken.NULL) { + reader.nextNull() + return 0 + } + if (jt == JsonToken.STRING) { + return reader.nextString().toIntOrNull() ?: 0 + } + return reader.nextInt() + } + + @Throws(IOException::class) + override fun write(writer: JsonWriter, value: Int?) { + if (value == null) { + writer.nullValue() + return + } + writer.value(value) + } + } + + //Long处理 + inner class LongNullAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun read(reader: JsonReader): Long { + val jt = reader.peek() + if (jt == JsonToken.NULL) { + reader.nextNull() + return 0 + } + if (jt == JsonToken.STRING) { + return reader.nextString().toLongOrNull() ?: 0L + } + return reader.nextLong() + } + + @Throws(IOException::class) + override fun write(writer: JsonWriter, value: Long?) { + if (value == null) { + writer.nullValue() + return + } + writer.value(value) + } + } + + //Double处理 + inner class DoubleNullAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun read(reader: JsonReader): Double { + val jt = reader.peek() + if (jt == JsonToken.NULL) { + reader.nextNull() + return 0.0 + } + if (jt == JsonToken.STRING) { + return reader.nextString().toDoubleOrNull() ?: 0.0 + } + return reader.nextDouble() + } + + @Throws(IOException::class) + override fun write(writer: JsonWriter, value: Double?) { + if (value == null) { + writer.nullValue() + return + } + writer.value(value) + } + } + + +} + diff --git a/app/src/main/java/com/cheng/bole/net/RequestHeaderInterceptor.kt b/app/src/main/java/com/cheng/bole/net/RequestHeaderInterceptor.kt new file mode 100644 index 0000000..07e790e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/RequestHeaderInterceptor.kt @@ -0,0 +1,53 @@ +package com.cheng.bole.net + +import com.cheng.bole.BuildConfig +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.utils.ChannelUtils +import com.example.base.utils.AppUtils +import com.example.base.utils.L +import com.example.base.utils.Utils +import com.github.gzuliyujiang.oaid.DeviceIdentifier +import com.cheng.bole.common.Constants +import com.cheng.bole.manager.UserConfigManager +import okhttp3.Interceptor +import okhttp3.Response + +class RequestHeaderInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + val requestBuilder = request.newBuilder() + + //添加header + request = requestBuilder + .addHeader("x-token", LoginManager.getToken()) + .addHeader("x-version", AppUtils.getAppVersionName()) + .addHeader("x-platform", "android") + .addHeader("x-device-id", DeviceIdentifier.getAndroidID(Utils.getApp())) + .addHeader("x-mobile-brand", android.os.Build.BRAND) + .addHeader("x-mobile-model", android.os.Build.MODEL) + .addHeader("x-base-version", AppUtils.getAppVersionName()) + .addHeader("x-channel", "jk_${ChannelUtils.getChannel()}") + .addHeader("x-click-id", UserConfigManager.getBDVID()) + .addHeader("x-app-id", Constants.APP_ID) + .addHeader("x-package", BuildConfig.APPLICATION_ID) + .build() + val stringBuilder = StringBuilder() + stringBuilder.append("-------------header start-------------\n") + stringBuilder.append("x-token = ${LoginManager.getToken()}\n") + stringBuilder.append("x-version = ${AppUtils.getAppVersionName()}\n") + stringBuilder.append("x-device-id = ${DeviceIdentifier.getAndroidID(Utils.getApp())}\n") + stringBuilder.append("x-mobile-brand = ${android.os.Build.BRAND}\n") + stringBuilder.append("x-mobile-model = ${android.os.Build.MODEL}\n") + stringBuilder.append("x-base-version = ${AppUtils.getAppVersionName()}\n") + stringBuilder.append("x-channel = jk_${ChannelUtils.getChannel()}\n") + stringBuilder.append("x-click-id = ${UserConfigManager.getBDVID()}\n") + stringBuilder.append("x-package = ${BuildConfig.APPLICATION_ID}\n") + stringBuilder.append("-------------header end-------------") + L.d(stringBuilder) + + return chain.proceed(request) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/net/RequestInterceptor.kt b/app/src/main/java/com/cheng/bole/net/RequestInterceptor.kt new file mode 100644 index 0000000..58c5416 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/RequestInterceptor.kt @@ -0,0 +1,77 @@ +package com.cheng.bole.net + +import android.text.TextUtils +import com.cheng.bole.common.Constants +import com.cheng.bole.utils.StringUtils +import com.example.base.utils.L +import okhttp3.Interceptor +import okhttp3.Response +import okio.Buffer +import java.nio.charset.StandardCharsets +import java.util.Arrays +import java.util.Locale + +/** + * @date 2020-06-05 13:53 + * @desc 网络请求可以在此处添加n多的请求拦截-加密数据 + */ + +class RequestInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + val method = request.method.lowercase(Locale.getDefault()).trim { it <= ' ' } + val url = request.url + val apiPath = String.format("%s", url) + L.d("TAG-->>url=$apiPath") + //如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(Constants.BaseUrl)) { + L.d("TAG-->>content-type=${request.headers["Content-Type"]}") + val requestBody = request.body + L.d("TAG-->>${requestBody.toString()}") + return chain.proceed(request) + } + var queryString = url.encodedQuery + queryString = if (!TextUtils.isEmpty(queryString)) { + (queryString + "&nonce=" + StringUtils.createUUID()) + "×tamp=" + System.currentTimeMillis() / 1000 + } else { + ("nonce=" + StringUtils.createUUID()) + "×tamp=" + System.currentTimeMillis() / 1000 + } + val sortQueryString = Arrays.stream(queryString.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .sorted { obj: String, anotherString: String? -> obj.compareTo(anotherString!!) } + .reduce { x: String, y: String -> "$x&$y" } + .get() + //如果请求方式是get或者delete 则没有body + var paramsStr = "" + var bytes = ByteArray(0) + var signature: String? = "" + if (method == "put" || method == "post") { + val requestBody = request.body + L.d("TAG-->>${requestBody.toString()}") + val buffer = Buffer() + requestBody!!.writeTo(buffer) + bytes = StringUtils.addByte(bytes, buffer.readByteArray()) + L.e("签名后bodyByte的长度为" + bytes.size) + paramsStr = String(bytes, StandardCharsets.UTF_8) + // paramsStr = buffer.readUtf8(); + L.e("签名后body的长度为" + paramsStr.length) + } + signature = if (bytes.isNotEmpty()) { + L.e("当前的数组长度为----" + bytes.size) + StringUtils.getMD5Byte( + StringUtils.addByte( + StringUtils.addByte("$sortQueryString&".toByteArray(), bytes), + ("&" + StringUtils.getMD5String(Constants.Signature)).toByteArray() + ) + ) + } else { + StringUtils.getMD5String(sortQueryString + "&" + StringUtils.getMD5String(Constants.Signature)) + } + val newUrl = apiPath.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + "?" + queryString + "&signature=" + signature + L.e("签名后的路径---", newUrl) + request = request.newBuilder().url(newUrl).build() + + return chain.proceed(request) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/net/ResponseInterceptor.kt b/app/src/main/java/com/cheng/bole/net/ResponseInterceptor.kt new file mode 100644 index 0000000..0b18631 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/ResponseInterceptor.kt @@ -0,0 +1,109 @@ +package com.cheng.bole.net + +import android.content.Intent +import android.text.TextUtils +import android.util.Log +import com.alibaba.fastjson.JSON +import com.example.base.common.RxBus +import com.example.base.utils.L +import com.example.base.utils.Utils +import com.cheng.bole.BuildConfig +import com.cheng.bole.common.Constants +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.ui.activity.LoginActivity +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.ui.fragment.mine.vip.VipFragment +import com.cheng.bole.utils.AESpkcs7paddingUtil +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Response +import okhttp3.ResponseBody +import java.nio.charset.Charset +import java.nio.charset.UnsupportedCharsetException + + +/** + * @date 2020-06-05 13:55 + * @desc 可以在此处添加n多的响应拦截- 比如解密数据 + */ +class ResponseInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url + val apiPath = String.format("%s", url) + //如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(if (BuildConfig.DEBUG) Constants.TestUrl else Constants.BaseUrl)) { + return chain.proceed(request) + } + val response = chain.proceed(request) + var charset = Charset.forName("UTF-8") + val contentType = response.header("Content-Type") + if (contentType != null && contentType.contains("application/json")) { + val responseBody = response.body + if (responseBody != null) { + val source = responseBody.source() + source.request(Long.MAX_VALUE) + val buffer = source.buffer + val mediaType = responseBody.contentType() + if (mediaType != null) { + try { + charset = mediaType.charset(Charset.forName("UTF-8")) + } catch (e: UnsupportedCharsetException) { + e.printStackTrace() + } + } + val respBody = buffer.clone().readString(charset!!) + L.d("response=${respBody}") + if (TextUtils.isEmpty(respBody)) { + return response + } + val code = JSON.parseObject(respBody).getInteger("code") + if (code == 1001003 || code == 1001004 || code == 1001005) { + LoginManager.logout() + val intent = Intent(Utils.getApp(), LoginActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + Utils.getApp().startActivity(intent) + } + + val isEncrypt = JSON.parseObject(respBody).getBoolean("encrypt") + L.e("是否需要解密$isEncrypt") + if (isEncrypt == null || !isEncrypt) { //如果不需要加密 直接返回 + L.d("response=${response.body}") + return response + } + val decrybody = JSON.parseObject(respBody).getString("data") + L.e("Json解析后的字符串为$decrybody") + var decryString: String? + try { + decryString = AESpkcs7paddingUtil.decryptNormal(decrybody, Constants.Encrypt) + Log.e("ResponseInterceptor", "解密后返回的字符串为$decryString") + val decCode = JSON.parseObject(decryString).getInteger("code") + when (decCode) { + 11018 -> { + RxBus.defaultInstance.post(com.cheng.bole.event.HomeRefreshEvent()) + } + + 19000 -> { + PublicActivity.newStart(Utils.getApp(), VipFragment::class.java, Pair("origin", "interceptor")) + } + + 1001003, 1001004, 1001005 -> { + LoginManager.logout() + val intent = Intent(Utils.getApp(), LoginActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + Utils.getApp().startActivity(intent) + } + } + + //返回新创建的response + return response.newBuilder().body(ResponseBody.create("text/plain".toMediaType(), decryString)).build() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + return response + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/net/model/HttpBaseResult.kt b/app/src/main/java/com/cheng/bole/net/model/HttpBaseResult.kt new file mode 100644 index 0000000..37b4341 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/model/HttpBaseResult.kt @@ -0,0 +1,38 @@ +package com.cheng.bole.net.model + +/** + * @date 2021-08-05 09:45 + * @desc 数据基本结构 + */ +data class HttpBaseResult(val code: Int, val data: T) { + + val status: Boolean + get() { + return code == 0 + } + val message: String = "" +} + +/** + * @param + * 获取数据转成 Result + */ +fun HttpBaseResult.toResult(): Result { + return if (this.status) { + Result.success(this.data) + } else { + Result.failure(RuntimeException(this.message)) + } +} + +/** + * @param + * 获取数据转成 Result> + */ +fun HttpBaseResult>.toListResult(): Result> { + return if (this.status) { + Result.success(this.data.items) + } else { + Result.failure(RuntimeException(this.message)) + } +} diff --git a/app/src/main/java/com/cheng/bole/net/model/HttpListResult.kt b/app/src/main/java/com/cheng/bole/net/model/HttpListResult.kt new file mode 100644 index 0000000..f4eb248 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/net/model/HttpListResult.kt @@ -0,0 +1,39 @@ +package com.cheng.bole.net.model + + +/** + * 列表基本结构(服务返回) + */ + +open class HttpListResult { + + var total: String = "0" //总条数 + var total_amount:String = "0.00" //总金额 + + private var content = listOf() //列表数据 + private var records = listOf() //列表数据 + private var list = listOf() //列表数据 + val items = listOf() //列表数据 + get() { + if (content.isNotEmpty()) { + return content + } + if (records.isNotEmpty()) { + return records + } + if (list.isNotEmpty()) { + return list + } + return field // 防止服务器数据为null 蛋疼 + } + + + override fun toString(): String { + return "HttpListResult{" + + "total=" + total + + "total_amount="+total_amount+ + ", rows=" + items + + '}' + } +} + diff --git a/app/src/main/java/com/cheng/bole/ui/activity/DrawerActivity.kt b/app/src/main/java/com/cheng/bole/ui/activity/DrawerActivity.kt new file mode 100644 index 0000000..72123b9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/activity/DrawerActivity.kt @@ -0,0 +1,78 @@ +package com.cheng.bole.ui.activity + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentContainerView +import com.example.base.extensions.onClick +import com.example.base.ui.BaseActivity +import com.example.base.utils.ScreenUtils +import org.jetbrains.anko.find + + +/** + * 仿抽屉activity + */ +class DrawerActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val containerView = find(com.example.base.R.id.fcvContainer) + val lp = containerView.layoutParams as FrameLayout.LayoutParams + lp.width = (ScreenUtils.getWindowSize().x * 0.75).toInt() + lp.height = ViewGroup.LayoutParams.MATCH_PARENT + lp.gravity = Gravity.END + containerView.layoutParams = lp + + window.decorView.find(android.R.id.content).onClick { finish() } + } + + override fun onResume() { + window.setBackgroundDrawable(ColorDrawable(Color.parseColor("#80000000"))) + super.onResume() + } + + override fun onPause() { + window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + super.onPause() + } + + override fun getFragment(): Fragment { + val className = intent.getStringExtra(FRAGMENT_CLASS_NAME)!! + val clazz = Class.forName(className) + + val fragment = clazz.getDeclaredConstructor().newInstance() as Fragment + fragment.arguments = intent.extras + return fragment + } + + companion object { + const val FRAGMENT_CLASS_NAME = "fragment_class_name" + + fun start(context: Context?, clazz: Class, vararg args: Pair) { + val intent = Intent(context, DrawerActivity::class.java) + intent.putExtra(FRAGMENT_CLASS_NAME, clazz.name) + intent.putExtras(bundleOf(*args)) + + context?.startActivity(intent) + } + + fun newStart(context: Context?, clazz: Class, vararg args: Pair) { + val intent = Intent(context, DrawerActivity::class.java) + intent.putExtra(FRAGMENT_CLASS_NAME, clazz.name) + intent.putExtras(bundleOf(*args)) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context?.startActivity(intent) + } + } + +} diff --git a/app/src/main/java/com/cheng/bole/ui/activity/GuideActivity.kt b/app/src/main/java/com/cheng/bole/ui/activity/GuideActivity.kt new file mode 100644 index 0000000..e3c7ef0 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/activity/GuideActivity.kt @@ -0,0 +1,45 @@ +package com.cheng.bole.ui.activity + +import android.annotation.SuppressLint +import android.os.Bundle +import android.window.OnBackInvokedDispatcher +import androidx.activity.addCallback +import androidx.core.os.BuildCompat +import com.cheng.bole.R +import com.cheng.bole.ui.fragment.guide.GuideFragment +import com.example.base.common.ActivityManager +import com.example.base.extensions.toast +import com.example.base.ui.BaseActivity + +class GuideActivity :BaseActivity(){ + + private var lastBackClickTime = 0L + override fun getFragment() = GuideFragment() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + backApp() + } + + @SuppressLint("UnsafeOptInUsageError") + private fun backApp() { + if (BuildCompat.isAtLeastT()) { + onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) { + exitApp() + } + } else { + onBackPressedDispatcher.addCallback(this) { + exitApp() + } + } + } + + private fun exitApp() { + if (System.currentTimeMillis() - lastBackClickTime > 1500) { + toast(getString(R.string.exit_app)) + lastBackClickTime = System.currentTimeMillis() + } else { + ActivityManager.exitApp() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/activity/LauncherActivity.kt b/app/src/main/java/com/cheng/bole/ui/activity/LauncherActivity.kt new file mode 100644 index 0000000..c68224a --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/activity/LauncherActivity.kt @@ -0,0 +1,184 @@ +package com.cheng.bole.ui.activity + +import android.annotation.SuppressLint +import android.os.Bundle +import android.text.TextUtils +import android.window.OnBackInvokedDispatcher +import androidx.activity.addCallback +import androidx.core.os.BuildCompat +import androidx.fragment.app.Fragment +import com.bytedance.ads.convert.BDConvert +import com.cheng.bole.BuildConfig +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.common.EventConstants +import com.cheng.bole.manager.DialogEnum +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.dialog.PrivacyPolicyDialog +import com.cheng.bole.utils.ChannelUtils +import com.example.base.dialog.LoadingDialog +import com.example.base.ui.BaseActivity +import com.example.base.utils.L +import com.example.base.utils.MMKVUtils +import com.example.base.utils.Utils +import com.g.gysdk.GYManager +import com.g.gysdk.GYResponse +import com.g.gysdk.GyCallBack +import com.g.gysdk.GyConfig +import com.gyf.immersionbar.BarHide +import com.gyf.immersionbar.ImmersionBar +import com.umeng.analytics.MobclickAgent +import com.umeng.commonsdk.UMConfigure +import org.jetbrains.anko.startActivity + + +/** + * 闪屏页 + */ +class LauncherActivity : BaseActivity() { + + private val privacyPolicyDialog by lazy { + PrivacyPolicyDialog() + } + + private val loadingDialog by lazy { + LoadingDialog(this) + } + + override fun getFragment() = Fragment(R.layout.activity_launcher) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ImmersionBar.with(this).hideBar(BarHide.FLAG_HIDE_NAVIGATION_BAR).init() + backApp() + + if (MMKVUtils.getBoolean("isAgree")) { + loadingDialog.show() + UserConfigManager.saveBDVID() + UserConfigManager.getUserConfig { + EventReportManager.eventReport(EventConstants.APP_LAUNCH, "", "") + loadingDialog.dismiss() + initUM() + initBD() + initGT() + intentMain() + } + } else { + privacyDialog() + } + } + + private fun privacyDialog() { + privacyPolicyDialog.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + loadingDialog.show() + MMKVUtils.put("isAgree", true) + UserConfigManager.saveBDVID() + UserConfigManager.getUserConfig { + EventReportManager.eventReport(EventConstants.APP_LAUNCH, "", "") + loadingDialog.dismiss() + initUM() + initBD() + initGT() + intentMain() + } + } else { + finish() + } + } + privacyPolicyDialog.show(supportFragmentManager, null) + } + + private fun intentMain() { + if (UserConfigManager.isFirstUseApp()) { + UserConfigManager.saveFirstUseApp(false) + if (UserConfigManager.getGuideEnable()) { + startActivity() + } else { + if (!LoginManager.isLogin()) { + if (TextUtils.isEmpty(LoginManager.getLastLoginType())) { + startActivity() + } else { + LoginActivity.start(this@LauncherActivity) + } + } else { + startActivity() + } + } + finish() + } else { + if (!LoginManager.isLogin()) { + if (TextUtils.isEmpty(LoginManager.getLastLoginType())) { + startActivity() + } else { + LoginActivity.start(this@LauncherActivity) + } + } else { + startActivity() + } + finish() + } + } + + /** + * 初始化友盟 + */ + private fun initUM() { + UMConfigure.preInit(Utils.getApp(), Constants.UmengAppkey, ChannelUtils.getChannel()) + UMConfigure.init(this, Constants.UmengAppkey, ChannelUtils.getChannel(), UMConfigure.DEVICE_TYPE_PHONE, "") + MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO) + } + + /** + * 巨量融合初始化 + */ + private fun initBD() { + BDConvert.init(Utils.getApp(), this) + } + + /** + * 个推初始化 + */ + private fun initGT() { + GYManager.getInstance().preInit(this) //个验SDK初始化 + GYManager.getInstance().init( + GyConfig.with(this) + .preLoginUseCache(true)//预取号使用缓存,可以提高预取号的成功率,建议设置为true + .channel(ChannelUtils.getChannel())//应用渠道 + .eLoginDebug(BuildConfig.DEBUG)//运营商debug调试模式 + .debug(BuildConfig.DEBUG)//个验debug调试模式 + .callBack(object : GyCallBack { + override fun onSuccess(gyResponse: GYResponse) { + L.d("TAG-->>GYManager init onSuccess =${gyResponse.code}") + } + + override fun onFailed(gyResponse: GYResponse) { + L.d("TAG-->>GYManager init onFailed =${gyResponse.code}") + } + }).build() + ) + //预登录 + GYManager.getInstance().ePreLogin(8000, object : GyCallBack { + override fun onSuccess(gyResponse: GYResponse) { + L.d("TAG-->>GYManager ePreLogin onSuccess =${gyResponse.code}") + } + + override fun onFailed(gyResponse: GYResponse) { + L.d("TAG-->>GYManager ePreLogin onFailed =${gyResponse.code}") + } + }) + } + + @SuppressLint("UnsafeOptInUsageError") + private fun backApp() { + if (BuildCompat.isAtLeastT()) { + onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) { + } + } else { + onBackPressedDispatcher.addCallback(this) { + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/activity/LoginActivity.kt b/app/src/main/java/com/cheng/bole/ui/activity/LoginActivity.kt new file mode 100644 index 0000000..c1eb153 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/activity/LoginActivity.kt @@ -0,0 +1,72 @@ +package com.cheng.bole.ui.activity + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.window.OnBackInvokedDispatcher +import androidx.activity.addCallback +import androidx.core.os.BuildCompat +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import com.cheng.bole.R +import com.cheng.bole.common.EventConstants +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.ui.fragment.login.LoginFragment +import com.cheng.bole.ui.fragment.login.onekey.OneKeyLoginFragment +import com.example.base.common.ActivityManager +import com.example.base.extensions.toast +import com.example.base.ui.BaseActivity +import com.g.gysdk.GYManager + +class LoginActivity : BaseActivity() { + + private var lastBackClickTime = 0L + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val from = intent.getIntExtra("from", 0) + if (from == 0) backApp() + } + + override fun getFragment(): Fragment { + val fragment = if (GYManager.getInstance().isPreLoginResultValid) { + OneKeyLoginFragment() + } else { + LoginFragment() + } + fragment.arguments = intent.extras + return fragment + } + + companion object { + fun start(activity: Activity?, vararg args: Pair) { + val intent = Intent(activity, LoginActivity::class.java) + intent.putExtras(bundleOf(*args)) + activity?.startActivity(intent) + } + } + + @SuppressLint("UnsafeOptInUsageError") + private fun backApp() { + if (BuildCompat.isAtLeastT()) { + onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) { + exitApp() + } + } else { + onBackPressedDispatcher.addCallback(this) { + exitApp() + } + } + } + + private fun exitApp() { + if (System.currentTimeMillis() - lastBackClickTime > 1500) { + toast(getString(R.string.exit_app)) + lastBackClickTime = System.currentTimeMillis() + } else { + EventReportManager.eventReport(EventConstants.EXIT_APP, "login", "") + ActivityManager.exitApp() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/activity/MainActivity.kt b/app/src/main/java/com/cheng/bole/ui/activity/MainActivity.kt new file mode 100644 index 0000000..80d760b --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/activity/MainActivity.kt @@ -0,0 +1,78 @@ +package com.cheng.bole.ui.activity + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.window.OnBackInvokedDispatcher +import androidx.activity.addCallback +import androidx.core.os.BuildCompat +import com.cheng.bole.R +import com.cheng.bole.common.EventConstants +import com.cheng.bole.event.LogoutSuccessEvent +import com.cheng.bole.event.PushMessageEvent +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.ui.fragment.main.MainFragment +import com.example.base.common.ActivityManager +import com.example.base.common.RxBus +import com.example.base.extensions.toast +import com.example.base.ui.BaseActivity +import com.example.base.utils.ClipboardUtils + +class MainActivity : BaseActivity() { + + private var lastBackClickTime = 0L + + override fun getFragment() = MainFragment() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + backApp() + + RxBus.defaultInstance.toObservable(LogoutSuccessEvent::class.java).subscribe { finish() } + + handleIntent(intent) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent?) { + val payload = intent?.getStringExtra("payload") + if (!TextUtils.isEmpty(payload)) { + RxBus.defaultInstance.post(PushMessageEvent(payload!!, 3)) + } else { + if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") { + val text = intent.getStringExtra(Intent.EXTRA_TEXT) + ClipboardUtils.copyText(text) + toast("复制成功") + return + } + } + } + + @SuppressLint("UnsafeOptInUsageError") + private fun backApp() { + if (BuildCompat.isAtLeastT()) { + onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) { + exitApp() + } + } else { + onBackPressedDispatcher.addCallback(this) { + exitApp() + } + } + } + + private fun exitApp() { + if (System.currentTimeMillis() - lastBackClickTime > 1500) { + toast(getString(R.string.exit_app)) + lastBackClickTime = System.currentTimeMillis() + } else { + EventReportManager.eventReport(EventConstants.EXIT_APP, "main", "") + ActivityManager.exitApp() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/activity/PublicActivity.kt b/app/src/main/java/com/cheng/bole/ui/activity/PublicActivity.kt new file mode 100644 index 0000000..2836762 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/activity/PublicActivity.kt @@ -0,0 +1,47 @@ +package com.cheng.bole.ui.activity + +import android.content.Context +import android.content.Intent +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import com.example.base.ui.BaseActivity + + +/** + * 所有的普通的fragment页面启动都用此activity装载 + */ +class PublicActivity : BaseActivity() { + private var mFragment: Fragment? = null + + val attachedFragment: Fragment? + get() = mFragment + + override fun getFragment(): Fragment { + val className = intent.getStringExtra(FRAGMENT_CLASS_NAME)!! + val clazz = Class.forName(className) + + mFragment = clazz.getDeclaredConstructor().newInstance() as Fragment + mFragment!!.arguments = intent.extras + return mFragment!! + } + + companion object { + const val FRAGMENT_CLASS_NAME = "fragment_class_name" + + fun start(context: Context?, clazz: Class, vararg args: Pair) { + val intent = Intent(context, PublicActivity::class.java) + intent.putExtra(FRAGMENT_CLASS_NAME, clazz.name) + intent.putExtras(bundleOf(*args)) + + context?.startActivity(intent) + } + + fun newStart(context: Context?, clazz: Class, vararg args: Pair) { + val intent = Intent(context, PublicActivity::class.java) + intent.putExtra(FRAGMENT_CLASS_NAME, clazz.name) + intent.putExtras(bundleOf(*args)) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context?.startActivity(intent) + } + } +} diff --git a/app/src/main/java/com/cheng/bole/ui/activity/VideoPlayerActivity.kt b/app/src/main/java/com/cheng/bole/ui/activity/VideoPlayerActivity.kt new file mode 100644 index 0000000..7953460 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/activity/VideoPlayerActivity.kt @@ -0,0 +1,23 @@ +package com.cheng.bole.ui.activity + +import android.annotation.SuppressLint +import android.os.Bundle +import com.cheng.bole.ui.fragment.video.VideoPlayerFragment +import com.example.base.ui.BaseActivity + +class VideoPlayerActivity : BaseActivity() { + private var fragment: VideoPlayerFragment? = null + + override fun getFragment() = fragment!! + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + val title = intent.getStringExtra("title") ?: "" + val url = intent.getStringExtra("url") ?: "" + fragment = VideoPlayerFragment.newInstance(url, title) + fragment!!.setFullScreenChangedListener { +// requestedOrientation = it + } + super.onCreate(savedInstanceState) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/base/BaseDialog.kt b/app/src/main/java/com/cheng/bole/ui/base/BaseDialog.kt new file mode 100644 index 0000000..dc103f0 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/base/BaseDialog.kt @@ -0,0 +1,31 @@ +package com.cheng.bole.ui.base + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.KeyEvent +import com.cheng.bole.R + +open class BaseDialog @JvmOverloads constructor( + context: Context, + styleRes: Int = R.style.BaseStyleDialog, + private val isNoReturn: Boolean = false +) : Dialog(context, styleRes) { + + @SuppressLint("GestureBackNavigation") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (isNoReturn) { + setOnKeyListener { _, keyCode, _ -> + var isShield = false + if (keyCode == KeyEvent.KEYCODE_BACK) { + isShield = true + } + isShield + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/base/BasePageAdapter.kt b/app/src/main/java/com/cheng/bole/ui/base/BasePageAdapter.kt new file mode 100644 index 0000000..8216648 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/base/BasePageAdapter.kt @@ -0,0 +1,42 @@ +package com.cheng.bole.ui.base + +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter + +class BasePageAdapter( + private val fm: FragmentManager, + private val titles: List, + private val mList: MutableList, + behavior: Int = BEHAVIOR_SET_USER_VISIBLE_HINT +) : FragmentStatePagerAdapter(fm, behavior) { + + override fun getItem(position: Int): Fragment { + return mList[position] + } + + override fun getCount(): Int { + return mList.size + } + + override fun getPageTitle(position: Int): CharSequence? { + if (titles.isEmpty()) return null + return titles[position] + } + + override fun instantiateItem(container: View, position: Int): Any { + val fragment = super.instantiateItem(container, position) as Fragment + this.fm.beginTransaction().show(fragment).commit() + return fragment + } + + override fun destroyItem(container: View, position: Int, `object`: Any) { + val fragment = mList[position] + fm.beginTransaction().hide(fragment).commitAllowingStateLoss() + } + + override fun getItemPosition(`object`: Any): Int { + return POSITION_NONE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/AccountListDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/AccountListDialog.kt new file mode 100644 index 0000000..ba4b3f9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/AccountListDialog.kt @@ -0,0 +1,112 @@ +package com.cheng.bole.ui.dialog + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.decoration.SpacesItemDecoration +import com.example.base.extensions.onClick +import com.example.base.utils.DensityUtils +import com.example.base.utils.ScreenUtils +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.DialogAccountListBinding +import com.cheng.bole.manager.DialogEnum + +class AccountListDialog : DialogFragment() { + private val btnText by lazy { arguments?.getString("btnText") ?: "我知道了" } + private val mAdapter by lazy { AccountAdapter() } + + private var mOnBackListener: ((DialogEnum) -> Unit)? = null //回调事件 + + lateinit var binding: DialogAccountListBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.8).toInt() + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + dialog?.setCancelable(false) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_account_list, null) + + binding = DialogAccountListBinding.bind(view) + + binding.tvTitle.typeface = Constants.youSheBiaoTiHei + + binding.mRecyclerView.adapter = mAdapter + binding.mRecyclerView.addItemDecoration(SpacesItemDecoration(DensityUtils.dp2px(10f))) + + val listStr = arguments?.getString("list") + if (!TextUtils.isEmpty(listStr)) { + val list = Gson().fromJson>(listStr, object : TypeToken>(){}.type) + mAdapter.setList(list) + } + + binding.tvOk.text = btnText + + binding.tvOk.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_OK) + dismiss() + } + + binding.ivClose.onClick { dismiss() } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + fun setOnSelectListener(listener: ((DialogEnum) -> Unit)) { + mOnBackListener = listener + } + + companion object { + fun newInstance(list: List, btnText: String? = null): AccountListDialog { + val arg = Bundle() + arg.putString("list", Gson().toJson(list)) + arg.putString("btnText", btnText) + val fragment = AccountListDialog() + fragment.arguments = arg + return fragment + } + } + + class AccountAdapter: BaseQuickAdapter(R.layout.listitem_account_login_tip) { + override fun convert(holder: BaseViewHolder, item: com.cheng.bole.bean.AccountEntity) { + if (item.vip_type == "1") { + holder.setGone(R.id.tv_vip_tag, true) + } else if (item.vip_type == "2" || item.vip_type == "3") { + holder.setText(R.id.tv_vip_tag, if (item.vip_type == "2") "会员" else "终生会员") + holder.setGone(R.id.tv_vip_tag, false) + } + holder.setText(R.id.tv_username, item.name) + holder.setText(R.id.tv_create_time, "${item.create_time} 注册") + holder.setGone(R.id.iv_bind_wx, !item.bind.contains("weixin")) + holder.setGone(R.id.iv_bind_phone, !item.bind.contains("phone")) + if (!TextUtils.isEmpty(item.phone)) { + holder.setText(R.id.tv_phone, item.phone.replaceRange(3, 7, "****")) + } else { + holder.setText(R.id.tv_phone, "") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/BindPhoneDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/BindPhoneDialog.kt new file mode 100644 index 0000000..af9fac0 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/BindPhoneDialog.kt @@ -0,0 +1,158 @@ +package com.cheng.bole.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.DialogFragment +import com.example.base.common.RxCountDown +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.utils.RegexUtils +import com.example.base.utils.ScreenUtils +import com.cheng.bole.R +import com.cheng.bole.databinding.DialogBindPhoneBinding +import com.cheng.bole.impl.TextWatcherImpl +import com.cheng.bole.utils.KeyboardUtils +import io.reactivex.rxjava3.disposables.Disposable + +class BindPhoneDialog : DialogFragment(), KeyboardUtils.OnSoftInputChangedListener { + private var mOnBackListener: ((String, String, String) -> Unit)? = null //回调事件 + private var mOnSendCodeClickListener:((String) -> Unit)? = null + + private var disposable: Disposable? = null + private var timestamp = "" + + lateinit var binding: DialogBindPhoneBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = ScreenUtils.getWindowSize().x + windowParams?.gravity = Gravity.BOTTOM + windowParams?.windowAnimations = R.style.dialog_bottom + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setCancelable(false) + KeyboardUtils.registerSoftInputChangedListener(requireActivity(), this) + return super.onCreateView(inflater, container, savedInstanceState) + } + + private val textWatcher = object : TextWatcherImpl() { + @SuppressLint("SetTextI18n") + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.ivClearText.visibility = if (TextUtils.isEmpty(s)) View.GONE else View.VISIBLE + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_bind_phone, null) + + binding = DialogBindPhoneBinding.bind(view) + + binding.etPhone.addTextChangedListener(textWatcher) + + binding.ivClose.onClick { + dismiss() + } + binding.ivClearText.onClick { + binding.etPhone.setText("") + } + binding.tvSendCode.onClick { + val phone = binding.etPhone.text?.trim().toString() + if (TextUtils.isEmpty(phone)) { + toast("请输入手机号") + return@onClick + } + if (!RegexUtils.isMobileSimple(phone)) { + toast("请输入正确的手机号") + return@onClick + } + mOnSendCodeClickListener?.invoke(phone) + } + binding.btnNext.onClick { + val phone = binding.etPhone.text?.trim().toString() + val code = binding.etCode.text?.trim().toString() + if (TextUtils.isEmpty(phone)) { + toast("请输入手机号") + return@onClick + } + if (!RegexUtils.isMobileSimple(phone)) { + toast("请输入正确的手机号") + return@onClick + } + if (TextUtils.isEmpty(code)) { + toast("请输入验证码") + return@onClick + } + mOnBackListener?.invoke(phone, code, timestamp) + dismiss() + } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + @SuppressLint("SetTextI18n") + private fun startTimer() { + disposable?.dispose() + disposable = RxCountDown.countdown(60).subscribe { + if (it > 0) { + binding.tvSendCode.text = "${it}s" + binding.tvSendCode.isEnabled = false + binding.tvSendCode.alpha = 0.5f + } else { + binding.tvSendCode.text = "重新发送" + binding.tvSendCode.isEnabled = true + binding.tvSendCode.alpha = 1f + } + } + } + + fun setOnSendCodeClickListener(listener: (String) -> Unit) { + mOnSendCodeClickListener = listener + } + + fun setOnBackListener(listener: (String, String, String) -> Unit) { + mOnBackListener = listener + } + + fun setTimestamp(timestamp:String) { + this.timestamp = timestamp + startTimer() + } + + override fun onSoftInputChanged(height: Int) { + val lp = binding.layoutContent.layoutParams as ConstraintLayout.LayoutParams + lp.bottomMargin = height + binding.layoutContent.layoutParams = lp + } + + override fun onDestroyView() { + KeyboardUtils.unregisterSoftInputChangedListener(requireActivity().window) + binding.etPhone.removeTextChangedListener(textWatcher) + super.onDestroyView() + } + + companion object { + fun newInstance(): BindPhoneDialog { + val arg = Bundle() + val fragment = BindPhoneDialog() + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/EditTextDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/EditTextDialog.kt new file mode 100644 index 0000000..bebd0f4 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/EditTextDialog.kt @@ -0,0 +1,123 @@ +package com.cheng.bole.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.InputType +import android.text.TextUtils +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.DialogFragment +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.utils.ScreenUtils +import com.cheng.bole.R +import com.cheng.bole.databinding.DialogEditTextBinding +import com.cheng.bole.impl.TextWatcherImpl +import com.cheng.bole.utils.KeyboardUtils + +class EditTextDialog : DialogFragment(), KeyboardUtils.OnSoftInputChangedListener { + private val title by lazy { arguments?.getString("title") ?: "" } + private val content by lazy { arguments?.getString("content") ?: "" } + private val hint by lazy { arguments?.getString("hint") } + private val inputType by lazy { arguments?.getInt("input_type") ?: InputType.TYPE_CLASS_TEXT } + private var mOnBackListener: ((String) -> Unit)? = null //回调事件 + + lateinit var binding: DialogEditTextBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = ScreenUtils.getWindowSize().x + windowParams?.gravity = Gravity.BOTTOM + windowParams?.windowAnimations = R.style.dialog_bottom + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + KeyboardUtils.registerSoftInputChangedListener(requireActivity(), this) + return super.onCreateView(inflater, container, savedInstanceState) + } + + private val textWatcher = object : TextWatcherImpl() { + @SuppressLint("SetTextI18n") + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.ivClearText.visibility = if (TextUtils.isEmpty(s)) View.GONE else View.VISIBLE + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_edit_text, null) + + binding = DialogEditTextBinding.bind(view) + + binding.etContent.addTextChangedListener(textWatcher) + + binding.tvTitle.text = title + binding.etContent.hint = hint ?: "请输入" + binding.etContent.setText(content) + binding.etContent.inputType = inputType + + binding.ivClose.onClick { + dismiss() + } + binding.ivClearText.onClick { + binding.etContent.setText("") + } + binding.btnNext.onClick { + val content = binding.etContent.text?.trim().toString() + if (TextUtils.isEmpty(content)) { + toast(hint ?: "请输入内容") + return@onClick + } + mOnBackListener?.invoke(content) + dismiss() + } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + fun setOnTextListener(listener: ((String) -> Unit)) { + mOnBackListener = listener + } + + override fun onSoftInputChanged(height: Int) { + val lp = binding.layoutContent.layoutParams as ConstraintLayout.LayoutParams + lp.bottomMargin = height + binding.layoutContent.layoutParams = lp + } + + override fun onDestroyView() { + KeyboardUtils.unregisterSoftInputChangedListener(requireActivity().window) + binding.etContent.removeTextChangedListener(textWatcher) + super.onDestroyView() + } + + companion object { + fun newInstance( + title: String? = null, + content: String? = null, + hint: String? = null, + inputType: Int = InputType.TYPE_CLASS_TEXT + ): EditTextDialog { + val arg = Bundle() + val fragment = EditTextDialog() + arg.putString("title", title) + arg.putString("content", content) + arg.putString("hint", hint) + arg.putInt("input_type", inputType) + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/LoginTipDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/LoginTipDialog.kt new file mode 100644 index 0000000..ab88e00 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/LoginTipDialog.kt @@ -0,0 +1,115 @@ +package com.cheng.bole.ui.dialog + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.example.base.browser.BrowserActivity +import com.example.base.extensions.getColor +import com.example.base.extensions.onClick +import com.example.base.utils.ScreenUtils +import com.example.base.utils.SpanUtils +import com.g.gysdk.GYManager +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.DialogLoginTipBinding +import com.cheng.bole.utils.UrlHelper + +class LoginTipDialog :DialogFragment(){ + private val type by lazy { arguments?.getInt("type") ?: 0 } + + private var mOnBackListener: (() -> Unit)? = null //回调事件 + + lateinit var binding: DialogLoginTipBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.8).toInt() + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_login_tip, null) + + binding = DialogLoginTipBinding.bind(view) + + binding.tvTitle.typeface = Constants.youSheBiaoTiHei + + if (type == 0){ + initLoginPrivacyTv() + } else { + initOneKeyLoginPrivacyTv() + } + + binding.ivClose.onClick { dismiss() } + binding.tvOk.onClick { + mOnBackListener?.invoke() + dismiss() + } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + fun setOnSelectListener(listener: (() -> Unit)) { + mOnBackListener = listener + } + + private fun initLoginPrivacyTv() { + SpanUtils.with(binding.tvTip) + .append("登录之前须先查看并同意") + .append("《用户协议》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startUserAgreement(requireContext()) + } + .append("和") + .append("《隐私政策》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startPrivacyPolicy(requireContext()) + } + .create() + } + private fun initOneKeyLoginPrivacyTv() { + val preLoginResult = GYManager.getInstance().preLoginResult + SpanUtils.with(binding.tvTip) + .append("登录之前须先查看并同意") + .append("《${preLoginResult.privacyName}》") + .setClickSpan(getColor(R.color.color_466afd), false) { + BrowserActivity.start(requireContext(), preLoginResult.privacyName, preLoginResult.privacyUrl, false) + } + .append("、") + .append("《用户协议》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startUserAgreement(requireContext()) + } + .append("、") + .append("《隐私政策》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startPrivacyPolicy(requireContext()) + } + .create() + } + + companion object { + fun newInstance(type: Int = 0): LoginTipDialog { + val arg = Bundle() + val fragment = LoginTipDialog() + arg.putInt("type", type) + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/PayTipDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/PayTipDialog.kt new file mode 100644 index 0000000..4b1a006 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/PayTipDialog.kt @@ -0,0 +1,94 @@ +package com.cheng.bole.ui.dialog + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.example.base.extensions.getColor +import com.example.base.extensions.onClick +import com.example.base.utils.ScreenUtils +import com.example.base.utils.SpanUtils +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.DialogPayTipBinding +import com.cheng.bole.manager.DialogEnum +import com.cheng.bole.utils.UrlHelper + +class PayTipDialog : DialogFragment() { + private val showRenew by lazy { arguments?.getBoolean("show_renew") ?: false } + + private var mOnBackListener: ((DialogEnum) -> Unit)? = null //回调事件 + + lateinit var binding: DialogPayTipBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.8).toInt() + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_pay_tip, null) + + binding = DialogPayTipBinding.bind(view) + + binding.tvTitle.typeface = Constants.youSheBiaoTiHei + + binding.tvOk.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_OK) + dismiss() + } + + binding.tvClose.onClick { dismiss() } + binding.ivClose.onClick { dismiss() } + + initAgreement() + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + private fun initAgreement() { + val spanUtils = SpanUtils.with(binding.tvContent) + .append("我已阅读并同意") + .append("《会员服务协议规则》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startUserAgreement(requireContext(), "会员服务协议规则") + } + if (showRenew) { + spanUtils.append("和") + spanUtils.append("《自动续费服务规则》") + spanUtils.setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startRenewAgreement(requireContext()) + } + } + spanUtils.create() + } + + fun setOnSelectListener(listener: ((DialogEnum) -> Unit)) { + mOnBackListener = listener + } + + companion object { + fun newInstance(showRenew: Boolean = false): PayTipDialog { + val arg = Bundle() + arg.putBoolean("show_renew", showRenew) + val fragment = PayTipDialog() + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/PermissionTipDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/PermissionTipDialog.kt new file mode 100644 index 0000000..9abc790 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/PermissionTipDialog.kt @@ -0,0 +1,65 @@ +package com.cheng.bole.ui.dialog + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.example.base.utils.ScreenUtils +import com.cheng.bole.R +import com.cheng.bole.databinding.DialogPermissionTipBinding + +class PermissionTipDialog : DialogFragment() { + + private var content: CharSequence? = null + + lateinit var binding: DialogPermissionTipBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.9).toInt() + windowParams?.gravity = Gravity.TOP + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_permission_tip, null) + + binding = DialogPermissionTipBinding.bind(view) + + content = arguments?.getCharSequence("content") + + if (!TextUtils.isEmpty(content)) { + binding.tvContent.text = content + } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + companion object { + fun newInstance( + content: CharSequence, + ): PermissionTipDialog { + val arg = Bundle() + val fragment = PermissionTipDialog() + arg.putCharSequence("content", content) + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/PopupDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/PopupDialog.kt new file mode 100644 index 0000000..de7f219 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/PopupDialog.kt @@ -0,0 +1,42 @@ +package com.cheng.bole.ui.dialog + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import com.example.base.extensions.onClick +import com.cheng.bole.R +import com.cheng.bole.databinding.PopAboutTipBinding +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.ui.fragment.guide.GuideFragment +import com.cheng.bole.ui.fragment.mine.about.AppConfigFragment + +object PopupDialog { + + fun showAboutTip(context: Context,v:View){ + val view = LayoutInflater.from(context).inflate(R.layout.pop_about_tip,null,false) + val binding = PopAboutTipBinding.bind(view) + + binding.tvTip1.onClick { + PublicActivity.start(context, AppConfigFragment::class.java) + } + + binding.tvTip2.onClick { + PublicActivity.start(context, GuideFragment::class.java) + } + + val popWindow = PopupWindow(view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true) + popWindow.isClippingEnabled = false + popWindow.isFocusable = true + popWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + val location = IntArray(2) + v.getLocationOnScreen(location) + val y: Int = location[1] + v.height + val height = context.resources.displayMetrics.heightPixels - y + popWindow.showAsDropDown(v, 0, 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/PrivacyPolicyDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/PrivacyPolicyDialog.kt new file mode 100644 index 0000000..2890286 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/PrivacyPolicyDialog.kt @@ -0,0 +1,94 @@ +package com.cheng.bole.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.example.base.extensions.getColor +import com.example.base.extensions.onClick +import com.example.base.utils.ScreenUtils +import com.example.base.utils.SpanUtils +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.DialogPrivacyPolicyBinding +import com.cheng.bole.manager.DialogEnum +import com.cheng.bole.utils.UrlHelper + +class PrivacyPolicyDialog : DialogFragment() { + + private var mOnBackListener: ((DialogEnum) -> Unit)? = null //回调事件 + + lateinit var binding: DialogPrivacyPolicyBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.8).toInt() + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + dialog?.setCancelable(false) + return super.onCreateView(inflater, container, savedInstanceState) + } + + @SuppressLint("SetTextI18n") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_privacy_policy, null) + + binding = DialogPrivacyPolicyBinding.bind(view) + binding.tvTitle.typeface = Constants.youSheBiaoTiHei + + binding.btnNext.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_OK) + dismiss() + } + + binding.btnDisagree.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_CANCEL) + dismiss() + } + + initAgreement() + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + fun setOnSelectListener(listener: ((DialogEnum) -> Unit)) { + mOnBackListener = listener + } + + private fun initAgreement() { + SpanUtils.with(binding.tvContent) + .append("请你务必审慎阅读、充分理解") + .append("《服务协议》") + .setBold() + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startUserAgreement(requireContext()) + } + .append("和") + .append("《隐私政策》") + .setBold() + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startPrivacyPolicy(requireContext()) + } + .append("各条款,包括但不限于:为了更好的向你提供服务,我们需要访问你的相册、位置信息等。你可阅读") + .append("《隐私政策》") + .setBold() + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startPrivacyPolicy(requireContext()) + } + .append("了解详细信息。如果你同意,请点击下面同意按钮开始接受我们的服务。") + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/SelectCouponDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/SelectCouponDialog.kt new file mode 100644 index 0000000..692d858 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/SelectCouponDialog.kt @@ -0,0 +1,190 @@ +package com.cheng.bole.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.extensions.getColor +import com.example.base.extensions.getYYYYMMDD2 +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.visible +import com.example.base.utils.ScreenUtils +import com.example.base.utils.SpanUtils +import com.example.base.widget.EmptyView +import com.example.base.widget.PageStatus +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.DialogSelectCouponBinding +import com.cheng.bole.ui.fragment.mine.vip.VipFragment +import java.text.DecimalFormat + +class SelectCouponDialog : DialogFragment() { + private val id by lazy { arguments?.getString("id") ?: "" } + private val price by lazy { arguments?.getString("price") ?: "" } + + private val mAdapter by lazy { CouponAdapter(price) } + private val mEmptyView by lazy { EmptyView(requireContext()) } + private var mOnBackListener: ((com.cheng.bole.bean.CouponEntity?) -> Unit)? = null + private var selectedCoupon: com.cheng.bole.bean.CouponEntity? = null + + lateinit var binding: DialogSelectCouponBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = ScreenUtils.getWindowSize().x + windowParams?.gravity = Gravity.BOTTOM + windowParams?.windowAnimations = R.style.dialog_bottom + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_select_coupon, null) + + binding = DialogSelectCouponBinding.bind(view) + + binding.mRecyclerView.adapter = mAdapter + mEmptyView.setBtnVisible(false) + mAdapter.setEmptyView(mEmptyView) + + mAdapter.setOnItemClickListener { _, _, i -> + val item = mAdapter.getItem(i) + if (TextUtils.isEmpty(item.threshold) || item.threshold.toFloat() / 100 <= price.toFloat()) { + mAdapter.data.forEach { + if (it.id == item.id) { + it.isChecked = !it.isChecked + } else { + it.isChecked = false + } + } + selectedCoupon = if (item.isChecked) item else null + mAdapter.notifyDataSetChanged() + } + } + + binding.ivClose.onClick { + dismiss() + } + binding.btnNext.onClick { + mOnBackListener?.invoke(selectedCoupon) + dismiss() + } + + initData() + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + @SuppressLint("NotifyDataSetChanged") + private fun initData() { + val fragment = requireParentFragment() + if (fragment is VipFragment) { + fragment.mViewModel.couponListLiveData.observe(this) { list -> + mAdapter.setList(list) + mAdapter.data.forEach { + if (it.id == id) { + it.isChecked = true + selectedCoupon = it + } + } + mAdapter.notifyDataSetChanged() + if (mAdapter.data.isEmpty()) { + mEmptyView.setStatus(PageStatus.NO_DATA) + binding.layoutBottom.gone() + } else { + mEmptyView.setStatus(PageStatus.GONG) + binding.layoutBottom.visible() + } + } + fragment.mViewModel.couponList() + } + } + + fun setOnBackListener(listener: (com.cheng.bole.bean.CouponEntity?) -> Unit) { + mOnBackListener = listener + } + + companion object { + fun newInstance(id: String?, price: String): SelectCouponDialog { + val args = Bundle() + args.putString("id", id) + args.putString("price", price) + val fragment = SelectCouponDialog() + fragment.arguments = args + return fragment + } + } + + internal class CouponAdapter(private val price: String) : BaseQuickAdapter(R.layout.listitem_coupon_dialog) { + + override fun convert(holder: BaseViewHolder, item: com.cheng.bole.bean.CouponEntity) { + holder.setText( + R.id.tv_name, + if (!TextUtils.isEmpty(item.coupon_name)) item.coupon_name else "${item.coupon_value_name}${item.coupon_type_name}" + ) + holder.setText(R.id.tv_expire_time, "有效期至 ${(item.expire_timestamp.toLong() * 1000).getYYYYMMDD2()}") + holder.setText(R.id.tv_type, item.coupon_type_name) + holder.getView(R.id.tv_discount_amount).typeface = Constants.dDIN_PRO_M + when (item.coupon_type) { + com.cheng.bole.bean.CouponEntity.TYPE_CASH -> { + SpanUtils.with(holder.getView(R.id.tv_discount_amount)) + .append("¥") + .setFontSize(12, true) + .append(DecimalFormat("0.##").format(item.coupon_value.toFloat() / 100)) + .create() + if (!TextUtils.isEmpty(item.threshold)) { + holder.setText(R.id.tv_threshold_amount, "满${DecimalFormat("0.##").format(item.threshold.toFloat() / 100)}可用") + } else { + holder.setText(R.id.tv_threshold_amount, "无门槛") + } + } + + com.cheng.bole.bean.CouponEntity.TYPE_DISCOUNT -> { + SpanUtils.with(holder.getView(R.id.tv_discount_amount)) + .append(DecimalFormat("0.##").format(item.coupon_value.toFloat() * 10)) + .append("折") + .setFontSize(12, true) + .create() + if (!TextUtils.isEmpty(item.threshold)) { + holder.setText(R.id.tv_threshold_amount, "满${DecimalFormat("0.##").format(item.threshold.toFloat() / 100)}可用") + } else { + holder.setText(R.id.tv_threshold_amount, "无门槛") + } + } + } + + if (TextUtils.isEmpty(item.threshold) || item.threshold.toFloat() / 100 <= price.toFloat()) { + holder.setTextColor(R.id.tv_discount_amount, getColor(R.color.color_ff5846)) + holder.setTextColor(R.id.tv_threshold_amount, getColor(R.color.color_ff5846)) + holder.setBackgroundResource(R.id.layout_amount, R.drawable.shape_fdf4f2_cor6) + holder.setImageResource(R.id.iv_check, if (item.isChecked) R.mipmap.ic_coupon_check_true else R.mipmap.ic_coupon_check_false) + } else { + holder.setTextColor(R.id.tv_discount_amount, getColor(R.color.color_d8d8d8)) + holder.setTextColor(R.id.tv_threshold_amount, getColor(R.color.color_d8d8d8)) + holder.setBackgroundResource(R.id.layout_amount, R.drawable.shape_f6f6f6_cor6) + holder.setImageResource(R.id.iv_check, R.mipmap.ic_coupon_check_disable) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/ShareDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/ShareDialog.kt new file mode 100644 index 0000000..ccf0032 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/ShareDialog.kt @@ -0,0 +1,85 @@ +package com.cheng.bole.ui.dialog + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.utils.ClipboardUtils +import com.example.base.utils.ScreenUtils +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import com.umeng.socialize.bean.SHARE_MEDIA +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.DialogShareBinding +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.ShareManager +import com.cheng.bole.manager.UserConfigManager + +class ShareDialog : DialogFragment() { + private lateinit var api: IWXAPI + + lateinit var binding: DialogShareBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = ScreenUtils.getWindowSize().x + windowParams?.gravity = Gravity.BOTTOM + windowParams?.windowAnimations = R.style.dialog_bottom + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + api = WXAPIFactory.createWXAPI(requireContext(), Constants.WechatAppId) + + val view = layoutInflater.inflate(R.layout.dialog_share, null) + binding = DialogShareBinding.bind(view) + + binding.ivWxShare.onClick { + if (!api.isWXAppInstalled) { + toast("您没有安装微信客户端,请先下载安装") + return@onClick + } + ShareManager.shareUrl(requireActivity(), SHARE_MEDIA.WEIXIN, UserConfigManager.getShareEntity()!!) {} + EventReportManager.eventReport(EventConstants.SHARE_APP, "", "weixin") + dismiss() + } + + binding.ivWxCircleShare.onClick { + if (!api.isWXAppInstalled) { + toast("您没有安装微信客户端,请先下载安装") + return@onClick + } + ShareManager.shareUrl(requireActivity(), SHARE_MEDIA.WEIXIN_CIRCLE, UserConfigManager.getShareEntity()!!) {} + EventReportManager.eventReport(EventConstants.SHARE_APP, "", "weixin_friend") + dismiss() + } + + binding.ivLinkShare.onClick { + ClipboardUtils.copyText(UserConfigManager.getShareEntity()!!.link) + EventReportManager.eventReport(EventConstants.SHARE_APP, "", "copy_url") + toast("复制成功") + dismiss() + } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/SimpleTipDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/SimpleTipDialog.kt new file mode 100644 index 0000000..7c38fbc --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/SimpleTipDialog.kt @@ -0,0 +1,99 @@ +package com.cheng.bole.ui.dialog + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.visible +import com.example.base.utils.ScreenUtils +import com.cheng.bole.R +import com.cheng.bole.databinding.DialogSimpleTipBinding +import com.cheng.bole.manager.DialogEnum + +class SimpleTipDialog : DialogFragment() { + + private var mOnBackListener: ((DialogEnum) -> Unit)? = null //回调事件 + + private var content: CharSequence? = null + private var leftText: String? = null + private var rightText: String? = null + + lateinit var binding: DialogSimpleTipBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.8).toInt() + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_simple_tip, null) + + binding = DialogSimpleTipBinding.bind(view) + + content = arguments?.getCharSequence("content") + leftText = arguments?.getString("leftText") + rightText = arguments?.getString("rightText") + + if (!TextUtils.isEmpty(content)) { + binding.tvContent.text = content + } + if (!TextUtils.isEmpty(leftText)) { + binding.tvCancel.text = leftText + binding.tvCancel.visible() + } else { + binding.tvCancel.gone() + } + if (!TextUtils.isEmpty(rightText)) { + binding.tvOk.text = rightText + } + + binding.tvCancel.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_CANCEL) + dismiss() + } + binding.tvOk.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_OK) + dismiss() + } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + fun setOnSelectListener(listener: ((DialogEnum) -> Unit)) { + mOnBackListener = listener + } + + companion object { + fun newInstance( + content: CharSequence? = null, + leftText: String? = null, + rightText: String? = null, + ): SimpleTipDialog { + val arg = Bundle() + val fragment = SimpleTipDialog() + arg.putCharSequence("content", content) + arg.putString("leftText", leftText) + arg.putString("rightText", rightText) + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/TipDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/TipDialog.kt new file mode 100644 index 0000000..3e0dd60 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/TipDialog.kt @@ -0,0 +1,110 @@ +package com.cheng.bole.ui.dialog + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.example.base.extensions.onClick +import com.example.base.utils.ScreenUtils +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.DialogTipBinding +import com.cheng.bole.manager.DialogEnum + +class TipDialog : DialogFragment() { + + private var mOnBackListener: ((DialogEnum) -> Unit)? = null //回调事件 + + private var title: String? = null + private var content: CharSequence? = null + private var leftText: String? = null + private var rightText: String? = null + private var cancelable: Boolean = true + + lateinit var binding: DialogTipBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.8).toInt() + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setCancelable(cancelable) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_tip, null) + + binding = DialogTipBinding.bind(view) + + binding.tvTitle.typeface = Constants.youSheBiaoTiHei + + title = arguments?.getString("title") + content = arguments?.getCharSequence("content") + leftText = arguments?.getString("leftText") + rightText = arguments?.getString("rightText") + cancelable = arguments?.getBoolean("cancelable") ?: true + + if (!TextUtils.isEmpty(title)) { + binding.tvTitle.text = title + } + + if (!TextUtils.isEmpty(content)) { + binding.tvContent.text = content + } + if (!TextUtils.isEmpty(leftText)) { + binding.tvCancel.text = leftText + } + if (!TextUtils.isEmpty(rightText)) { + binding.tvOk.text = rightText + } + + binding.tvCancel.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_CANCEL) + dismiss() + } + binding.tvOk.onClick { + mOnBackListener?.invoke(DialogEnum.CLICK_OK) + dismiss() + } + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + return dialog + } + + fun setOnSelectListener(listener: ((DialogEnum) -> Unit)) { + mOnBackListener = listener + } + + companion object { + fun newInstance( + title: String? = null, + content: CharSequence? = null, + leftText: String? = null, + rightText: String? = null, + cancelable: Boolean = true + ): TipDialog { + val arg = Bundle() + val fragment = TipDialog() + arg.putString("title", title) + arg.putCharSequence("content", content) + arg.putString("leftText", leftText) + arg.putString("rightText", rightText) + arg.putBoolean("cancelable", cancelable) + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/UpdateVersionDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/UpdateVersionDialog.kt new file mode 100644 index 0000000..2e26dfd --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/UpdateVersionDialog.kt @@ -0,0 +1,208 @@ +package com.cheng.bole.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.FileProvider +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import com.cheng.bole.R +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.DialogUpdateVerisonBinding +import com.cheng.bole.manager.DialogEnum +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.utils.DownLoadUtils +import com.cheng.bole.utils.PermissionUtils +import com.cheng.bole.utils.UIUtils +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.visible +import com.example.base.utils.ScreenUtils +import com.cheng.bole.common.Constants +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class UpdateVersionDialog : DialogFragment() { + private var mOnBackListener: ((DialogEnum) -> Unit)? = null //回调事件 + + var apkFilePath = "" + + lateinit var binding: DialogUpdateVerisonBinding + + override fun onStart() { + super.onStart() + val window = dialog?.window + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.7).toInt() + dialog?.window?.attributes = windowParams + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_update_verison, null) + + binding = DialogUpdateVerisonBinding.bind(view) + + binding.tvUpdateTitle.typeface = Constants.youSheBiaoTiHei + + val versionEntity = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arguments?.getSerializable("versionEntity", com.cheng.bole.bean.VersionEntity::class.java) + } else { + arguments?.getSerializable("versionEntity") as com.cheng.bole.bean.VersionEntity? + } + + binding.cancelBtn.onClick { + dismiss() + EventReportManager.eventReport(EventConstants.PKG_CANCEL, "", "升级的版本号${versionEntity?.version}") + } + + versionEntity?.apply { + binding.tvVersionName.text = version + binding.tvUpdateTitle.text = title + binding.tvUpdateDesc.text = description + if (force) { + binding.cancelBtn.gone() + } else { + binding.cancelBtn.visible() + } + } + + binding.ivClose.onClick { + val f = TipDialog.newInstance("提示", "确定取消下载?") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + DownLoadUtils.getInstance().cancel() + dismiss() + } + } + f.show(childFragmentManager, TipDialog::class.java.simpleName) + } + + binding.updateBtn.onClick { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (binding.updateBtn.text.toString() == "立即安裝" && !TextUtils.isEmpty(apkFilePath)) { + install() + } else { + versionEntity?.apply { + download(url, force) + EventReportManager.eventReport(EventConstants.PKG_UPDATE, "", "升级的版本号${versionEntity.version}") + } + } + } else { + PermissionUtils.checkStoragePermission(requireActivity(), childFragmentManager) { isGranted -> + if (isGranted) { + if (binding.updateBtn.text.toString() == "立即安裝" && !TextUtils.isEmpty(apkFilePath)) { + install() + } else { + versionEntity?.apply { + download(url, force) + EventReportManager.eventReport(EventConstants.PKG_UPDATE, "", "升级的版本号${versionEntity.version}") + } + } + } + } + } + } + + isCancelable = false + + val dialog = Dialog(requireContext()) + dialog.setContentView(view) + dialog.setCancelable(false) + dialog.setCanceledOnTouchOutside(false) + return dialog + } + + @SuppressLint("SetTextI18n") + private fun download(url: String, force: Boolean) { + + if (force) { + binding.ivClose.gone() + } else { + binding.ivClose.visible() + } + binding.cancelBtn.gone() + binding.updateBtn.gone() + binding.progressbar.visible() + binding.tvProgress.visible() + + lifecycleScope.launch(Dispatchers.IO) { + var totalProgress = 0L + DownLoadUtils.getInstance() + .setReadTImeOut(10L) + .setDeleteWhenException(false) + .initUrl(url, null) + .setFilePath(com.cheng.bole.utils.FileUtils.getInstance().cacheDownLoderDir.absolutePath) + .setFileName(UIUtils.getFileNameFromUrl(url)) + .setActionCallBack( + { totalProgress = it }, + { + val percent = it.toDouble() / totalProgress.toDouble() * 100 + val curProgress = percent.toInt() + binding.tvProgress.text = "${curProgress}%" + binding.progressbar.progress = curProgress + }, + { + binding.tvProgress.text = "100%" + apkFilePath = it.absolutePath + binding.updateBtn.text = "立即安裝" + + binding.updateBtn.visible() + binding.progressbar.gone() + binding.tvProgress.gone() + + install() + }, { + binding.updateBtn.text = "下载失败,点击重试" + binding.updateBtn.visible() + binding.progressbar.gone() + binding.tvProgress.gone() + }).down() + } + } + + fun setOnSelectListener(listener: ((DialogEnum) -> Unit)) { + mOnBackListener = listener + } + + companion object { + fun newInstance(versionEntity: com.cheng.bole.bean.VersionEntity): UpdateVersionDialog { + val arg = Bundle() + val fragment = UpdateVersionDialog() + arg.putSerializable("versionEntity", versionEntity) + fragment.arguments = arg + return fragment + } + } + + private fun install() { + try { + val apkFile = File(com.cheng.bole.utils.FileUtils.getInstance().cacheDownLoderDir, UIUtils.getFileNameFromUrl(apkFilePath)) + if (!apkFile.exists()) { + return + } + val apkUri = FileProvider.getUriForFile(requireContext(), requireContext().packageName + ".fileprovider", apkFile) + val intent = Intent(Intent.ACTION_VIEW) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.setDataAndType(apkUri, "application/vnd.android.package-archive") + startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/dialog/VipLoginTipDialog.kt b/app/src/main/java/com/cheng/bole/ui/dialog/VipLoginTipDialog.kt new file mode 100644 index 0000000..202ac79 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/dialog/VipLoginTipDialog.kt @@ -0,0 +1,43 @@ +package com.cheng.bole.ui.dialog + +import android.os.Bundle +import androidx.fragment.app.Fragment +import com.example.base.extensions.onClick +import com.example.base.utils.ScreenUtils +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.DialogVipLoginTipBinding +import com.cheng.bole.ui.base.BaseDialog + +class VipLoginTipDialog(mFragment: Fragment) : BaseDialog(mFragment.requireContext()){ + private var mOnBackListener: (() -> Unit)? = null //回调事件 + + lateinit var binding: DialogVipLoginTipBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val view = layoutInflater.inflate(R.layout.dialog_vip_login_tip, null) + setContentView(view) + + val windowParams = window?.attributes + windowParams?.dimAmount = 0.7f + windowParams?.width = (ScreenUtils.getWindowSize().x * 0.8).toInt() + window?.attributes = windowParams + + setCancelable(false) + + binding = DialogVipLoginTipBinding.bind(view) + + binding.tvTitle.typeface = Constants.youSheBiaoTiHei + + binding.btnOk.onClick { + mOnBackListener?.invoke() + dismiss() + } + + } + + fun setOnSelectListener(listener: (() -> Unit)) { + mOnBackListener = listener + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideFragment.kt new file mode 100644 index 0000000..b8b11b3 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideFragment.kt @@ -0,0 +1,103 @@ +package com.cheng.bole.ui.fragment.guide + +import android.annotation.SuppressLint +import androidx.fragment.app.Fragment +import androidx.viewpager.widget.ViewPager +import androidx.viewpager.widget.ViewPager.OnPageChangeListener +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.ui.BaseFragment +import com.cheng.bole.ui.activity.MainActivity +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.common.Constants +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentGuideContentBinding +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.base.BasePageAdapter +import com.cheng.bole.ui.fragment.mine.vip.VipFragment +import org.jetbrains.anko.startActivity + +class GuideFragment :BaseFragment(){ + + private var titles = listOf("guide1", "guide2", "guide3","guide4") + + private val list by lazy { + ArrayList() + } + + private val indicatorAdapter by lazy { IndicatorAdapter() } + + private var mIsScrolled = false + private var currentPosition = 0 + + override fun initView() { + super.initView() + titles.forEachIndexed { index, s -> + list.add(GuideItemFragment.newInstance(index)) + } + val pageAdapter = BasePageAdapter(childFragmentManager, titles, list) + binding.viewPager.adapter = pageAdapter + binding.viewPager.offscreenPageLimit = list.size + + indicatorAdapter.setList(MutableList(titles.size) { i -> i }) + binding.rvIndicator.adapter = indicatorAdapter + + binding.tvTitle.typeface = Constants.almmsht + } + + override fun initData() { + super.initData() + binding.tvDesc.text = UserConfigManager.getGuideHint() + EventReportManager.eventReport(EventConstants.GUIDE_LAUNCH, "", "") + } + + override fun initListener() { + super.initListener() + binding.btnStart.onClick { + binding.btnStart.postDelayed({ + binding.layoutGuide.gone() + }, 200) + } + + binding.viewPager.addOnPageChangeListener(object :OnPageChangeListener{ + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + + } + + @SuppressLint("NotifyDataSetChanged") + override fun onPageSelected(position: Int) { + currentPosition = position + indicatorAdapter.currentIndex = currentPosition + indicatorAdapter.notifyDataSetChanged() + EventReportManager.eventReport(EventConstants.GUIDE_OPPORTUNITY_SCROLL, "${currentPosition + 1}", "") + } + + override fun onPageScrollStateChanged(state: Int) { + when(state){ + ViewPager.SCROLL_STATE_DRAGGING->{ + mIsScrolled = false + return + } + ViewPager.SCROLL_STATE_SETTLING->{ + mIsScrolled = true + return + } + ViewPager.SCROLL_STATE_IDLE->{ + if (!mIsScrolled && currentPosition == indicatorAdapter.itemCount - 1){ + if (UserConfigManager.getGuidePayEnable()){ + PublicActivity.start(requireActivity(), VipFragment::class.java, Pair("origin", "guide")) + }else{ + requireActivity().startActivity() + } + } + mIsScrolled = true + return + } + } + } + + }) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideItemFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideItemFragment.kt new file mode 100644 index 0000000..f181eaa --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideItemFragment.kt @@ -0,0 +1,36 @@ +package com.cheng.bole.ui.fragment.guide + +import android.os.Bundle +import com.cheng.bole.R +import com.cheng.bole.databinding.FragmentGuideItemBinding +import com.example.base.ui.BaseFragment + +class GuideItemFragment : BaseFragment() { + private val curPos by lazy { arguments?.getInt("curPos") ?: 0 } + + companion object { + fun newInstance(pos: Int): GuideItemFragment { + val bundle = Bundle() + bundle.putInt("curPos", pos) + val f = GuideItemFragment() + f.arguments = bundle + return f + } + + } + + override fun initView() { + super.initView() + when (curPos) { + 0 -> binding.ivGuide.setBackgroundResource(R.mipmap.ic_guide_1) + 1 -> binding.ivGuide.setBackgroundResource(R.mipmap.ic_guide_2) + 2 -> binding.ivGuide.setBackgroundResource(R.mipmap.ic_guide_3) + 3 -> binding.ivGuide.setBackgroundResource(R.mipmap.ic_guide_4) + } + } + + override fun initListener() { + super.initListener() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideViewModel.kt new file mode 100644 index 0000000..951214d --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/guide/GuideViewModel.kt @@ -0,0 +1,6 @@ +package com.cheng.bole.ui.fragment.guide + +import com.example.base.viewmodel.BaseViewModel + +class GuideViewModel :BaseViewModel(){ +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/guide/IndicatorAdapter.kt b/app/src/main/java/com/cheng/bole/ui/fragment/guide/IndicatorAdapter.kt new file mode 100644 index 0000000..6b59805 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/guide/IndicatorAdapter.kt @@ -0,0 +1,13 @@ +package com.cheng.bole.ui.fragment.guide + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.cheng.bole.R + +class IndicatorAdapter: BaseQuickAdapter(R.layout.listitem_indicator) { + var currentIndex = 0 + + override fun convert(holder: BaseViewHolder, item: Int) { + holder.setImageResource(R.id.iv_indicator, if (item == currentIndex) R.drawable.shape_indicator_select else R.drawable.shape_indicator_default) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/home/HomeFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/home/HomeFragment.kt new file mode 100644 index 0000000..b78187f --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/home/HomeFragment.kt @@ -0,0 +1,12 @@ +package com.cheng.bole.ui.fragment.home + +import com.cheng.bole.databinding.FragmentHomeBinding +import com.example.base.ui.BaseFragment + + +/** + * 首页 + */ +class HomeFragment : BaseFragment() { + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/home/HomeViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/home/HomeViewModel.kt new file mode 100644 index 0000000..b596e88 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/home/HomeViewModel.kt @@ -0,0 +1,7 @@ +package com.cheng.bole.ui.fragment.home + +import com.example.base.viewmodel.BaseViewModel + +class HomeViewModel : BaseViewModel() { + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/login/LoginFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/login/LoginFragment.kt new file mode 100644 index 0000000..66035a3 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/login/LoginFragment.kt @@ -0,0 +1,267 @@ +package com.cheng.bole.ui.fragment.login + +import android.annotation.SuppressLint +import android.text.TextUtils +import com.bytedance.ads.convert.event.ConvertReportHelper +import com.example.base.common.RxBus +import com.example.base.extensions.getColor +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.extensions.visible +import com.example.base.ui.BaseFragment +import com.example.base.utils.SpanUtils +import com.example.base.utils.Utils +import com.google.gson.JsonObject +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentLoginBinding +import com.cheng.bole.event.LoginSuccessEvent +import com.cheng.bole.event.WxLoginEvent +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.activity.LoginActivity +import com.cheng.bole.ui.activity.MainActivity +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.ui.dialog.AccountListDialog +import com.cheng.bole.ui.dialog.LoginTipDialog +import com.cheng.bole.ui.fragment.login.onekey.OneKeyLoginFragment +import com.cheng.bole.utils.UrlHelper +import org.jetbrains.anko.sdk27.listeners.onCheckedChange +import org.jetbrains.anko.startActivity + +/** + * 登录 + */ +class LoginFragment : BaseFragment() { + private val from by lazy { arguments?.getInt("from") ?: 0 } + private lateinit var api: IWXAPI + + private var isAgree = false + var timestamp = "" + + private var isVisible = false + + private var isWxRequesting = false + + @SuppressLint("SetTextI18n") + override fun initView() { + super.initView() + setHomeAsUp(from != 0 || requireActivity() !is LoginActivity) + mTitleBar?.background = null + + binding.tvAppName.typeface = Constants.almmsht + + initLoginType() + initPrivacyTv() + } + + override fun initData() { + super.initData() + api = WXAPIFactory.createWXAPI(requireContext(), Constants.WechatAppId) + + if (from != 2 && requireActivity() is LoginActivity) mViewModel.accountList() + } + + override fun onStart() { + super.onStart() + isVisible = true + } + + override fun onStop() { + super.onStop() + isVisible = false + } + + override fun initListener() { + super.initListener() + binding.ivWxLogin.onClick { + if (!api.isWXAppInstalled) { + toast("您没有安装微信客户端,请先下载安装") + return@onClick + } + if (!isAgree) { + val loginTipDialog = LoginTipDialog.newInstance() + loginTipDialog.setOnSelectListener { + isAgree = true + binding.cbAgree.isChecked = true + wxAuth() + } + loginTipDialog.show(childFragmentManager, loginTipDialog::class.java.simpleName) + } else { + wxAuth() + } + } + binding.ivOnekeyLogin.onClick { + if (requireActivity() is LoginActivity) { + PublicActivity.start(requireContext(), OneKeyLoginFragment::class.java) + } else { + requireActivity().finish() + } + } + binding.tvSendCode.onClick { + sendCode() + } + binding.loginBtn.onClick { + loginByPhone() + } + binding.cbAgree.onCheckedChange { _, isChecked -> + isAgree = isChecked + } + binding.tvAgree.onClick { + binding.cbAgree.isChecked = !binding.cbAgree.isChecked + } + } + + @SuppressLint("SetTextI18n") + override fun initObserve() { + super.initObserve() + mViewModel.accountLiveData.observe(this) { + if (it.isNotEmpty()) { + AccountListDialog.newInstance(it).show(childFragmentManager, AccountListDialog::class.java.simpleName) + } + } + mViewModel.sendCodeLiveData.observe(this) { timestamp = it.timestamp } + mViewModel.countTimeLiveEvent.observe(this) { + if (it > 0) { + binding.tvSendCode.text = "重新获取(${it}s)" + binding.tvSendCode.isEnabled = false + binding.tvSendCode.alpha = 0.5f + } else { + binding.tvSendCode.text = "重新获取" + binding.tvSendCode.isEnabled = true + binding.tvSendCode.alpha = 1f + } + } + mViewModel.phoneLoginLiveData.observe(this) { + EventReportManager.eventReport(EventConstants.LOGIN, "phone", binding.edPhone.text.toString()) + ConvertReportHelper.onEventRegister("phone",true) + LoginManager.saveLastLoginType("phone") + loginSuccess(it) + } + mViewModel.wxLoginLiveData.observe(this) { + EventReportManager.eventReport(EventConstants.LOGIN, "weixin", "") + ConvertReportHelper.onEventRegister("weixin",true) + LoginManager.saveLastLoginType("weixin") + loginSuccess(it) + } + val wxLoginEvent = RxBus.defaultInstance.toObservable(WxLoginEvent::class.java).subscribe { + isWxRequesting = false + if (!TextUtils.isEmpty(it.code) && isVisible && it.state.endsWith("phone")) loginByWx(it.code) + } + addDisposable(wxLoginEvent) + val loginSuccessEvent = RxBus.defaultInstance.toObservable(LoginSuccessEvent::class.java).subscribe { + requireActivity().finish() + } + addDisposable(loginSuccessEvent) + } + + private fun initLoginType() { + val loginType = UserConfigManager.getLoginType() + if (loginType.contains("weixin")) { + binding.ivWxLogin.visible() + } else { + binding.ivWxLogin.gone() + } + } + + private fun initPrivacyTv() { + SpanUtils.with(binding.tvAgree) + .append("我已阅读并同意") + .append("《用户协议》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startUserAgreement(requireContext()) + } + .append("和") + .append("《隐私政策》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startPrivacyPolicy(requireContext()) + } + .create() + } + + private fun loginSuccess(entity: com.cheng.bole.bean.LoginEntity) { + LoginManager.saveToken(entity.token) + UserConfigManager.userConfig { + requireActivity().startActivity() + RxBus.defaultInstance.post(LoginSuccessEvent()) + } + } + + /** + * 手机号登录 + */ + private fun loginByPhone() { + val phone = binding.edPhone.text?.trim().toString() + val code = binding.edCode.text?.trim().toString() + if (TextUtils.isEmpty(phone)) { + toast("请输入手机号") + return + } + if (phone.length != 11) { + toast("请输入正确的手机号") + return + } + if (TextUtils.isEmpty(code)) { + toast("请输入验证码") + return + } + if (!isAgree) { + val loginTipDialog = LoginTipDialog.newInstance() + loginTipDialog.setOnSelectListener { + isAgree = true + binding.cbAgree.isChecked = true + mViewModel.loginByPhone(timestamp, phone, code) + } + loginTipDialog.show(childFragmentManager, "") + } else { + mViewModel.loginByPhone(timestamp, phone, code) + } + } + + private fun sendCode() { + val phone = binding.edPhone.text?.trim().toString() + if (TextUtils.isEmpty(phone)) { + toast("请输入手机号") + return + } + if (phone.length != 11) { + toast("请输入正确的手机号") + return + } + EventReportManager.eventReport(EventConstants.GET_CODE, "code_login", phone) + mViewModel.sendCode(phone) + } + + /** + * 微信登录 + */ + private fun loginByWx(wechatCode: String) { + val jsonWx = JsonObject() + jsonWx.addProperty("code", wechatCode) + jsonWx.addProperty("code_type", "") + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "weixin") + jsonObject.add("weixin", jsonWx) + mViewModel.loginByWx(wechatCode) + } + + private fun wxAuth() { + if (isWxRequesting) return + if (!api.isWXAppInstalled) { + toast("未安装微信客户端,请先下载安装微信客户端") + return + } + isWxRequesting = true + val req = SendAuth.Req() + req.scope = "snsapi_userinfo" // 只能填 snsapi_userinfo + req.state = Utils.getApp().packageName + Math.random() * 1000 + "_phone" + api.sendReq(req) + } +} diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/login/LoginViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/login/LoginViewModel.kt new file mode 100644 index 0000000..574dcd2 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/login/LoginViewModel.kt @@ -0,0 +1,114 @@ +package com.cheng.bole.ui.fragment.login + +import androidx.lifecycle.MutableLiveData +import com.example.base.common.RxCountDown +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.google.gson.JsonObject +import com.cheng.bole.net.ApiFactory +import io.reactivex.rxjava3.disposables.Disposable +import okhttp3.RequestBody.Companion.toRequestBody + +class LoginViewModel : BaseViewModel() { + private var disposable: Disposable? = null + + val accountLiveData = MutableLiveData>() + val countTimeLiveEvent = MutableLiveData() + var sendCodeLiveData = MutableLiveData() + var phoneLoginLiveData = MutableLiveData() + var wxLoginLiveData = MutableLiveData() + + fun accountList() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.accountList("login") + if (response.status) { + accountLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun sendCode(phone: String) { + showDialog() + launchOnUiTryCatch({ + val map = HashMap() + map["phone"] = phone + val response = ApiFactory.apiService.sendCode(map) + if (response.status) { + sendCodeLiveData.postValue(response.data) + startTimer() + } else { + toast(response.message, true) + } + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun loginByPhone(timestamp: String, phone:String, code:String) { + showDialog() + launchOnUiTryCatch({ + val jsonPhone = JsonObject() + jsonPhone.addProperty("timestamp", timestamp) + jsonPhone.addProperty("phone", phone) + jsonPhone.addProperty("code", code) + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "phone") + jsonObject.add("phone", jsonPhone) + val response = ApiFactory.apiService.login(jsonObject.toString().toRequestBody()) + if (response.status) { + phoneLoginLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun loginByWx(code: String) { + showDialog() + launchOnUiTryCatch({ + val jsonWx = JsonObject() + jsonWx.addProperty("code", code) + jsonWx.addProperty("code_type", "") + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "weixin") + jsonObject.add("weixin", jsonWx) + val response = ApiFactory.apiService.login(jsonObject.toString().toRequestBody()) + if (response.status) { + wxLoginLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + private fun startTimer() { + disposable?.dispose() + disposable = RxCountDown.countdown(60).subscribe { + countTimeLiveEvent.value = it + } + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/login/onekey/OneKeyLoginFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/login/onekey/OneKeyLoginFragment.kt new file mode 100644 index 0000000..3374809 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/login/onekey/OneKeyLoginFragment.kt @@ -0,0 +1,284 @@ +package com.cheng.bole.ui.fragment.login.onekey + +import android.annotation.SuppressLint +import android.text.TextUtils +import android.view.View +import com.alibaba.fastjson.JSONObject +import com.bytedance.ads.convert.event.ConvertReportHelper +import com.example.base.browser.BrowserActivity +import com.example.base.common.RxBus +import com.example.base.extensions.getColor +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.extensions.visible +import com.example.base.ui.BaseFragment +import com.example.base.utils.L +import com.example.base.utils.SpanUtils +import com.example.base.utils.Utils +import com.g.gysdk.EloginActivityParam +import com.g.gysdk.GYManager +import com.g.gysdk.GYResponse +import com.g.gysdk.GyCallBack +import com.google.gson.JsonObject +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentOnekeyLoginBinding +import com.cheng.bole.event.LoginSuccessEvent +import com.cheng.bole.event.WxLoginEvent +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.activity.LoginActivity +import com.cheng.bole.ui.activity.MainActivity +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.ui.dialog.AccountListDialog +import com.cheng.bole.ui.dialog.LoginTipDialog +import com.cheng.bole.ui.fragment.login.LoginFragment +import com.cheng.bole.utils.UrlHelper +import okhttp3.RequestBody.Companion.toRequestBody +import org.jetbrains.anko.startActivity + +/** + * 一健登录 + */ +class OneKeyLoginFragment : BaseFragment() { + private val from by lazy { arguments?.getInt("from") ?: 0 } + private lateinit var api: IWXAPI + + private var gyuid = "" + + private var isVisible = false + + private var isWxRequesting = false + + @SuppressLint("SetTextI18n") + override fun initView() { + super.initView() + setHomeAsUp(from != 0 || requireActivity() !is LoginActivity) + binding.mTitleBar.background = null + + initLoginType() + initPrivacyTv() + } + + override fun initData() { + super.initData() + api = WXAPIFactory.createWXAPI(requireContext(), Constants.WechatAppId) + + if (from != 2 && requireActivity() is LoginActivity) mViewModel.accountList() + if (GYManager.getInstance().isPreLoginResultValid) { + initOneKeyLogin()//预登录有效,启动登录授权页 + } else { + //预登录 + GYManager.getInstance().ePreLogin(5000, object : GyCallBack { + override fun onSuccess(gyResponse: GYResponse) { + initOneKeyLogin() + L.d("TAG-->>GYManager ePreLogin onSuccess =${gyResponse.code}") + } + + override fun onFailed(gyResponse: GYResponse) { + L.d("TAG-->>GYManager ePreLogin onFailed =${gyResponse.code}") + } + }) + } + } + + override fun onStart() { + super.onStart() + isVisible = true + } + + override fun onStop() { + super.onStop() + isVisible = false + } + + override fun initListener() { + super.initListener() + binding.ivWxLogin.onClick { + if (!api.isWXAppInstalled) { + toast("您没有安装微信客户端,请先下载安装") + return@onClick + } + if (!binding.cbAgree.isChecked) { + val loginTipDialog = LoginTipDialog.newInstance(1) + loginTipDialog.setOnSelectListener { + binding.cbAgree.isChecked = true + wxAuth() + } + loginTipDialog.show(childFragmentManager, loginTipDialog::class.java.simpleName) + } else { + wxAuth() + } + } + binding.ivPhoneLogin.onClick { + if (requireActivity() is LoginActivity) { + PublicActivity.start(requireContext(), LoginFragment::class.java) + } else { + requireActivity().finish() + } + } + } + + override fun initObserve() { + super.initObserve() + mViewModel.accountLiveData.observe(this) { + if (it.isNotEmpty()) { + AccountListDialog.newInstance(it).show(childFragmentManager, AccountListDialog::class.java.simpleName) + } + } + mViewModel.onekeyLoginLiveData.observe(this) { + EventReportManager.eventReport(EventConstants.LOGIN, "onekey", gyuid) + ConvertReportHelper.onEventRegister("onekey",true) + LoginManager.saveLastLoginType("onekey") + loginSuccess(it) + } + mViewModel.wxLoginLiveData.observe(this) { + EventReportManager.eventReport(EventConstants.LOGIN, "weixin", "") + ConvertReportHelper.onEventRegister("weixin",true) + LoginManager.saveLastLoginType("weixin") + loginSuccess(it) + } + val wxLoginEvent = RxBus.defaultInstance.toObservable(WxLoginEvent::class.java).subscribe { + isWxRequesting = false + if (!TextUtils.isEmpty(it.code) && isVisible && it.state.endsWith("onekey")) loginByWx(it.code) + } + addDisposable(wxLoginEvent) + val loginSuccessEvent = RxBus.defaultInstance.toObservable(LoginSuccessEvent::class.java).subscribe { + requireActivity().finish() + } + addDisposable(loginSuccessEvent) + } + + private fun initOneKeyLogin() { + val eloginActivityParam = EloginActivityParam() + .setActivity(requireActivity()) + .setNumberTextview(binding.tvPhoneNumber) + .setSloganTextview(binding.tvSlogan) + .setLoginButton(binding.loginBtn) + .setPrivacyCheckbox(binding.cbAgree) + .setPrivacyTextview(binding.tvAgree) + .setUiErrorListener { //隐私协议未打勾、界面不合规、setLoginOnClickListener抛出异常等情况下的回调 + mViewModel.dismissDialog() + } + .setLoginOnClickListener { view: View? -> + L.d("一键登录按钮 onLoginClick:") + if (!binding.cbAgree.isChecked) { + val loginTipDialog = LoginTipDialog.newInstance(1) + loginTipDialog.setOnSelectListener { + binding.cbAgree.isChecked = true + } + loginTipDialog.show(childFragmentManager, loginTipDialog::class.java.simpleName) + // 抛出异常,避免sdk进行后续登录动作(否则eAccountLogin会回调onFailed错误) + throw IllegalStateException("请先仔细阅读协议并勾选,然后再点击登录") + } + mViewModel.showDialog() + } + // 请在预登录成功之后、设置好privacyTv的内容之后再调用eAccountLogin + GYManager.getInstance().eAccountLogin(eloginActivityParam, 5000, object : GyCallBack { + override fun onSuccess(response: GYResponse) { + try { + val jsonObject = JSONObject.parseObject(response.msg) + val data = jsonObject.getJSONObject("data") + val token = data.getString("token") + gyuid = response.gyuid + mViewModel.oneKeyLogin(response.gyuid, token) + } catch (e: Exception) { + e.printStackTrace() + } + + } + + override fun onFailed(response: GYResponse) { + L.e("登录失败 response:$response") + try { + val jsonObject = JSONObject.parseObject(response.msg) + val errorCode = jsonObject.getIntValue("errorCode") + if (errorCode != -20301) { //关闭一键登录界面不用提示 + toast("一键登录失败:$response") + } + //... + } catch (e: Exception) { + e.printStackTrace() + } + + } + }) + } + + private fun loginSuccess(entity: com.cheng.bole.bean.LoginEntity) { + LoginManager.saveToken(entity.token) + UserConfigManager.userConfig { + requireActivity().startActivity() + RxBus.defaultInstance.post(LoginSuccessEvent()) + } + } + + private fun initLoginType() { + val loginType = UserConfigManager.getLoginType() + if (loginType.contains("weixin")) { + binding.ivWxLogin.visible() + } else { + binding.ivWxLogin.gone() + } + } + + /** + * 初始化隐私协议,请在onCreate/预登录成功之后&调用eAccountLogin之前就设置好,不允许动态改变 + * + * @param textView + */ + private fun initPrivacyTv() { + val preLoginResult = GYManager.getInstance().preLoginResult + SpanUtils.with(binding.tvAgree) + .append("登录即认可") + .append("《${preLoginResult.privacyName}》") + .setClickSpan(getColor(R.color.color_466afd), false) { + BrowserActivity.start(requireContext(), preLoginResult.privacyName, preLoginResult.privacyUrl, false) + } + .append("、") + .append("《用户协议》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startUserAgreement(requireContext()) + } + .append("和") + .append("《隐私政策》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startPrivacyPolicy(requireContext()) + } + .append("并使用本机号码登录") + .create() + } + + /** + * 微信登录 + */ + private fun loginByWx(wechatCode: String) { + val jsonWx = JsonObject() + jsonWx.addProperty("code", wechatCode) + jsonWx.addProperty("code_type", "") + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "weixin") + jsonObject.add("weixin", jsonWx) + mViewModel.loginByWx(jsonObject.toString().toRequestBody()) + } + + private fun wxAuth() { + if (isWxRequesting) return + if (!api.isWXAppInstalled) { + toast("未安装微信客户端,请先下载安装微信客户端") + return + } + isWxRequesting = true + val req = SendAuth.Req() + req.scope = "snsapi_userinfo" // 只能填 snsapi_userinfo + req.state = Utils.getApp().packageName + Math.random() * 1000 + "_onekey" + api.sendReq(req) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/login/onekey/OnekeyLoginViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/login/onekey/OnekeyLoginViewModel.kt new file mode 100644 index 0000000..5768bd5 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/login/onekey/OnekeyLoginViewModel.kt @@ -0,0 +1,65 @@ +package com.cheng.bole.ui.fragment.login.onekey + +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.google.gson.JsonObject +import com.cheng.bole.net.ApiFactory +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody + +class OnekeyLoginViewModel : BaseViewModel() { + val accountLiveData = MutableLiveData>() + val onekeyLoginLiveData = MutableLiveData() + val wxLoginLiveData = MutableLiveData() + + fun accountList() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.accountList("login") + if (response.status) { + accountLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun oneKeyLogin(gyuid: String, token: String) { + launchOnUiTryCatch({ + val jsonOneKey = JsonObject() + jsonOneKey.addProperty("gyuid", gyuid) + jsonOneKey.addProperty("token", token) + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "onekey") + jsonObject.add("onekey", jsonOneKey) + jsonObject.addProperty("bind", false) + val response = ApiFactory.apiService.login(jsonObject.toString().toRequestBody()) + if (response.status) { + onekeyLoginLiveData.postValue(response.data) + } else toast(response.message, true) + }, { + setError(it) + L.d(it) + }) + } + + fun loginByWx(requestBody: RequestBody) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.login(requestBody) + if (response.status) { + wxLoginLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/main/MainFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/main/MainFragment.kt new file mode 100644 index 0000000..8f82fbd --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/main/MainFragment.kt @@ -0,0 +1,176 @@ +package com.cheng.bole.ui.fragment.main + +import android.text.TextUtils +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT +import com.cheng.bole.R +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentMainBinding +import com.cheng.bole.event.HomeRefreshEvent +import com.cheng.bole.event.LoginSuccessEvent +import com.cheng.bole.event.MineRefreshEvent +import com.cheng.bole.event.PushMessageEvent +import com.cheng.bole.event.VipPaySuccessEvent +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.activity.LoginActivity +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.ui.base.BasePageAdapter +import com.cheng.bole.ui.dialog.VipLoginTipDialog +import com.cheng.bole.ui.fragment.home.HomeFragment +import com.cheng.bole.ui.fragment.mine.MineFragment +import com.cheng.bole.ui.fragment.mine.coupon.CouponFragment +import com.cheng.bole.ui.fragment.mine.vip.VipFragment +import com.example.base.common.RxBus +import com.example.base.extensions.toast +import com.example.base.ui.BaseFragment +import com.google.gson.Gson +import com.gyf.immersionbar.ImmersionBar +import com.huantansheng.easyphotos.EasyPhotos + + +class MainFragment : BaseFragment() { + private val tabText = listOf("首页", "我的") + private val fragmentList by lazy { mutableListOf() } + private val pageAdapter by lazy { BasePageAdapter(childFragmentManager, tabText, fragmentList, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) } + + private var userInfo: com.cheng.bole.bean.UserEntity? = null + + private val intervalTime: Int = 3000 + private var lastCheckTime = System.currentTimeMillis() + + private var currentPosition = 0 + + override fun initView() { + super.initView() + ImmersionBar.with(this).navigationBarColor(R.color.white).init() + EasyPhotos.preLoad(requireContext()) + + initFragment() + binding.viewPager.adapter = pageAdapter + binding.viewPager.offscreenPageLimit = fragmentList.size + } + + private fun initFragment() { + fragmentList.add(HomeFragment()) + fragmentList.add(MineFragment()) + } + + override fun initListener() { + super.initListener() + binding.rgNavigation.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.rb_home -> currentPosition = 0 + R.id.rb_mine -> currentPosition = 1 + } + binding.viewPager.setCurrentItem(currentPosition, false) + + if (userInfo != null && userInfo!!.temp && (userInfo?.vip == "2" || userInfo?.vip == "3")) { + val vipLoginTipDialog = VipLoginTipDialog(this@MainFragment) + vipLoginTipDialog.setOnSelectListener { + LoginActivity.start(requireActivity()) + requireActivity().finish() + } + vipLoginTipDialog.show() + } + + when (currentPosition) { + 0 -> RxBus.defaultInstance.post(HomeRefreshEvent()) + } + lastCheckTime = System.currentTimeMillis() + + EventReportManager.eventReport(EventConstants.HOME_BOTTOM_TAB_CHECK, tabText[currentPosition], "") + } + } + + override fun initData() { + super.initData() + UserConfigManager.userConfig { + checkVersion() + } + } + + override fun onResume() { + super.onResume() + mViewModel.userInfo() + } + + override fun initObserve() { + super.initObserve() + mViewModel.userInfoLiveData.observe(this) { + userInfo = it + UserConfigManager.userInfoLiveData.postValue(it) + + if (it.temp && (it.vip == "2" || it.vip == "3")) { + val vipLoginTipDialog = VipLoginTipDialog(this) + vipLoginTipDialog.setOnSelectListener { + LoginActivity.start(requireActivity()) + requireActivity().finish() + } + vipLoginTipDialog.show() + } + if (!TextUtils.isEmpty(UserConfigManager.getGTCid())) { + if (UserConfigManager.getGTCid() != it.client_cid) { + mViewModel.updateGTCid(UserConfigManager.getGTCid()) + } + } + } + mViewModel.getCouponLiveData.observe(this) { + toast("领取成功") + PublicActivity.start(requireContext(), CouponFragment::class.java) + } + + val loginSuccessEvent = RxBus.defaultInstance.toObservable(LoginSuccessEvent::class.java).subscribe { + RxBus.defaultInstance.post(HomeRefreshEvent()) + RxBus.defaultInstance.post(MineRefreshEvent()) + } + addDisposable(loginSuccessEvent) + + val paySuccessEvent = RxBus.defaultInstance.toObservable(VipPaySuccessEvent::class.java).subscribe { + UserConfigManager.userConfig {} + } + addDisposable(paySuccessEvent) + + val pushMessageEvent = RxBus.defaultInstance.toObservable(PushMessageEvent::class.java).subscribe { + if (!TextUtils.isEmpty(it.payload)) { + val payload = Gson().fromJson(it.payload, com.cheng.bole.bean.PushPayloadEntity::class.java) + if (it.type == 3) handlePushMessage(payload) + mViewModel.reportPushReceipt(payload.msgId, "${it.type}") + } + } + addDisposable(pushMessageEvent) + } + + private fun handlePushMessage(payloadEntity: com.cheng.bole.bean.PushPayloadEntity) { + if (TextUtils.isEmpty(payloadEntity.page)) return + when (payloadEntity.page) { + "member_recharge" -> { + PublicActivity.start( + requireContext(), + VipFragment::class.java, + Pair("origin", parseData("source", payloadEntity.params)), +// Pair("goodsType", parseData("type", payloadEntity.params)) + ) + } + } + } + + private fun parseData(key: String, paramStr: String): String { + if (!TextUtils.isEmpty(paramStr)) { + val param = paramStr.split(",") + for (i in param.indices) { + val keyValue = param[i].split("=") + if (keyValue.size > 1) { + if (keyValue[0] == key) { + return keyValue[1] + } + } + } + } + return "" + } + + private fun checkVersion() { + com.cheng.bole.common.AppUpdate.checkUpdate(this, false) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/main/MainViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/main/MainViewModel.kt new file mode 100644 index 0000000..da06b35 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/main/MainViewModel.kt @@ -0,0 +1,64 @@ +package com.cheng.bole.ui.fragment.main + +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.google.gson.JsonObject +import com.cheng.bole.net.ApiFactory +import okhttp3.RequestBody.Companion.toRequestBody + +class MainViewModel : BaseViewModel() { + val userInfoLiveData = MutableLiveData() + val getCouponLiveData = MutableLiveData() + + fun userInfo() { + launchOnUiTryCatch({ + val response = ApiFactory.apiService.userInfo() + if (response.status) { + userInfoLiveData.postValue(response.data) + } + }, { + setError(it) + L.d(it) + }) + } + + fun getActivityCoupon(ids: String, type: Int) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.getActivityCoupons(ids) + if (response.status) { + getCouponLiveData.postValue(type) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun updateGTCid(cid: String) { + launchOnUiTryCatch({ + val jsonObject = JsonObject() + jsonObject.addProperty("client_cid", cid) + ApiFactory.apiService.changUser(jsonObject.toString().toRequestBody()) + }, { + setError(it) + L.d(it) + }) + } + + fun reportPushReceipt(msgId: String, status: String) { + launchOnUiTryCatch({ + val jsonObject = JsonObject() + jsonObject.addProperty("id", msgId) + jsonObject.addProperty("status", status) + ApiFactory.apiService.reportPushReceipt(jsonObject.toString().toRequestBody()) + }, { + setError(it) + L.d(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/MineFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/MineFragment.kt new file mode 100644 index 0000000..a62b37b --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/MineFragment.kt @@ -0,0 +1,11 @@ +package com.cheng.bole.ui.fragment.mine + +import com.cheng.bole.databinding.FragmentMineBinding +import com.example.base.ui.BaseFragment + +/** + * 我的 + */ +class MineFragment : BaseFragment() { + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/MineViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/MineViewModel.kt new file mode 100644 index 0000000..eb35a5d --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/MineViewModel.kt @@ -0,0 +1,62 @@ +package com.cheng.bole.ui.fragment.mine + +import androidx.lifecycle.MutableLiveData +import com.cheng.bole.common.Constants +import com.cheng.bole.net.ApiFactory +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.utils.Utils +import com.example.base.viewmodel.BaseViewModel +import com.tencent.mm.opensdk.constants.Build +import com.tencent.mm.opensdk.modelbiz.WXOpenCustomerServiceChat +import com.tencent.mm.opensdk.openapi.WXAPIFactory + +class MineViewModel : BaseViewModel() { + val userInfoLiveData = MutableLiveData() + + fun userInfo(showLoading: Boolean = false) { + if (showLoading) showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.userInfo() + if (response.status) { + userInfoLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun wxService() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.wxService() + if (response.status) { + contactClientService(response.data) + } else { + toast(response.message, true) + } + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + private fun contactClientService(data: com.cheng.bole.bean.WxServiceEntity) { + val api = WXAPIFactory.createWXAPI(Utils.getApp(), Constants.WechatAppId) + if (!api!!.isWXAppInstalled) { + toast("未安装微信客户端") + return + } + if (api.wxAppSupportAPI >= Build.SUPPORT_OPEN_CUSTOMER_SERVICE_CHAT) { + val req = WXOpenCustomerServiceChat.Req() + req.corpId = data.corpid + req.url = data.address + api.sendReq(req) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AboutFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AboutFragment.kt new file mode 100644 index 0000000..c385c06 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AboutFragment.kt @@ -0,0 +1,39 @@ +package com.cheng.bole.ui.fragment.mine.about + +import android.annotation.SuppressLint +import com.example.base.extensions.onClick +import com.example.base.ui.BaseFragment +import com.example.base.utils.AppUtils +import com.cheng.bole.databinding.FragmentAboutBinding +import com.cheng.bole.ui.dialog.PopupDialog +import com.cheng.bole.utils.ChannelUtils +import com.cheng.bole.utils.UrlHelper +import org.jetbrains.anko.sdk27.listeners.onLongClick + +class AboutFragment : BaseFragment() { + @SuppressLint("SetTextI18n") + override fun initView() { + super.initView() + mTitleBar?.background = null + + binding.tvVersionName.text = "版本号:${AppUtils.getAppVersionName()}" + + if (ChannelUtils.getChannel() == "huawei") { + binding.tvAppName.text = AppUtils.getAppName() + } + } + + override fun initListener() { + super.initListener() + binding.ivIco.onLongClick { + PopupDialog.showAboutTip(requireContext(),binding.ivIco) + false + } + binding.tvAgreement.onClick { + UrlHelper.startUserAgreement(requireContext()) + } + binding.tvPolicy.onClick { + UrlHelper.startPrivacyPolicy(requireContext()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AboutViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AboutViewModel.kt new file mode 100644 index 0000000..8f14041 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AboutViewModel.kt @@ -0,0 +1,6 @@ +package com.cheng.bole.ui.fragment.mine.about + +import com.example.base.viewmodel.BaseViewModel + +class AboutViewModel : BaseViewModel() { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AppConfigFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AppConfigFragment.kt new file mode 100644 index 0000000..723135e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/about/AppConfigFragment.kt @@ -0,0 +1,15 @@ +package com.cheng.bole.ui.fragment.mine.about + +import com.example.base.ui.BaseFragment +import com.google.gson.Gson +import com.cheng.bole.databinding.FragmentAppConfigBinding +import com.cheng.bole.manager.UserConfigManager + +class AppConfigFragment:BaseFragment() { + override fun initData() { + super.initData() + UserConfigManager.userConfig.apply { + binding.tvContent.text = Gson().toJson(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountAdapter.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountAdapter.kt new file mode 100644 index 0000000..700a48f --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountAdapter.kt @@ -0,0 +1,33 @@ +package com.cheng.bole.ui.fragment.mine.account + +import android.widget.ImageView +import coil.load +import coil.transform.CircleCropTransformation +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.cheng.bole.R +import com.cheng.bole.manager.UserConfigManager + +class AccountAdapter:BaseQuickAdapter(R.layout.listitem_account) { + + override fun convert(holder: BaseViewHolder, item: com.cheng.bole.bean.AccountEntity) { + holder.getView(R.id.iv_avatar).load(item.avater) { + transformations(CircleCropTransformation()) + placeholder(R.mipmap.ic_default_avatar) + error(R.mipmap.ic_default_avatar) + } + if (item.vip_type == "1") { + holder.setGone(R.id.tv_vip_tag, true) + } else if (item.vip_type == "2" || item.vip_type == "3") { + holder.setText(R.id.tv_vip_tag, if (item.vip_type == "2") item.vip_name else "终生会员") + holder.setGone(R.id.tv_vip_tag, false) + } + holder.setVisible(R.id.tv_current_account, item.user_id == UserConfigManager.userInfo?.user_id) + holder.setVisible(R.id.tv_change_account, item.user_id != UserConfigManager.userInfo?.user_id) + + holder.setText(R.id.tv_username, item.name) + holder.setText(R.id.tv_user_id, "ID:${item.user_id}") + holder.setGone(R.id.iv_bind_wx, !item.bind.contains("weixin")) + holder.setGone(R.id.iv_bind_phone, !item.bind.contains("phone")) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountManageFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountManageFragment.kt new file mode 100644 index 0000000..519430c --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountManageFragment.kt @@ -0,0 +1,100 @@ +package com.cheng.bole.ui.fragment.mine.account + +import android.graphics.Color +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentAccountManageBinding +import com.cheng.bole.manager.DialogEnum +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.dialog.TipDialog +import com.example.base.common.RxBus +import com.example.base.extensions.toast +import com.example.base.ui.BaseFragment +import com.google.gson.JsonObject +import okhttp3.RequestBody.Companion.toRequestBody +import org.jetbrains.anko.backgroundColor + +class AccountManageFragment : BaseFragment() { + private val mAdapter by lazy { AccountAdapter() } + private var mAccount: com.cheng.bole.bean.AccountEntity? = null + + override fun initView() { + super.initView() + mTitleBar?.backgroundColor = Color.WHITE + + binding.rvAccount.adapter = mAdapter + } + + override fun initData() { + super.initData() + mViewModel.accountList() + } + + override fun initListener() { + super.initListener() + mAdapter.setOnItemClickListener { _, _, i -> + val item = mAdapter.getItem(i) + if (item.user_id != UserConfigManager.userInfo?.user_id) { + val f = TipDialog.newInstance("提示", "您确定要切换账户吗?") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + LoginManager.saveToken("") + changeAccount(mAdapter.getItem(i).user_id) + + EventReportManager.eventReport(EventConstants.SWITCH_ACCOUNT, mAdapter.getItem(i).user_id, "") + } + } + f.show(childFragmentManager, TipDialog::class.java.simpleName) + } + } + } + + override fun initObserve() { + super.initObserve() + mViewModel.accountLiveData.observe(this) { + val list = it.toMutableList() + for (account in list) { + if (account.user_id == UserConfigManager.userInfo?.user_id) { + mAccount = account + list.remove(account) + break + } + } + mAdapter.setList(list) + if (mAccount != null) { + mAdapter.addData(0, mAccount!!) + } + } + + mViewModel.loginLiveData.observe(this) { + EventReportManager.eventReport(EventConstants.LOGIN, "device", it.user_id) + loginSuccess(it) + } + } + + /** + * 切换账户 + */ + private fun changeAccount(userId: String) { + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "device") + + val jsonDevice = JsonObject() + jsonDevice.addProperty("user_id", userId) + jsonObject.add("device", jsonDevice) + mViewModel.login(jsonObject.toString().toRequestBody()) + } + + private fun loginSuccess(entity: com.cheng.bole.bean.LoginEntity) { + mViewModel.showDialog() + LoginManager.saveToken(entity.token) + UserConfigManager.saveLastUserinfo(UserConfigManager.userInfo) + UserConfigManager.userConfig { + toast("切换成功") + mViewModel.dismissDialog() + RxBus.defaultInstance.post(com.cheng.bole.event.LoginSuccessEvent()) + requireActivity().finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountManageViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountManageViewModel.kt new file mode 100644 index 0000000..8a5373a --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/AccountManageViewModel.kt @@ -0,0 +1,43 @@ +package com.cheng.bole.ui.fragment.mine.account + +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.cheng.bole.net.ApiFactory +import okhttp3.RequestBody + +class AccountManageViewModel : BaseViewModel() { + val accountLiveData = MutableLiveData>() + val loginLiveData = MutableLiveData() + + fun accountList() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.accountList("account") + if (response.status) { + accountLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun login(requestBody: RequestBody) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.login(requestBody) + if (response.status) { + loginLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/BindAccountFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/BindAccountFragment.kt new file mode 100644 index 0000000..ba70cc7 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/BindAccountFragment.kt @@ -0,0 +1,217 @@ +package com.cheng.bole.ui.fragment.mine.account + +import android.graphics.Color +import android.text.TextUtils +import com.example.base.common.RxBus +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.ui.BaseFragment +import com.example.base.utils.Utils +import com.google.gson.JsonObject +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import com.cheng.bole.common.Constants +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentBindAccountBinding +import com.cheng.bole.event.HomeRefreshEvent +import com.cheng.bole.event.MineRefreshEvent +import com.cheng.bole.event.WxLoginEvent +import com.cheng.bole.manager.DialogEnum +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.dialog.BindPhoneDialog +import com.cheng.bole.ui.dialog.TipDialog +import okhttp3.RequestBody.Companion.toRequestBody +import org.jetbrains.anko.backgroundColor + +class BindAccountFragment : BaseFragment() { + private var userInfo: com.cheng.bole.bean.UserEntity? = null + private lateinit var api: IWXAPI + + private var isVisible = false + + private var isWxRequesting = false + + override fun initView() { + super.initView() + mTitleBar?.backgroundColor = Color.WHITE + } + + override fun initData() { + super.initData() + mViewModel.userInfo() + api = WXAPIFactory.createWXAPI(requireContext(), Constants.WechatAppId) + } + + override fun onStart() { + super.onStart() + isVisible = true + } + + override fun onStop() { + super.onStop() + isVisible = false + } + + override fun initListener() { + super.initListener() + binding.btnBindWx.onClick { + if (TextUtils.isEmpty(userInfo?.unionid)) { + wxAuth() + } else { + val f = TipDialog.newInstance("解除微信绑定", "解绑后将无法使用该微信号登录此账号,请谨慎操作!") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + if (!TextUtils.isEmpty(userInfo?.phone)) { + unbindWx() + } else { + val f1 = TipDialog.newInstance( + "预警提示", + "您若继续解除绑定,您的账号将没有绑定账号,软件在使用过程中可能会造成账号丢失!是否继续解除绑定!" + ) + f1.setOnSelectListener { e -> + if (e == DialogEnum.CLICK_OK) { + unbindWx() + } + } + f1.show(childFragmentManager, TipDialog::class.java.simpleName) + } + } + } + f.show(childFragmentManager, TipDialog::class.java.simpleName) + } + } + binding.btnBindPhone.onClick { + if (TextUtils.isEmpty(userInfo?.phone)) { + val f = BindPhoneDialog.newInstance() + f.setOnSendCodeClickListener { phone -> + mViewModel.sendCode(phone) { + f.setTimestamp(it.timestamp) + } + } + f.setOnBackListener { phone, code, timestamp -> + bindPhone(phone, code, timestamp) + } + f.show(childFragmentManager, BindPhoneDialog::class.java.simpleName) + } else { + val f = TipDialog.newInstance("解除手机绑定", "解绑后将无法使用该手机号登录此账号,请谨慎操作!") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + if (!TextUtils.isEmpty(userInfo?.unionid)) { + unbindPhone() + } else { + val f1 = TipDialog.newInstance( + "预警提示", + "您若继续解除绑定,您的账号将没有绑定账号,软件在使用过程中可能会造成账号丢失!是否继续解除绑定!" + ) + f1.setOnSelectListener { e -> + if (e == DialogEnum.CLICK_OK) { + unbindPhone() + } + } + f1.show(childFragmentManager, TipDialog::class.java.simpleName) + } + } + } + f.show(childFragmentManager, TipDialog::class.java.simpleName) + } + } + } + + override fun initObserve() { + super.initObserve() + mViewModel.userInfoLiveData.observe(this) { + userInfo = it + setUserInfo() + } + mViewModel.bindPhoneLiveData.observe(this) { + toast("绑定成功") + refreshData() + EventReportManager.eventReport(EventConstants.ACCOUNT_BIND, it, "") + } + mViewModel.unbindPhoneLiveData.observe(this) { + toast("解绑成功") + refreshData() + EventReportManager.eventReport(EventConstants.ACCOUNT_UNBIND, it, "") + } + val wxLoginEvent = RxBus.defaultInstance.toObservable(WxLoginEvent::class.java).subscribe { + isWxRequesting = false + if (!TextUtils.isEmpty(it.code) && isVisible && it.state.endsWith("bind")) bindWx(it.code) + } + addDisposable(wxLoginEvent) + } + + private fun refreshData() { + UserConfigManager.userConfig { + mViewModel.userInfo() + RxBus.defaultInstance.post(HomeRefreshEvent()) + RxBus.defaultInstance.post(MineRefreshEvent()) + } + } + + private fun setUserInfo() { + if (TextUtils.isEmpty(userInfo?.unionid)) { + binding.btnBindWx.text = "立即绑定" + } else { + binding.btnBindWx.text = "解除绑定" + } + if (TextUtils.isEmpty(userInfo?.phone)) { + binding.btnBindPhone.text = "立即绑定" + } else { + binding.btnBindPhone.text = "解除绑定" + } + } + + private fun bindWx(code: String) { + val jsonWx = JsonObject() + jsonWx.addProperty("code", code) + jsonWx.addProperty("code_type", "") + + val jsonObject = JsonObject() + jsonObject.add("weixin", jsonWx) + jsonObject.addProperty("login_type", "weixin") + jsonObject.addProperty("bind", true) + mViewModel.bind(jsonObject.toString().toRequestBody(), "weixin") + } + + private fun bindPhone(phone: String, code: String, timestamp: String) { + val jsonPhone = JsonObject() + jsonPhone.addProperty("timestamp", timestamp) + jsonPhone.addProperty("phone", phone) + jsonPhone.addProperty("code", code) + + val jsonObject = JsonObject() + jsonObject.add("phone", jsonPhone) + jsonObject.addProperty("login_type", "phone") + jsonObject.addProperty("bind", true) + mViewModel.bind(jsonObject.toString().toRequestBody(), "phone") + } + + private fun unbindWx() { + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "weixin") + jsonObject.addProperty("unbind", true) + mViewModel.unbind(jsonObject.toString().toRequestBody(), "weixin") + } + + private fun unbindPhone() { + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "phone") + jsonObject.addProperty("unbind", true) + mViewModel.unbind(jsonObject.toString().toRequestBody(), "phone") + } + + private fun wxAuth() { + if (isWxRequesting) return + if (!api.isWXAppInstalled) { + toast("未安装微信客户端,请先下载安装微信客户端") + return + } + isWxRequesting = true + val req = SendAuth.Req() + req.scope = "snsapi_userinfo" // 只能填 snsapi_userinfo + req.state = Utils.getApp().packageName + Math.random() * 1000 + "_bind" + api.sendReq(req) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/BindAccountViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/BindAccountViewModel.kt new file mode 100644 index 0000000..866ad2e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/account/BindAccountViewModel.kt @@ -0,0 +1,82 @@ +package com.cheng.bole.ui.fragment.mine.account + +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.cheng.bole.net.ApiFactory +import okhttp3.RequestBody + +class BindAccountViewModel : BaseViewModel() { + val userInfoLiveData = MutableLiveData() + var bindPhoneLiveData = MutableLiveData() + var unbindPhoneLiveData = MutableLiveData() + + fun userInfo() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.userInfo() + if (response.status) { + userInfoLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun sendCode(phone: String, callback: (com.cheng.bole.bean.SendCodeEntity) -> Unit) { + showDialog() + launchOnUiTryCatch({ + val map = HashMap() + map["phone"] = phone + val response = ApiFactory.apiService.sendCode(map) + if (response.status) { + callback.invoke(response.data) + } else { + toast(response.message, true) + } + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + //绑定 手机,微信 + fun bind(requestBody: RequestBody, type: String) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.login(requestBody) + if (response.status) { + bindPhoneLiveData.postValue(type) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + + } + + //解绑 手机,微信 + fun unbind(requestBody: RequestBody, type: String) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.login(requestBody) + if (response.status) { + unbindPhoneLiveData.postValue(type) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/CouponFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/CouponFragment.kt new file mode 100644 index 0000000..029e58e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/CouponFragment.kt @@ -0,0 +1,90 @@ +package com.cheng.bole.ui.fragment.mine.coupon + +import android.graphics.Typeface +import androidx.fragment.app.Fragment +import androidx.viewpager.widget.ViewPager.OnPageChangeListener +import com.cheng.bole.R +import com.cheng.bole.databinding.FragmentCouponBinding +import com.cheng.bole.ui.base.BasePageAdapter +import com.cheng.bole.ui.fragment.mine.coupon.list.CouponListFragment +import com.example.base.extensions.getColor +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.visible +import com.example.base.ui.BaseFragment + +class CouponFragment : BaseFragment() { + private val fragmentList by lazy { mutableListOf() } + private val pageAdapter by lazy { BasePageAdapter(childFragmentManager, listOf("未使用", "已过期"), fragmentList) } + + override fun initView() { + super.initView() + mTitleBar?.background = null + mTitleBar?.titleView?.setTextColor(getColor(R.color.color_1a1a1a)) + setBackColor(R.color.black) + + fragmentList.add(CouponListFragment.newInstance("1", "1")) + fragmentList.add(CouponListFragment.newInstance("1", "2")) + + binding.viewPager.adapter = pageAdapter + } + + override fun initData() { + super.initData() + } + + override fun initListener() { + super.initListener() + + binding.tvUnused.onClick { + binding.viewPager.setCurrentItem(0) + } + + binding.tvExpired.onClick { + binding.viewPager.setCurrentItem(1) + } + + binding.viewPager.addOnPageChangeListener(object : OnPageChangeListener { + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + + } + + override fun onPageSelected(position: Int) { + setTabStyle(position) + } + + override fun onPageScrollStateChanged(state: Int) { + + } + + }) + } + + override fun initObserve() { + super.initObserve() + } + + private fun setTabStyle(position: Int) { + if (position == 0) { + binding.tvUnused.textSize = 16f + binding.tvUnused.setTextColor(getColor(R.color.color_1a1a1a)) + binding.tvUnused.typeface = Typeface.DEFAULT_BOLD + binding.ivUnusedIndicator.visible() + + binding.tvExpired.textSize = 14f + binding.tvExpired.setTextColor(getColor(R.color.color_999999)) + binding.tvExpired.typeface = Typeface.DEFAULT + binding.ivExpiredIndicator.gone() + } else { + binding.tvExpired.textSize = 16f + binding.tvExpired.setTextColor(getColor(R.color.color_1a1a1a)) + binding.tvExpired.typeface = Typeface.DEFAULT_BOLD + binding.ivExpiredIndicator.visible() + + binding.tvUnused.textSize = 14f + binding.tvUnused.setTextColor(getColor(R.color.color_999999)) + binding.tvUnused.typeface = Typeface.DEFAULT + binding.ivUnusedIndicator.gone() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/CouponViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/CouponViewModel.kt new file mode 100644 index 0000000..c583f3e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/CouponViewModel.kt @@ -0,0 +1,6 @@ +package com.cheng.bole.ui.fragment.mine.coupon + +import com.example.base.viewmodel.BaseViewModel + +class CouponViewModel : BaseViewModel() { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponAdapter.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponAdapter.kt new file mode 100644 index 0000000..54fc36c --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponAdapter.kt @@ -0,0 +1,76 @@ +package com.cheng.bole.ui.fragment.mine.coupon.list + +import android.text.TextUtils +import android.widget.TextView +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.extensions.getColor +import com.example.base.extensions.getYYYYMMDD2 +import com.example.base.ui.list.LoadMoreAdapter +import com.example.base.utils.SpanUtils +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import java.text.DecimalFormat + +class CouponAdapter(private var expired: Boolean) : LoadMoreAdapter(R.layout.listitem_coupon) { + + init { + addChildClickViewIds(R.id.btn_next) + } + + override fun convert(holder: BaseViewHolder, item: com.cheng.bole.bean.CouponEntity) { + holder.setText( + R.id.tv_name, + if (!TextUtils.isEmpty(item.coupon_name)) item.coupon_name else "${item.coupon_value_name}${item.coupon_type_name}" + ) + holder.setText(R.id.tv_expire_time, "有效期至 ${(item.expire_timestamp.toLong() * 1000).getYYYYMMDD2()}") + holder.setText(R.id.tv_type, item.coupon_type_name) + holder.getView(R.id.tv_discount_amount).typeface = Constants.dDIN_PRO_M + when (item.coupon_type) { + com.cheng.bole.bean.CouponEntity.TYPE_CASH -> { + SpanUtils.with(holder.getView(R.id.tv_discount_amount)) + .append("¥") + .setFontSize(12, true) + .append(DecimalFormat("0.##").format(item.coupon_value.toFloat() / 100)) + .create() + if (!TextUtils.isEmpty(item.threshold)) { + holder.setText(R.id.tv_threshold_amount, "满${DecimalFormat("0.##").format(item.threshold.toFloat() / 100)}可用") + } else { + holder.setText(R.id.tv_threshold_amount, "无门槛") + } + } + + com.cheng.bole.bean.CouponEntity.TYPE_DISCOUNT -> { + SpanUtils.with(holder.getView(R.id.tv_discount_amount)) + .append(DecimalFormat("0.##").format(item.coupon_value.toFloat() * 10)) + .append("折") + .setFontSize(12, true) + .create() + if (!TextUtils.isEmpty(item.threshold)) { + holder.setText(R.id.tv_threshold_amount, "满${DecimalFormat("0.##").format(item.threshold.toFloat() / 100)}可用") + } else { + holder.setText(R.id.tv_threshold_amount, "无门槛") + } + } + } + + if (!expired) { + holder.setTextColor(R.id.tv_discount_amount, getColor(R.color.color_ff5846)) + holder.setTextColor(R.id.tv_threshold_amount, getColor(R.color.color_ff5846)) + holder.setTextColor(R.id.tv_name, getColor(R.color.color_1a1a1a)) + holder.setTextColor(R.id.tv_type, getColor(R.color.color_fb7528)) + holder.setBackgroundResource(R.id.tv_type, R.drawable.shape_10fb7528_cor2) + holder.setBackgroundResource(R.id.layout_amount, R.drawable.shape_fdf4f2_cor6) + holder.setGone(R.id.btn_next, false) + holder.setGone(R.id.iv_expire_tag, true) + } else { + holder.setTextColor(R.id.tv_discount_amount, getColor(R.color.color_d8d8d8)) + holder.setTextColor(R.id.tv_threshold_amount, getColor(R.color.color_d8d8d8)) + holder.setTextColor(R.id.tv_name, getColor(R.color.color_d8d8d8)) + holder.setTextColor(R.id.tv_type, getColor(R.color.color_d8d8d8)) + holder.setBackgroundResource(R.id.tv_type, R.drawable.shape_10d8d8d8_cor2) + holder.setBackgroundResource(R.id.layout_amount, R.drawable.shape_f6f6f6_cor6) + holder.setGone(R.id.btn_next, true) + holder.setGone(R.id.iv_expire_tag, false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponListFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponListFragment.kt new file mode 100644 index 0000000..93fc410 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponListFragment.kt @@ -0,0 +1,63 @@ +package com.cheng.bole.ui.fragment.mine.coupon.list + +import android.os.Bundle +import com.example.base.common.RxBus +import com.example.base.ui.list.ListFragment +import com.cheng.bole.R +import com.cheng.bole.databinding.FragmentCouponListBinding +import com.cheng.bole.event.CouponRefreshEvent +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.ui.fragment.mine.vip.VipFragment + +class CouponListFragment : ListFragment() { + private val status by lazy { arguments?.getString("status") ?: "1" } + private val expire by lazy { arguments?.getString("expire") ?: "1" } + + companion object { + fun newInstance(status: String, expire: String): CouponListFragment { + val args = Bundle() + args.putString("status", status) + args.putString("expire", expire) + val fragment = CouponListFragment() + fragment.arguments = args + return fragment + } + } + + override fun bindAdapter() = CouponAdapter(expire == "2") + + override fun noDataClick() { + + } + + override fun initView() { + super.initView() + mEmptyView.setBtnVisible(false) + } + + override fun initData() { + super.initData() + mViewModel.params["status"] = status + mViewModel.params["expire"] = expire + firstLoad() + } + + override fun initListener() { + super.initListener() + mAdapter.setOnItemChildClickListener { _, view, _ -> + when(view.id) { + R.id.btn_next -> { + PublicActivity.start(requireContext(), VipFragment::class.java) + } + } + } + } + + override fun initObserve() { + super.initObserve() + val couponRefreshEvent = RxBus.defaultInstance.toObservable(CouponRefreshEvent::class.java).subscribe { + firstLoad() + } + addDisposable(couponRefreshEvent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponListViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponListViewModel.kt new file mode 100644 index 0000000..f966350 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/coupon/list/CouponListViewModel.kt @@ -0,0 +1,13 @@ +package com.cheng.bole.ui.fragment.mine.coupon.list + +import androidx.collection.ArrayMap +import com.example.base.viewmodel.ListViewModel +import com.cheng.bole.net.ApiFactory +import com.cheng.bole.net.model.toListResult + +class CouponListViewModel : ListViewModel() { + + override suspend fun requestApi(params: ArrayMap): Result> { + return ApiFactory.apiService.couponList(params).toListResult() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/feedback/FeedbackFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/feedback/FeedbackFragment.kt new file mode 100644 index 0000000..84e064d --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/feedback/FeedbackFragment.kt @@ -0,0 +1,165 @@ +package com.cheng.bole.ui.fragment.mine.feedback + +import android.annotation.SuppressLint +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import androidx.recyclerview.widget.GridLayoutManager +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.ui.BaseFragment +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.huantansheng.easyphotos.EasyPhotos +import com.huantansheng.easyphotos.callback.SelectCallback +import com.huantansheng.easyphotos.models.album.entity.Photo +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.FragmentFeedbackBinding +import com.cheng.bole.ui.fragment.photo.AddImageAdapter +import com.cheng.bole.utils.PermissionUtils +import okhttp3.RequestBody.Companion.toRequestBody + +class FeedbackFragment : BaseFragment() { + private var type = TYPE_SUGGESTION + + private val imageAdapter by lazy { AddImageAdapter(requireContext(), selectedPhotoList) } + private var selectedPhotoList: ArrayList = ArrayList() + private var selectImg: ArrayList = ArrayList() + private var deletedImg: ArrayList = ArrayList() + + companion object { + const val TYPE_SUGGESTION = 0 + const val TYPE_BUG = 1 + const val TYPE_OTHER = 2 + } + + override fun initView() { + super.initView() + mTitleBar?.background = null + + binding.etFeedbackContent.addTextChangedListener(textWatcher) + + binding.rvImage.layoutManager = GridLayoutManager(context, 3) + imageAdapter.selectMax = 3 + binding.rvImage.adapter = imageAdapter + } + + override fun initData() { + super.initData() + } + + @SuppressLint("NotifyDataSetChanged") + override fun initListener() { + super.initListener() + binding.rgFeedbackType.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.rg_1 -> type = TYPE_SUGGESTION + R.id.rg_2 -> type = TYPE_BUG + R.id.rg_3 -> type = TYPE_OTHER + } + } + binding.btnNext.onClick { + val content = binding.etFeedbackContent.text?.trim().toString() + if (TextUtils.isEmpty(content)) { + toast("请输入详情内容") + return@onClick + } + if (selectImg.isEmpty() && selectedPhotoList.size > 0) { + mViewModel.upload(requireContext(), selectedPhotoList) + } else { + commit() + } + } + + imageAdapter.setOnItemClick { type, position -> + when (type) { + AddImageAdapter.TYPE_ADD -> { + PermissionUtils.checkPhotoPermission(requireActivity(), childFragmentManager, true) { + if (it) selectPhoto() + } + } + + AddImageAdapter.TYPE_DELETE -> { + if (selectImg.isNotEmpty() && selectImg.size == imageAdapter.data.size) { + deletedImg.add(selectImg[position]) + selectImg.removeAt(position) + } + if (selectedPhotoList.isNotEmpty()) { + selectedPhotoList.removeAt(position) + imageAdapter.notifyDataSetChanged() + } + } + } + } + } + + override fun initObserve() { + super.initObserve() + mViewModel.feedbackLiveData.observe(this) { + toast("反馈成功") + requireActivity().finish() + } + mViewModel.uploadSuccessLiveData.observe(this) { + selectImg.addAll(it) + mViewModel.delete(deletedImg) + commit() + } + } + + private fun commit() { + var typeStr = if (type == TYPE_SUGGESTION) "产品建议" else if (type == TYPE_BUG) "功能异常" else "其他问题" + if (!TextUtils.isEmpty(arguments?.getString("text"))) { + typeStr += "(${arguments?.getString("text")})" + } + val content = binding.etFeedbackContent.text?.trim().toString() + val contactInfo = binding.etContactInfo.text?.trim().toString() + val jsonObject = JsonObject() + jsonObject.addProperty("type", typeStr) + jsonObject.addProperty("content", content) + jsonObject.addProperty("contact", contactInfo) + val imageArray = JsonArray() + selectImg.forEach { + imageArray.add(it.url) + } + jsonObject.add("images", imageArray) + mViewModel.feedback(jsonObject.toString().toRequestBody()) + } + + override fun onDestroyView() { + binding.etFeedbackContent.removeTextChangedListener(textWatcher) + super.onDestroyView() + } + + private fun selectPhoto() { + EasyPhotos.createAlbum(this, true, false, com.cheng.bole.utils.GlideEngine.getInstance()) + .setFileProviderAuthority(Constants.AppFilter) + .setCount(imageAdapter.selectMax - selectedPhotoList.size) + .start(object : SelectCallback() { + @SuppressLint("NotifyDataSetChanged") + override fun onResult(photos: ArrayList, isOriginal: Boolean) { + selectedPhotoList.addAll(photos) + imageAdapter.notifyDataSetChanged() + } + + override fun onCancel() { + } + }) + } + + private val textWatcher = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + @SuppressLint("SetTextI18n") + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.tvTextCount.text = "${s?.length}/200" + } + + override fun afterTextChanged(s: Editable?) { + + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/feedback/FeedbackViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/feedback/FeedbackViewModel.kt new file mode 100644 index 0000000..01b5eb9 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/feedback/FeedbackViewModel.kt @@ -0,0 +1,98 @@ +package com.cheng.bole.ui.fragment.mine.feedback + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.huantansheng.easyphotos.models.album.entity.Photo +import com.cheng.bole.net.ApiFactory +import com.cheng.bole.utils.BitmapUtils +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody + +class FeedbackViewModel : BaseViewModel() { + val feedbackLiveData = MutableLiveData() + val uploadSuccessLiveData = MutableLiveData>() + val imgList = ArrayList() + + fun feedback(requestBody: RequestBody) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.feedback(requestBody) + if (response.status) { + feedbackLiveData.postValue(Any()) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + L.d(it) + }) + } + + fun delete(list: ArrayList) { + list.forEachIndexed { index, photo -> + if (index == 0) { + delStoreImg(photo) { + list.remove(photo) + if (list.size > 0) { + delete(list) + } + } + } + } + } + + //删除文件 + fun delStoreImg(img: com.cheng.bole.bean.UploadImgEntity, delBack: (() -> Unit)?) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.delUserFile(img.id) + if (response.status) { + delBack?.invoke() + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + L.d(it) + }) + } + + fun upload(context: Context, list: ArrayList) { + showDialog() + list.forEachIndexed { index, photo -> + if (index == 0) { + uploadImg(context, photo) { + list.remove(photo) + if (list.size > 0) { + upload(context, list) + } else { + uploadSuccessLiveData.postValue(imgList) + dismissDialog() + } + } + } + } + } + + private fun uploadImg(context: Context, item: Photo, clickFun: () -> Unit) { + BitmapUtils.compressImg(context, item.path) { + launchOnUiTryCatch({ + val requestFile = RequestBody.create("multipart/form-data".toMediaTypeOrNull(), it) + val filePart = MultipartBody.Part.createFormData("file", it.getName(), requestFile) + val response = ApiFactory.apiService.upload(filePart, "feedback") + if (response.status) { + imgList.add(response.data) + clickFun.invoke() + } else { + toast(response.message, true) + dismissDialog() + } + }, { + dismissDialog() + L.d(it) + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/settings/SettingsFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/settings/SettingsFragment.kt new file mode 100644 index 0000000..eb19c07 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/settings/SettingsFragment.kt @@ -0,0 +1,141 @@ +package com.cheng.bole.ui.fragment.mine.settings + +import android.graphics.Color +import androidx.lifecycle.lifecycleScope +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentSettingsBinding +import com.cheng.bole.event.LoginSuccessEvent +import com.cheng.bole.event.LogoutSuccessEvent +import com.cheng.bole.manager.DialogEnum +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.activity.LoginActivity +import com.cheng.bole.ui.activity.PublicActivity +import com.cheng.bole.ui.dialog.TipDialog +import com.cheng.bole.ui.fragment.mine.about.AboutFragment +import com.cheng.bole.ui.fragment.mine.account.AccountManageFragment +import com.cheng.bole.ui.fragment.mine.account.BindAccountFragment +import com.cheng.bole.utils.DataCacheUtils +import com.example.base.common.RxBus +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.extensions.visible +import com.example.base.ui.BaseFragment +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.anko.backgroundColor + +/** + * 设置 + */ +class SettingsFragment : BaseFragment() { + + override fun initView() { + super.initView() + mTitleBar?.backgroundColor = Color.WHITE + + if (LoginManager.isLogin()) { + binding.tvLogOff.visible() + binding.layoutBottom.visible() + } else { + binding.tvLogOff.gone() + binding.layoutBottom.gone() + } + } + + override fun initData() { + super.initData() + } + + override fun onResume() { + super.onResume() + binding.tvCacheSize.text = com.cheng.bole.utils.FileUtils.toFileSize(DataCacheUtils.getCacheSize()) + } + + override fun initListener() { + super.initListener() + binding.tvAbout.onClick { + PublicActivity.start(requireContext(), AboutFragment::class.java) + EventReportManager.eventReport(EventConstants.JUMP_TO_ABOUT_US, "", "") + } + binding.tvLogOff.onClick { + val f = TipDialog.newInstance("提示", "为了您的账户安全,注销账户后将会永久清除与该账户相关的所有信息,服务器不再保存") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + mViewModel.userDestroy() + } + } + f.show(childFragmentManager, "") + } + binding.tvBindAccount.onClick { + PublicActivity.start(requireContext(), BindAccountFragment::class.java) + EventReportManager.eventReport(EventConstants.JUMP_TO_ACCOUNT_BIND, "", "") + } + binding.tvManageAccount.onClick { + PublicActivity.start(requireContext(), AccountManageFragment::class.java) + EventReportManager.eventReport(EventConstants.JUMP_TO_ACCOUNT_MANAGE, "", "") + } + binding.layoutCache.onClick { + clearCache() + binding.tvCacheSize.text = com.cheng.bole.utils.FileUtils.toFileSize(DataCacheUtils.getCacheSize()) + EventReportManager.eventReport(EventConstants.CLEAR_CACHE, "mine", "") + } + binding.btnLogout.onClick { + val f = TipDialog.newInstance("提示", "是否退出登录?") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + mViewModel.logout() + } + } + f.show(childFragmentManager, "") + } + } + + override fun initObserve() { + super.initObserve() + mViewModel.logoutLiveData.observe(this) { + EventReportManager.eventReport(EventConstants.EXIT_LOGIN, "", "") + logout() + } + mViewModel.destroyLiveData.observe(this) { + EventReportManager.eventReport(EventConstants.CANCEL_ACCOUNT, "", "") + logout() + } + + val loginSuccessEvent = RxBus.defaultInstance.toObservable(LoginSuccessEvent::class.java).subscribe { + requireActivity().finish() + } + addDisposable(loginSuccessEvent) + } + + private fun clearCache() { + val f = TipDialog.newInstance("温馨提示", "您确定要清除缓存吗?") + f.setOnSelectListener { + if (it == DialogEnum.CLICK_OK) { + mViewModel.showDialog() + lifecycleScope.launch(Dispatchers.IO) { + val cacheSize = DataCacheUtils.getCacheSize() + DataCacheUtils.cleanInternalCache() + withContext(Dispatchers.Main) { + mViewModel.dismissDialog() + toast("已清除缓存,释放空间 ${com.cheng.bole.utils.FileUtils.toFileSize(cacheSize)}") + binding.tvCacheSize.text = com.cheng.bole.utils.FileUtils.toFileSize(DataCacheUtils.getCacheSize()) + } + } + } + } + f.show(childFragmentManager, TipDialog::class.java.simpleName) + } + + private fun logout() { + UserConfigManager.saveLastUserinfo(UserConfigManager.userInfo) + LoginManager.logout() + RxBus.defaultInstance.post(LogoutSuccessEvent()) + LoginActivity.start(requireActivity()) + requireActivity().finish() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/settings/SettingsViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/settings/SettingsViewModel.kt new file mode 100644 index 0000000..e262167 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/settings/SettingsViewModel.kt @@ -0,0 +1,46 @@ +package com.cheng.bole.ui.fragment.mine.settings + +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.cheng.bole.net.ApiFactory + +class SettingsViewModel : BaseViewModel() { + val logoutLiveData = MutableLiveData() + val destroyLiveData = MutableLiveData() + + fun logout() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.logout() + if (response.status) { + logoutLiveData.postValue(Any()) + } else { + toast(response.message, true) + } + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun userDestroy() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.userDestroy() + if (response.status) { + destroyLiveData.postValue(response.data) + } else { + toast(response.message, true) + } + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/user/UserSettingFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/user/UserSettingFragment.kt new file mode 100644 index 0000000..212721a --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/user/UserSettingFragment.kt @@ -0,0 +1,141 @@ +package com.cheng.bole.ui.fragment.mine.user + +import android.annotation.SuppressLint +import android.text.TextUtils +import coil.load +import coil.transform.CircleCropTransformation +import com.example.base.common.RxBus +import com.example.base.extensions.getColor +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.ui.BaseFragment +import com.google.gson.JsonObject +import com.huantansheng.easyphotos.EasyPhotos +import com.huantansheng.easyphotos.callback.SelectCallback +import com.huantansheng.easyphotos.models.album.entity.Photo +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import com.cheng.bole.databinding.FragmentUserSettingBinding +import com.cheng.bole.event.UserInfoEvent +import com.cheng.bole.ui.dialog.EditTextDialog +import com.cheng.bole.utils.PermissionUtils +import okhttp3.RequestBody.Companion.toRequestBody + +class UserSettingFragment : BaseFragment() { + private var userInfo: com.cheng.bole.bean.UserEntity? = null + private var selectedPhoto: Photo? = null + private var selectedImg: com.cheng.bole.bean.UploadImgEntity? = null + + override fun initView() { + super.initView() + mTitleBar?.background = null + } + + override fun initData() { + super.initData() + mViewModel.userInfo() + } + + override fun initListener() { + super.initListener() + binding.ivAvatar.onClick { + PermissionUtils.checkPhotoPermission(requireActivity(), childFragmentManager, true) { + if (it) selectPhoto() + } + } + binding.tvUsername.onClick { + val f = EditTextDialog.newInstance("编辑昵称", binding.tvUsername.text.toString()) + f.setOnTextListener { + if (it.length > 20) { + toast("昵称不能超过20个字") + return@setOnTextListener + } + binding.tvUsername.text = it + } + f.show(childFragmentManager, EditTextDialog::class.java.simpleName) + } + binding.tvSave.onClick { + val username = binding.tvUsername.text.toString() + if (TextUtils.isEmpty(username)) { + toast("请输入您的昵称") + return@onClick + } + if (selectedPhoto != null) { + mViewModel.uploadImg(requireContext(), selectedPhoto!!) + } else { + commit() + } + } + } + + private fun commit() { + val username = binding.tvUsername.text.toString() + if (TextUtils.isEmpty(username)) { + toast("请输入您的昵称") + return + } + val jsonObject = JsonObject() + if (selectedImg != null) { + jsonObject.addProperty("avater", selectedImg!!.url) + } + jsonObject.addProperty("name", username) + mViewModel.updateUserinfo(jsonObject.toString().toRequestBody()) + } + + override fun initObserve() { + super.initObserve() + mViewModel.userInfoLiveData.observe(this) { + userInfo = it + setUserInfo() + } + mViewModel.uploadUserinfoLiveData.observe(this) { + toast("修改成功") + RxBus.defaultInstance.post(UserInfoEvent()) + requireActivity().finish() + } + mViewModel.uploadImgLiveData.observe(this) { + selectedImg = it + commit() + } + val userinfoEvent = RxBus.defaultInstance.toObservable(UserInfoEvent::class.java).subscribe { + mViewModel.userInfo() + } + addDisposable(userinfoEvent) + } + + @SuppressLint("SetTextI18n") + private fun setUserInfo() { + binding.ivAvatar.load(userInfo?.avater) { + transformations(CircleCropTransformation()) + placeholder(R.mipmap.ic_default_avatar) + error(R.mipmap.ic_default_avatar) + } + if (userInfo?.vip == "1") { + binding.tvVip.text = "非会员" + binding.tvVip.setTextColor(getColor(R.color.color_999999)) + } else if (userInfo?.vip == "2") { + binding.tvVip.text = "${userInfo?.vip_expire} 到期" + binding.tvVip.setTextColor(getColor(R.color.color_212226)) + } else if (userInfo?.vip == "3") { + binding.tvVip.text = "终生会员" + binding.tvVip.setTextColor(getColor(R.color.color_212226)) + } + binding.tvUsername.text = userInfo?.name + } + + private fun selectPhoto() { + EasyPhotos.createAlbum(this, true, false, com.cheng.bole.utils.GlideEngine.getInstance()) + .setFileProviderAuthority(Constants.AppFilter) + .setCount(1) + .start(object : SelectCallback() { + override fun onResult(photos: ArrayList, isOriginal: Boolean) { + if (photos.isNotEmpty()) { + selectedPhoto = photos[0] + } + } + + override fun onCancel() { + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/user/UserSettingViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/user/UserSettingViewModel.kt new file mode 100644 index 0000000..95c13f1 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/user/UserSettingViewModel.kt @@ -0,0 +1,81 @@ +package com.cheng.bole.ui.fragment.mine.user + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.huantansheng.easyphotos.models.album.entity.Photo +import com.cheng.bole.net.ApiFactory +import com.cheng.bole.utils.BitmapUtils +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody + +class UserSettingViewModel : BaseViewModel() { + val userInfoLiveData = MutableLiveData() + val uploadImgLiveData = MutableLiveData() + val uploadUserinfoLiveData = MutableLiveData() + val deleteImageLiveData = MutableLiveData() + + fun userInfo() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.userInfo() + if (response.status) { + userInfoLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun uploadImg(context: Context, photo: Photo) { + showDialog() + BitmapUtils.compressImg(context, photo.path) { + launchOnUiTryCatch({ + val requestFile = RequestBody.create("multipart/form-data".toMediaTypeOrNull(), it) + val filePart = MultipartBody.Part.createFormData("file", it.getName(), requestFile) + val response = ApiFactory.apiService.upload(filePart, "avatar") + if (response.status) { + uploadImgLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + } + + fun updateUserinfo(requestBody: RequestBody) { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.changUser(requestBody) + if (response.status) { + uploadUserinfoLiveData.postValue(Any()) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun deleteImg(id: String) { + launchOnUiTryCatch({ + val response = ApiFactory.apiService.delUserFile(id) + if (response.status) { + deleteImageLiveData.postValue(id) + } + }, { + setError(it) + L.d(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipFragment.kt new file mode 100644 index 0000000..d9182df --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipFragment.kt @@ -0,0 +1,364 @@ +package com.cheng.bole.ui.fragment.mine.vip + +import android.annotation.SuppressLint +import android.text.TextUtils +import android.view.View +import android.window.OnBackInvokedDispatcher +import androidx.activity.addCallback +import androidx.core.content.ContextCompat +import androidx.core.os.BuildCompat +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.cheng.bole.R +import com.cheng.bole.common.EventConstants +import com.cheng.bole.databinding.FragmentVipBinding +import com.cheng.bole.event.PayStatusEnum +import com.cheng.bole.event.PayStatusEvent +import com.cheng.bole.event.VipPaySuccessEvent +import com.cheng.bole.manager.EventReportManager +import com.cheng.bole.manager.LoginManager +import com.cheng.bole.manager.UserConfigManager +import com.cheng.bole.ui.activity.MainActivity +import com.cheng.bole.ui.dialog.PayTipDialog +import com.cheng.bole.utils.UrlHelper +import com.cheng.bole.utils.pay.PayUtils +import com.bytedance.ads.convert.event.ConvertReportHelper +import com.example.base.common.RxBus +import com.example.base.decoration.FirstItemOffsetDecoration +import com.example.base.decoration.GridSpaceItemDecoration +import com.example.base.decoration.SpacesItemDecoration +import com.example.base.extensions.getColor +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.extensions.visible +import com.example.base.ui.BaseFragment +import com.example.base.utils.DensityUtils +import com.example.base.utils.SpanUtils +import com.google.gson.Gson +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import com.cheng.bole.common.Constants +import org.jetbrains.anko.sdk27.listeners.onCheckedChange +import org.jetbrains.anko.startActivity +import java.text.DecimalFormat + +class VipFragment : BaseFragment() { + private val origin by lazy { arguments?.getString("origin") ?: "center" } + private val isGuide by lazy { origin == "guide" } + + private lateinit var api: IWXAPI + private var isAgree = false + private var payType = 0 //0微信支付 1支付宝支付 + private var totalPrice = 0f + + private val mealAdapter by lazy { VipMealAdapter() } + private val tipAdapter by lazy { VipTipAdapter() } + + private var userInfo: com.cheng.bole.bean.UserEntity? = null + private var vipGoodsEntity: com.cheng.bole.bean.VipGoodsEntity? = null + private var orderEntity: com.cheng.bole.bean.OrderPayEntity? = null + + override fun initView() { + super.initView() + mTitleBar?.background = null + setBackColor(R.color.white) + + binding.tvPrice.typeface = Constants.dDIN_PRO_M + binding.tvTipTitle.typeface = Constants.almmsht + + binding.rvMeal.adapter = mealAdapter + + binding.rvTip.adapter = tipAdapter + tipAdapter.setList(com.cheng.bole.bean.VipTipItemEntity.getVipTipList()) + + binding.cbAgree.visibility = if (UserConfigManager.isPayAgreementEnable()) View.VISIBLE else View.GONE + binding.tvAgree.visibility = if (UserConfigManager.isPayAgreementEnable()) View.VISIBLE else View.GONE + } + + override fun initData() { + super.initData() + api = WXAPIFactory.createWXAPI(requireContext(), Constants.WechatAppId) + + mViewModel.userInfo() + mViewModel.getGoodsList() + } + + @SuppressLint("NotifyDataSetChanged", "SetTextI18n") + override fun initListener() { + super.initListener() + setBackPressed() + mTitleBar?.setNavigationOnClickListener { + if (isGuide) { + requireActivity().startActivity() + EventReportManager.eventReport(EventConstants.GUIDE_SKIP, "icon", pageDuration()) + } else { + requireActivity().finish() + } + } + + mealAdapter.setOnItemClickListener { _, _, i -> + val item = mealAdapter.getItem(i) + if (item.goods_id == vipGoodsEntity?.goods_id) return@setOnItemClickListener + mealAdapter.data.forEach { it.checked = it.goods_id == item.goods_id } + mealAdapter.notifyDataSetChanged() + EventReportManager.eventReport(EventConstants.GOODS_SELECT, "${origin}:${item.goods_name}", Gson().toJson(item)) + + vipGoodsEntity = item + setPrice(vipGoodsEntity!!.price.toFloat()) + releasePayType() + } + binding.tvWxPay.onClick { + payType = 0 + checkPayType() + EventReportManager.eventReport(EventConstants.PAY_SELECT, "weixin", origin) + } + binding.tvAliPay.onClick { + payType = 1 + checkPayType() + EventReportManager.eventReport(EventConstants.PAY_SELECT, "alipay", origin) + } + binding.nextBtn.onClick { + if (vipGoodsEntity == null) return@onClick + if (!UserConfigManager.getNoLoginPay() && !LoginManager.isLogin()) { + toast("请登录后支付") + return@onClick + } + if (payType == 0 && !api.isWXAppInstalled) { + toast("您没有安装微信客户端,请先下载安装") + return@onClick + } + if (UserConfigManager.isPayAgreementEnable() && !isAgree) { + val f = PayTipDialog.newInstance(!TextUtils.isEmpty(vipGoodsEntity?.sign_value) && payType == 1) + f.setOnSelectListener { + binding.cbAgree.isChecked = true + if (this.payType == 0) { + mViewModel.payCreateOrder(vipGoodsEntity!!.goods_id, "weixin", origin) + } else { + mViewModel.payCreateOrder(vipGoodsEntity!!.goods_id, "alipay", origin) + } + } + f.show(childFragmentManager, PayTipDialog::class.java.simpleName) + } else { + if (this.payType == 0) { + mViewModel.payCreateOrder(vipGoodsEntity!!.goods_id, "weixin", origin) + } else { + mViewModel.payCreateOrder(vipGoodsEntity!!.goods_id, "alipay", origin) + } + } + EventReportManager.eventReport(EventConstants.PAY_PAY, if (payType == 0) "weixin" else "alipay", Gson().toJson(vipGoodsEntity)) + } + + binding.cbAgree.onCheckedChange { _, isChecked -> + isAgree = isChecked + } + + binding.tvAgree.onClick { + binding.cbAgree.isChecked = !binding.cbAgree.isChecked + } + } + + @SuppressLint("SetTextI18n") + override fun initObserve() { + super.initObserve() + mViewModel.userInfoLiveData.observe(this) { + userInfo = it + } + mViewModel.mealListLiveData.observe(this) { list -> + if (list.size <= 3) { + binding.rvMeal.layoutManager = GridLayoutManager(requireContext(), 3) + binding.rvMeal.setPadding(DensityUtils.dp2px(20f), 0, DensityUtils.dp2px(20f),0) + binding.rvMeal.addItemDecoration(GridSpaceItemDecoration(3, 0, DensityUtils.dp2px(10f))) + } else { + binding.rvMeal.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) + binding.rvMeal.addItemDecoration(SpacesItemDecoration(DensityUtils.dp2px(10f), RecyclerView.HORIZONTAL)) + binding.rvMeal.addItemDecoration(FirstItemOffsetDecoration(DensityUtils.dp2px(20f), FirstItemOffsetDecoration.left)) + } + + mealAdapter.setList(list) + + vipGoodsEntity = list.find { it.checked } + if (vipGoodsEntity == null && list.isNotEmpty()) { + vipGoodsEntity = list[0] + } + if (vipGoodsEntity != null) { + setPrice(vipGoodsEntity!!.price.toFloat()) + releasePayType() + } + initPrivacyTv() + } + mViewModel.createOrderLiveData.observe(this) { + orderEntity = it + if (this.payType == 0) { + PayUtils.toWXPay(requireActivity(), it) + } else { + PayUtils.toAliPay(requireActivity(), it.payParam, "") + } + } + + val payStatusDisposable = RxBus.defaultInstance.toObservable(PayStatusEvent::class.java).subscribe { + when (it.payStatus) { + PayStatusEnum.PAY_SUCCESS -> { + toast("支付成功") + sendBDReport(true) + EventReportManager.eventReport( + EventConstants.PAY_SUCCESS, + if (payType == 0) "weixin" else "alipay", + "{isGuide:$isGuide, orderId:${orderEntity?.orderId}, meal:${Gson().toJson(vipGoodsEntity)}}" + ) + if (isGuide) { + requireActivity().startActivity() + } else { + RxBus.defaultInstance.post(VipPaySuccessEvent()) + requireActivity().finish() + } + } + + PayStatusEnum.PAY_CANCEL -> { + toast("支付取消") + sendBDReport(false) + EventReportManager.eventReport(EventConstants.PAY_CANCEL, if (payType == 0) "weixin" else "alipay", "{isGuide:$isGuide, orderId:${orderEntity?.orderId}") + } + + else -> { + toast("支付取消") + sendBDReport(false) + EventReportManager.eventReport(if (payType == 0) EventConstants.ERROR_CLIENT_WXPAY_ERR else EventConstants.ERROR_CLIENT_ALIPAY_ERR, "{isGuide:$isGuide, orderId:${orderEntity?.orderId}", it.message) + } + } + } + addDisposable(payStatusDisposable) + } + + private fun setPrice(price: Float) { + totalPrice = if (price < 0) 0f else price + SpanUtils.with(binding.tvPrice) + .append("¥") + .setFontSize(13, true) + .append(DecimalFormat("0.##").format(totalPrice)) + .append(formatPricePeriod(vipGoodsEntity!!.value)) + .setFontSize(13, true) + .create() + SpanUtils.with(binding.tvOriginPrice) + .append("¥${DecimalFormat("0.##").format(vipGoodsEntity!!.origin_price.toFloat())}") + .setStrikethrough() + .create() + } + + private fun formatPricePeriod(value: String): String { + if (value == "#") { + return "/终生" + } else if (value.endsWith("m")) { + val monthStr = value.replace("m", "") + if (!TextUtils.isEmpty(monthStr)) { + val month = monthStr.toInt() + return if (month >= 12) { + "/${if (month / 12 > 1) "${month / 12}" else ""}年" + } else if (month % 3 == 0) { + "/${if (month / 3 > 1) "${month / 3}" else ""}季" + } else { + "/${if (month > 1) "$month" else ""}月" + } + } + } + return "" + } + + private fun checkPayType() { + if (payType == 0) { + val start = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_wx_pay) + val end = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_pay_true) + binding.tvWxPay.setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null) + + val start2 = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_ali_pay) + val end2 = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_pay_false) + binding.tvAliPay.setCompoundDrawablesRelativeWithIntrinsicBounds(start2, null, end2, null) + } else { + val start = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_ali_pay) + val end = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_pay_true) + binding.tvAliPay.setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null) + + val start2 = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_wx_pay) + val end2 = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_pay_false) + binding.tvWxPay.setCompoundDrawablesRelativeWithIntrinsicBounds(start2, null, end2, null) + } + initPrivacyTv() + } + + private fun releasePayType() { + val list = vipGoodsEntity?.pay_type?.split(",")?.map { it.trim() }?.toList() + if (list?.find { it == "alipay" } != null) { + binding.tvAliPay.visible() + } else { + binding.tvAliPay.gone() + } + if (list?.find { it == "weixin" } != null) { + binding.tvWxPay.visible() + } else { + binding.tvWxPay.gone() + } + + if (vipGoodsEntity?.pay_type!!.startsWith("alipay")) { + payType = 1 + } + if (vipGoodsEntity?.pay_type!!.startsWith("weixin")) { + payType = 0 + } + + checkPayType() + } + + private fun initPrivacyTv() { + val spanUtils = SpanUtils.with(binding.tvAgree) + .append("我已阅读并同意") + .append("《会员服务协议规则》") + .setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startUserAgreement(requireContext(), "会员服务协议规则") + } + if (!TextUtils.isEmpty(vipGoodsEntity?.sign_value) && payType == 1) { + spanUtils.append("和") + spanUtils.append("《自动续费服务规则》") + spanUtils.setClickSpan(getColor(R.color.color_466afd), false) { + UrlHelper.startRenewAgreement(requireContext()) + } + } + spanUtils.create() + } + + private fun sendBDReport(isSuccess: Boolean) { + ConvertReportHelper.onEventPurchase( + "member", + vipGoodsEntity!!.goods_name, + vipGoodsEntity!!.goods_id, + 1, + if (payType == 0) "weixin" else "alipay", + "¥", + isSuccess, + totalPrice.toInt() + ) + } + + @SuppressLint("UnsafeOptInUsageError") + private fun setBackPressed() { + if (BuildCompat.isAtLeastT()) { + requireActivity().onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) { + if (isGuide) { + requireActivity().startActivity() + EventReportManager.eventReport(EventConstants.GUIDE_SKIP, "back", pageDuration()) + } else { + requireActivity().finish() + } + } + } else { + requireActivity().onBackPressedDispatcher.addCallback(this) { + if (isGuide) { + requireActivity().startActivity() + EventReportManager.eventReport(EventConstants.GUIDE_SKIP, "back", pageDuration()) + } else { + requireActivity().finish() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipMealAdapter.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipMealAdapter.kt new file mode 100644 index 0000000..3ed2659 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipMealAdapter.kt @@ -0,0 +1,81 @@ +package com.cheng.bole.ui.fragment.mine.vip + +import android.annotation.SuppressLint +import android.graphics.Color +import android.util.TypedValue +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.setPadding +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.extensions.getColor +import com.example.base.ui.list.LoadMoreAdapter +import com.example.base.utils.DensityUtils +import com.example.base.utils.SpanUtils +import com.cheng.bole.R +import com.cheng.bole.common.Constants +import java.text.DecimalFormat + +class VipMealAdapter : LoadMoreAdapter(R.layout.listitem_vip_meal) { + + @SuppressLint("NotifyDataSetChanged") + override fun convert(holder: BaseViewHolder, item: com.cheng.bole.bean.VipGoodsEntity) { + holder.setGone(R.id.tv_tag, true) + if (item.tips.isNotEmpty()) { + holder.setVisible(R.id.tv_tag, true) + holder.setText(R.id.tv_tag, item.tips) + } + + holder.setText(R.id.tv_goods_name, item.goods_name) + SpanUtils.with(holder.getView(R.id.tv_price)) + .append("¥") + .setFontSize(if (item.checked) 12 else 10, true) + .append(DecimalFormat("0.##").format(item.price.toFloat())) + .setFontSize(if (item.checked) 28 else 22, true) + .setTypeface(Constants.dDIN_PRO_M) + .create() + SpanUtils.with(holder.getView(R.id.tv_origin_price)) + .append("¥${item.origin_price}") + .setStrikethrough() + .create() + holder.setText(R.id.tv_save_price, "限时立省¥${DecimalFormat("0.##").format(item.origin_price.toFloat() - item.price.toFloat())}") + + holder.setBackgroundResource(R.id.tv_save_price, if (item.checked) R.drawable.shape_vip_meal_checked_bottom_bg else R.drawable.shape_vip_meal_default_bottom_bg) + holder.setBackgroundResource(R.id.layout_content, if (item.checked) R.drawable.shape_vip_meal_check_true else R.drawable.shape_vip_meal_check_false) + + holder.getView(R.id.tv_tag).setTextSize(TypedValue.COMPLEX_UNIT_SP, if (item.checked) 12f else 10f) + holder.getView(R.id.tv_goods_name).setTextSize(TypedValue.COMPLEX_UNIT_SP, if (item.checked) 15f else 12f) + holder.getView(R.id.tv_origin_price).setTextSize(TypedValue.COMPLEX_UNIT_SP, if (item.checked) 14f else 10f) + holder.getView(R.id.tv_save_price).setTextSize(TypedValue.COMPLEX_UNIT_SP, if (item.checked) 12f else 10f) + + if (item.checked) { + holder.itemView.setPadding(0) + holder.getView(R.id.layout_content).setPadding(0, DensityUtils.dp2px(9f),0, 0) + holder.getView(R.id.tv_tag).setPadding(DensityUtils.dp2px(6f), DensityUtils.dp2px(2f), DensityUtils.dp2px(6f), DensityUtils.dp2px(2f)) + holder.getView(R.id.tv_save_price).setPadding(0, DensityUtils.dp2px(6f),0, DensityUtils.dp2px(6f)) + + holder.setTextColor(R.id.tv_goods_name, getColor(R.color.color_54230c)) + holder.setTextColor(R.id.tv_price, getColor(R.color.color_f94747)) + holder.setTextColor(R.id.tv_origin_price, getColor(R.color.color_896451)) + holder.setTextColor(R.id.tv_save_price, getColor(R.color.color_54220b)) + } else { + holder.itemView.setPadding(DensityUtils.dp2px(5f), 0, DensityUtils.dp2px(5f),0) + holder.getView(R.id.tv_tag).setPadding(DensityUtils.dp2px(4f), DensityUtils.dp2px(1f), DensityUtils.dp2px(4f), DensityUtils.dp2px(1f)) + holder.getView(R.id.layout_content).setPadding(0, DensityUtils.dp2px(7f),0, 0) + holder.getView(R.id.tv_save_price).setPadding(0, DensityUtils.dp2px(4f),0, DensityUtils.dp2px(4f)) + + holder.setTextColor(R.id.tv_goods_name, Color.parseColor("#ABABAB")) + holder.setTextColor(R.id.tv_price, Color.parseColor("#EAEAEA")) + holder.setTextColor(R.id.tv_origin_price, Color.parseColor("#6B6B6B")) + holder.setTextColor(R.id.tv_save_price, getColor(R.color.color_80ffffff)) + } + + if (recyclerView.layoutManager is GridLayoutManager) { + holder.itemView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } else if (recyclerView.layoutManager is LinearLayoutManager) { + holder.itemView.layoutParams = + ViewGroup.LayoutParams(DensityUtils.dp2px(if (item.checked) 125f else 110f), ViewGroup.LayoutParams.MATCH_PARENT) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipTipAdapter.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipTipAdapter.kt new file mode 100644 index 0000000..4272a1e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipTipAdapter.kt @@ -0,0 +1,13 @@ +package com.cheng.bole.ui.fragment.mine.vip + +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.ui.list.LoadMoreAdapter +import com.cheng.bole.R + +class VipTipAdapter :LoadMoreAdapter(R.layout.listitem_vip_tip){ + + override fun convert(holder: BaseViewHolder, item: com.cheng.bole.bean.VipTipItemEntity) { + holder.setImageResource(R.id.iv_icon, item.icon) + holder.setText(R.id.tv_name, item.name) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipViewModel.kt new file mode 100644 index 0000000..01567ef --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/mine/vip/VipViewModel.kt @@ -0,0 +1,87 @@ +package com.cheng.bole.ui.fragment.mine.vip + +import androidx.lifecycle.MutableLiveData +import com.cheng.bole.net.ApiFactory +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.google.gson.JsonObject +import okhttp3.RequestBody.Companion.toRequestBody + +class VipViewModel : BaseViewModel() { + val userInfoLiveData = MutableLiveData() + val mealListLiveData = MutableLiveData>() + val couponListLiveData = MutableLiveData>() + val createOrderLiveData = MutableLiveData() + + fun userInfo() { + showDialog() + launchOnUiTryCatch({ + val response = ApiFactory.apiService.userInfo() + if (response.status) { + userInfoLiveData.postValue(response.data) + } else toast(response.message, true) + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } + + fun getGoodsList() { + launchOnUiTryCatch({ + val response = ApiFactory.apiService.getGoodsList() + if (response.status) { + mealListLiveData.postValue(response.data) + } + }, { + setError(it) + L.d(it) + }) + } + + fun couponList() { + val params = HashMap() + params["page"] = "1" + params["size"] = "50" + params["status"] = "1" + params["expire"] = "1" + launchOnUiTryCatch({ + val response = ApiFactory.apiService.couponList(params) + if (response.status) { + couponListLiveData.postValue(response.data.items) + } else toast(response.message, true) + },{ + setError(it) + L.d(it) + }) + } + + fun payCreateOrder(goos_id: String, pay_type: String, source: String) { + showDialog() + launchOnUiTryCatch({ + val json = JsonObject() + json.addProperty("goods_id", goos_id) + json.addProperty("pay_type", pay_type) + json.addProperty("source", source) + json.addProperty("pay_source", "app") + + val extra = JsonObject() +// extra.addProperty("choose_red_packet", true)//红包选中 + json.add("extra", extra) + L.d("TAG-->>$json") + val response = ApiFactory.apiService.payCreateOrder(json.toString().toRequestBody()) + if (response.status) { + createOrderLiveData.postValue(response.data) + } else { + toast(response.message, true) + } + dismissDialog() + }, { + dismissDialog() + setError(it) + L.d(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/photo/AddImageAdapter.kt b/app/src/main/java/com/cheng/bole/ui/fragment/photo/AddImageAdapter.kt new file mode 100644 index 0000000..d31e2a6 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/photo/AddImageAdapter.kt @@ -0,0 +1,118 @@ +package com.cheng.bole.ui.fragment.photo + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import com.example.base.utils.DensityUtils +import com.huantansheng.easyphotos.models.album.entity.Photo +import com.cheng.bole.R +import org.jetbrains.anko.find +import org.jetbrains.anko.sdk27.listeners.onClick + +class AddImageAdapter(private val context: Context, val data: MutableList) : RecyclerView.Adapter() { + + var selectMax = 10 + private val TYPE_ADD_IMAGE = 1001 + private val TYPE_PICTURE = 1002 + + companion object { + val TYPE_ADD = 10001 + val TYPE_RELOAD = 10002 + val TYPE_DELETE = 10003 + } + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val ivImg = view.find(R.id.iv_image) + val ivAddImg = view.find(R.id.ivAddImg) + val ivDeleteImg = view.find(R.id.iv_delete) + } + + override fun getItemCount(): Int { + return if (data.size < selectMax) { + data.size + 1 + } else { + data.size + } + } + + override fun getItemViewType(position: Int): Int { + return if (isAddItem(position)) { + TYPE_ADD_IMAGE + } else { + TYPE_PICTURE + } + } + + @SuppressLint("NotifyDataSetChanged") + fun removeAll() { + data.clear() + notifyDataSetChanged() + } + + /** + * 删除 + */ + fun remove(position: Int) { + try { + if (position != RecyclerView.NO_POSITION && data.size > position) { + data.removeAt(position) + notifyItemRemoved(position) + notifyItemRangeChanged(position, data.size) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * 创建ViewHolder + */ + override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { + val view = LayoutInflater.from(context).inflate(R.layout.listitem_add_img, viewGroup, false) + return ViewHolder(view) + } + + private fun isAddItem(position: Int): Boolean { + return position == data.size + } + + /** + * 设置值 + */ + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + if (viewHolder.itemViewType == TYPE_ADD_IMAGE) { + viewHolder.ivDeleteImg.visibility = View.GONE + viewHolder.ivAddImg.visibility = View.VISIBLE + } else { + viewHolder.ivAddImg.visibility = View.GONE + viewHolder.ivDeleteImg.visibility = View.VISIBLE + viewHolder.ivImg.load(data[position].path) { + transformations(RoundedCornersTransformation(DensityUtils.dp2px(8f).toFloat())) + } + } + viewHolder.ivAddImg.onClick { + onItemClick?.invoke(TYPE_ADD, position) + } + + viewHolder.ivDeleteImg.onClick { + onItemClick?.invoke(TYPE_DELETE, position) + } + viewHolder.ivImg.onClick { + if (viewHolder.itemViewType == TYPE_PICTURE) { + onItemClick?.invoke(TYPE_RELOAD, position) + } + } + } + + private var onItemClick: ((Int, Int) -> Unit)? = null + + fun setOnItemClick(onClick: ((Int, Int) -> Unit)?) { + onItemClick = onClick + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewAdapter.kt b/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewAdapter.kt new file mode 100644 index 0000000..6c2f10a --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewAdapter.kt @@ -0,0 +1,17 @@ +package com.cheng.bole.ui.fragment.photo + +import coil.load +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.github.chrisbanes.photoview.PhotoView +import com.cheng.bole.R +import com.cheng.bole.utils.BitmapUtils + +class PhotoViewAdapter : BaseQuickAdapter(R.layout.listitem_photo_view) { + + override fun convert(holder: BaseViewHolder, item: String) { + BitmapUtils.compressImg(context, item) { + holder.getView(R.id.iv_photo).load(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewFragment.kt new file mode 100644 index 0000000..2bd5c36 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewFragment.kt @@ -0,0 +1,38 @@ +package com.cheng.bole.ui.fragment.photo + +import android.graphics.Color +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.cheng.bole.R +import com.cheng.bole.databinding.FragmentPhotoViewBinding +import com.example.base.ui.BaseFragment + +class PhotoViewFragment : BaseFragment() { + private val photos by lazy { arguments?.getStringArrayList("photos") ?: arrayListOf() } + private val index by lazy { arguments?.getInt("index") ?: 0 } + private val mAdapter by lazy { PhotoViewAdapter() } + + override fun initView() { + super.initView() + mTitleBar?.background = null + mTitleBar?.setTitleTextColor(Color.WHITE) + setBackColor(R.color.white) + + binding.vpPhoto.adapter = mAdapter + } + + override fun initData() { + super.initData() + mAdapter.setList(photos) + binding.vpPhoto.setCurrentItem(index, false) + } + + override fun initListener() { + super.initListener() + binding.vpPhoto.registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + mTitleBar?.title = "${position + 1}/${mAdapter.data.size}" + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewViewModel.kt new file mode 100644 index 0000000..e50ca0c --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/photo/PhotoViewViewModel.kt @@ -0,0 +1,6 @@ +package com.cheng.bole.ui.fragment.photo + +import com.example.base.viewmodel.BaseViewModel + +class PhotoViewViewModel:BaseViewModel() { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/video/VideoPlayerFragment.kt b/app/src/main/java/com/cheng/bole/ui/fragment/video/VideoPlayerFragment.kt new file mode 100644 index 0000000..0383c13 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/video/VideoPlayerFragment.kt @@ -0,0 +1,180 @@ +package com.cheng.bole.ui.fragment.video + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.view.OrientationEventListener +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageButton +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import com.cheng.bole.R +import com.cheng.bole.databinding.FragmentVideoPlayerBinding +import com.example.base.extensions.getColor +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.toast +import com.example.base.extensions.visible +import com.example.base.ui.BaseFragment +import com.example.base.utils.L +import com.example.base.utils.ScreenUtils +import org.jetbrains.anko.find + + +class VideoPlayerFragment : BaseFragment() { + private val title by lazy { arguments?.getString("title") ?: "" } + private val url by lazy { arguments?.getString("url") ?: "" } + + private var orientationEventListener: OrientationEventListener? = null + private var currentOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + private var isFullScreen: Boolean = false + + private var listener: ((Int) -> Unit)? = null + + companion object { + fun newInstance(url: String, title: String): VideoPlayerFragment { + val args = Bundle() + args.putString("url", url) + args.putString("title", title) + val fragment = VideoPlayerFragment() + fragment.arguments = args + return fragment + } + } + + override fun initView() { + super.initView() + mTitleBar?.background = null + mTitleBar?.titleView?.text = title + mTitleBar?.titleView?.setTextColor(getColor(R.color.white)) + setBackColor(R.color.white) + + binding.playerView.player = ExoPlayer.Builder(requireContext()).build() + binding.playerView.player?.playWhenReady = true + binding.playerView.player?.repeatMode = Player.REPEAT_MODE_OFF + + orientationEventListener = object : OrientationEventListener(requireContext()) { + override fun onOrientationChanged(orientation: Int) { + if (!ScreenUtils.isScreenAutoRotate()) return + val activityOrientation = if (orientation >= 315 || orientation < 45) { // 0度,纵向向上 + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } else if (orientation in 45..134) { // 90度,横向向右 + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + } else if (orientation in 135..224) { // 180度,纵向向下 + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + } else { // 270度,横向向左 + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } + if (currentOrientation != activityOrientation) { + currentOrientation = activityOrientation + isFullScreen = + !(currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) +// updateFullScreenButton() + listener?.invoke(currentOrientation) + } + } + } + } + + override fun initData() { + super.initData() + binding.playerView.player?.setMediaItem(MediaItem.fromUri(url)) + binding.playerView.player?.prepare() + } + + override fun initListener() { + super.initListener() + binding.ivPlay.onClick { + binding.playerView.player?.prepare() + binding.playerView.player?.play() + } + binding.playerView.player?.addListener(object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + L.d("视频播放状态:$isPlaying") + binding.ivPlay.visibility = if (isPlaying) View.GONE else View.VISIBLE + } + + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + when (playbackState) { + Player.STATE_IDLE -> { + L.d("视频已暂停") + } + + Player.STATE_BUFFERING -> { + L.d("视频正在缓冲") + binding.progressbar.visible() + } + + Player.STATE_READY -> { + L.d("视频已准备好") + binding.progressbar.gone() + } + + Player.STATE_ENDED -> { + L.d("视频已结束") + binding.playerView.player?.seekTo(0) + binding.playerView.player?.pause() + } + } + } + + override fun onPlayerError(error: PlaybackException) { + super.onPlayerError(error) + binding.progressbar.gone() + toast("视频加载失败,错误:${error.errorCodeName}") + } + }) +// binding.playerView.setFullscreenButtonClickListener { +// isFullScreen = !isFullScreen +// updateFullScreenButton() +// listener?.invoke(if (isFullScreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) +// } +// binding.playerView.setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { +// mTitleBar?.visibility = it +// }) + } + + @SuppressLint("PrivateResource") + private fun updateFullScreenButton() { + val exoController = binding.playerView.find(androidx.media3.ui.R.id.exo_controller) + val exoFullscreen = exoController.find(androidx.media3.ui.R.id.exo_fullscreen) + if (isFullScreen) { + exoFullscreen.setImageResource(androidx.media3.ui.R.drawable.exo_styled_controls_fullscreen_exit) + exoFullscreen.setContentDescription(getString(androidx.media3.ui.R.string.exo_controls_fullscreen_exit_description)) + } else { + exoFullscreen.setImageResource(androidx.media3.ui.R.drawable.exo_styled_controls_fullscreen_enter) + exoFullscreen.setContentDescription(getString(androidx.media3.ui.R.string.exo_controls_fullscreen_enter_description)) + } + } + + fun setFullScreenChangedListener(listener: (Int) -> Unit) { + this.listener = listener + } + + override fun onResume() { + orientationEventListener?.enable() + // 恢复播放 + binding.playerView.onResume() + super.onResume() + } + + override fun onPause() { + orientationEventListener?.disable() + // 暂停播放 + binding.playerView.player?.pause() + binding.playerView.onPause() + super.onPause() + } + + override fun onDestroyView() { + // 释放播放器资源 + binding.playerView.player?.release() + binding.playerView.player = null + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/ui/fragment/video/VideoPlayerViewModel.kt b/app/src/main/java/com/cheng/bole/ui/fragment/video/VideoPlayerViewModel.kt new file mode 100644 index 0000000..68ad1c7 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/ui/fragment/video/VideoPlayerViewModel.kt @@ -0,0 +1,6 @@ +package com.cheng.bole.ui.fragment.video + +import com.example.base.viewmodel.BaseViewModel + +class VideoPlayerViewModel : BaseViewModel() { +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/AESpkcs7paddingUtil.kt b/app/src/main/java/com/cheng/bole/utils/AESpkcs7paddingUtil.kt new file mode 100644 index 0000000..6ad99b2 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/AESpkcs7paddingUtil.kt @@ -0,0 +1,32 @@ +package com.cheng.bole.utils + + +import android.util.Base64 +import com.example.base.utils.L +import io.github.fastaes.FastAES + +object AESpkcs7paddingUtil { + + /** + * 编码格式 + */ + const val ENCODING = "utf-8" + + /** + * AES解密 + * + * @param encryptStr 加密后的密文 + * @param key 密钥 + * @return 源字符串 + * @throws Exception + */ + @Throws(Exception::class) + fun decryptNormal(encryptStr: String?, key: String): String { + val sourceBytes = Base64.decode(encryptStr, Base64.NO_WRAP) + L.e("当前返回的字符串为" + sourceBytes.size) + val keyBytes = key.toByteArray(charset(ENCODING)) + val plain: ByteArray = FastAES.decrypt(sourceBytes, keyBytes, key.substring(0, 16).toByteArray(charset(ENCODING))) + return String(plain) + } +} + diff --git a/app/src/main/java/com/cheng/bole/utils/AnimatorUtils.kt b/app/src/main/java/com/cheng/bole/utils/AnimatorUtils.kt new file mode 100644 index 0000000..9d939f3 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/AnimatorUtils.kt @@ -0,0 +1,100 @@ +package com.cheng.bole.utils + +import android.animation.Animator +import android.animation.ValueAnimator +import android.app.Activity +import android.graphics.Path +import android.graphics.PathMeasure +import android.view.View +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.animation.addListener + + +object AnimatorUtils { + + fun doCartAnimator( + activity: Activity?, imageView: ImageView?, + cartView: View?, parentView: ViewGroup?, + listener: OnAnimatorListener? + ) { + //第一步: + //创造出执行动画的主题---imageView + //代码new一个imageView,图片资源是上面的imageView的图片 + // (这个图片就是执行动画的图片,从开始位置出发,经过一个抛物线(贝塞尔曲线),移动到购物车里) + if (activity == null || imageView == null || cartView == null || parentView == null) return + val goods = ImageView(activity) + goods.setPadding(1, 1, 1, 1) + //图片切割方式 + goods.setScaleType(ImageView.ScaleType.CENTER_CROP) + //获取图片资源 + goods.setImageDrawable(imageView.getDrawable()) + //设置RelativeLayout容器(这里必须设置RelativeLayout 设置LinearLayout动画会失效) + val params = ConstraintLayout.LayoutParams(100, 100) + //把动画view添加到动画层 + parentView.addView(goods, params) + + //第二步: + //得到父布局的起始点坐标(用于辅助计算动画开始/结束时的点的坐标) + val parentLocation = IntArray(2) + //获取购买按钮的在屏幕的X、Y坐标(这也是动画开始的坐标) + parentView.getLocationInWindow(parentLocation) + val startLoc = IntArray(2) + //获取商品图片在屏幕中的位置 + imageView.getLocationInWindow(startLoc) + //得到购物车图片的坐标(用于计算动画结束后的坐标) + val endLoc = IntArray(2) + cartView.getLocationInWindow(endLoc) + + //第三步: + //正式开始计算动画开始/结束的坐标 + //开始掉落的商品的起始点:商品起始点-父布局起始点+该商品图片的一半 + val startX = (startLoc[0] - parentLocation[0] + imageView.width / 2).toFloat() // 动画开始的X坐标 + val startY = (startLoc[1] - parentLocation[1] + imageView.height / 2).toFloat() //动画开始的Y坐标 + + //商品掉落后的终点坐标:购物车起始点-父布局起始点+购物车图片的1/5 + val toX = (endLoc[0] - parentLocation[0] + cartView.width / 5).toFloat() + val toY = (endLoc[1] - parentLocation[1]).toFloat() + + //第四步: + //计算中间动画的插值坐标,绘制贝塞尔曲线 + val path = Path() + //移动到起始点(贝塞尔曲线的起点) + path.moveTo(startX, startY) + //第一个起始坐标越大,贝塞尔曲线的横向距离就会越大 toX,toY:为终点 + path.quadTo((startX + toX) / 2, startY, toX, toY) + val pathMeasure = PathMeasure(path, false) + //实现动画具体博客可参考 鸿洋大神的https://blog.csdn.net/lmj623565791/article/details/38067475 + val valueAnimator = ValueAnimator.ofFloat(0f, pathMeasure.length) + //设置动画时间 + valueAnimator.setDuration(700) + //LinearInterpolator补间器:它的主要作用是可以控制动画的变化速率,比如去实现一种非线性运动的动画效果 + //具体可参考郭霖大神的:https://blog.csdn.net/guolin_blog/article/details/44171115 + valueAnimator.interpolator = LinearInterpolator() + valueAnimator.addUpdateListener { animation -> + //更新动画 + val value = animation.getAnimatedValue() as Float + val currentPosition = FloatArray(2) + pathMeasure.getPosTan(value, currentPosition, null) + goods.translationX = currentPosition[0] //改变了ImageView的X位置 + goods.translationY = currentPosition[1] //改变了ImageView的Y位置 + } + + //第五步: + //开始执行动画 + valueAnimator.start() + + //第六步: + //对动画添加监听 + valueAnimator.addListener(onEnd = { + parentView.removeView(goods) + listener?.onAnimationEnd(it) + }) + } + + interface OnAnimatorListener { + fun onAnimationEnd(animator: Animator?) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/BitmapUtils.kt b/app/src/main/java/com/cheng/bole/utils/BitmapUtils.kt new file mode 100644 index 0000000..6c16eba --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/BitmapUtils.kt @@ -0,0 +1,173 @@ +package com.cheng.bole.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Matrix +import android.util.Base64 +import android.util.LruCache +import android.view.View +import com.example.base.extensions.toast +import com.example.base.utils.ScreenUtils +import com.jakewharton.disklrucache.DiskLruCache +import top.zibin.luban.Luban +import top.zibin.luban.OnCompressListener +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + + +object BitmapUtils { + private val lruCache by lazy { + object : LruCache(20 * 1024 * 1024) { + override fun sizeOf(key: String?, value: Bitmap?): Int { + return value?.allocationByteCount ?: 0 + } + } + } + + private val diskLruCache by lazy { + DiskLruCache.open(com.cheng.bole.utils.FileUtils.getInstance().cacheDir, 1, 1, 100L * 1024 * 1024) + } + + fun putCacheBitmap(key: String, bitmap: Bitmap, diskCache: Boolean = true) { + lruCache.put(key, bitmap) + if (diskCache) { + val editor = diskLruCache.edit(key) + if (editor != null) { + val outputStream = editor.newOutputStream(0) + val success = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + if (success) { + editor.commit() + } else { + editor.abort() + } + } + } + } + + fun getCacheBitmap(key: String): Bitmap? { + var cacheBitmap: Bitmap? + cacheBitmap = lruCache.get(key) + if (cacheBitmap == null) { + try { + val snapShot = diskLruCache.get(key) // 通过key获取Snapshot对象 + if (snapShot != null) { + val inputStream = snapShot.getInputStream(0) // 通过Snapshot对象获取缓存文件的输入流 + cacheBitmap = BitmapFactory.decodeStream(inputStream) + } + } catch (e: IOException) { + e.printStackTrace() + } + } + return cacheBitmap + } + + fun clearMemoryCache() { + lruCache.trimToSize(20 * 1024 * 1024) + } + + fun Base64ToBitmap(str: String): Bitmap { + val decodeBytes = Base64.decode(str, Base64.DEFAULT) + return BitmapFactory.decodeByteArray(decodeBytes, 0, decodeBytes.size) + } + + fun compressImg(context: Context, str: String, compressBack: (File) -> Unit) { + Luban.with(context) + .load(str) + .ignoreBy(100) + .setTargetDir(com.cheng.bole.utils.FileUtils.getInstance().cachE_DIR) + .setCompressListener(object : OnCompressListener { + override fun onStart() { + + } + + override fun onSuccess(file: File?) { + if (file != null) + compressBack.invoke(file) + } + + override fun onError(e: Throwable?) { + toast("压缩失败") + } + + }).launch() + } + + /** + * 长图拼接 + */ + fun mergeLongBitmap(bitmaps: List, maxWidth: Int? = null): Bitmap { + val longBitmap: Bitmap + val totalW = maxWidth ?: ScreenUtils.getScreenWidth() + var totalH = 0 + val newBitmaps = mutableListOf() + bitmaps.forEach { + val height = it.getHeight().toFloat() * totalW / it.getWidth().toFloat() + newBitmaps.add(resizeBitmap(it, totalW, height.toInt())) + totalH += height.toInt() + } + longBitmap = Bitmap.createBitmap(totalW, totalH, Bitmap.Config.ARGB_8888) + val canvas = Canvas(longBitmap) + var top = 0f + newBitmaps.forEach { + canvas.drawBitmap(it, 0f, top, null) + top += it.height + } + return longBitmap + } + + /** + * 缩放图片 + */ + fun resizeBitmap(bitmap: Bitmap, newWidth: Int, newHeight: Int): Bitmap { + val scaleWidth = newWidth.toFloat() / bitmap.getWidth() + val scaleHeight = newHeight.toFloat() / bitmap.getHeight() + val matrix = Matrix() + matrix.postScale(scaleWidth, scaleHeight) + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false) + } + + /** + * 保存Bitmap到文件 + */ + fun saveBitmapToFile(bitmap: Bitmap, isPng: Boolean = false): File { + val file = File(com.cheng.bole.utils.FileUtils.getInstance().cacheDir, "jk_${System.currentTimeMillis()}.${if (isPng) "png" else "jpeg"}")//将要保存图片的路径 + try { + val bos = BufferedOutputStream(FileOutputStream(file)) + bitmap.compress(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 100, bos) + bos.flush() + bos.close() + } catch (e: IOException) { + e.printStackTrace() + } + return file + } + + /** + * view转bitmap + */ + fun getBitmapByView(context: Context, view: View): Bitmap { + val bitmap: Bitmap? + + // 步骤一:获取视图的宽和高 + val width: Int = view.right - view.left + val height: Int = view.bottom - view.top + + // 步骤二:生成bitmap + bitmap = Bitmap.createBitmap(context.resources.displayMetrics, width, height, Bitmap.Config.ARGB_8888) + bitmap.density = context.resources.displayMetrics.densityDpi + + // 步骤三:绘制canvas + val canvas = Canvas(bitmap) + view.computeScroll() + val restoreCount: Int = canvas.save() + canvas.translate((-view.scrollX).toFloat(), (-view.scrollY).toFloat()) + view.draw(canvas) + canvas.restoreToCount(restoreCount) + canvas.setBitmap(null) + return bitmap + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/ChannelUtils.kt b/app/src/main/java/com/cheng/bole/utils/ChannelUtils.kt new file mode 100644 index 0000000..33dd0bd --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/ChannelUtils.kt @@ -0,0 +1,58 @@ +package com.cheng.bole.utils + +import android.content.Context +import android.content.pm.PackageManager +import android.text.TextUtils +import com.bytedance.hume.readapk.HumeSDK +import com.cheng.bole.BuildConfig +import com.example.base.utils.MMKVUtils +import com.example.base.utils.Utils +import com.kwai.monitor.payload.TurboHelper +import com.tencent.vasdolly.helper.ChannelReaderUtil + + +object ChannelUtils { + + fun getChannel(): String { + if (BuildConfig.DEBUG) { + MMKVUtils.put("app_channel", "test") + } else { + if (TextUtils.isEmpty(MMKVUtils.getString("app_channel"))) { + MMKVUtils.put("app_channel", getChannelBy()) + } + } + return MMKVUtils.getString("app_channel") ?: "" + } + + private fun getChannelBy(): String? { + val kuaishou = TurboHelper.getChannel(Utils.getApp()) + if (!TextUtils.isEmpty(kuaishou)) { + return kuaishou + } + val tengxun = ChannelReaderUtil.getChannel(Utils.getApp()) + if (!TextUtils.isEmpty(tengxun)) { + return tengxun + } + val juliang = HumeSDK.getChannel(Utils.getApp()) + if (!TextUtils.isEmpty(juliang)) { + return juliang + } + return getChannel(Utils.getApp()) + } + + private fun getChannel(context: Context): String { + try { + val pm = context.packageManager + val appInfo = pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA) + val channel = appInfo.metaData.getString("UMENG_CHANNEL") + if (!TextUtils.isEmpty(channel)) { + return channel!! + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + +} + diff --git a/app/src/main/java/com/cheng/bole/utils/DataCacheUtils.kt b/app/src/main/java/com/cheng/bole/utils/DataCacheUtils.kt new file mode 100644 index 0000000..17b3411 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/DataCacheUtils.kt @@ -0,0 +1,72 @@ +package com.cheng.bole.utils + +import android.annotation.SuppressLint +import com.example.base.utils.Utils +import java.io.File + + +object DataCacheUtils { + + /** + * 获取缓存大小 + */ + fun getCacheSize(): Long { + return getDirectorySize(com.cheng.bole.utils.FileUtils.getInstance().cacheDir) + } + + /** + * 清除本应用内部缓存 + */ + fun cleanInternalCache() { + deleteFilesByDirectory(com.cheng.bole.utils.FileUtils.getInstance().cacheDir, false) + } + + /** + * 获取文件夹大小 + * + * @param directory + */ + private fun getDirectorySize(directory: File): Long { + var size: Long = 0 + val files = directory.listFiles() + if (files != null) { + for (file in files) { + size += if (file.isFile()) { + file.length() + } else { + getDirectorySize(file) + } + } + } + return size + } + + /** + * * 删除文件夹下的所有文件 + * + * @param directory + */ + private fun deleteFilesByDirectory(directory: File, deleteDir: Boolean = true): Boolean { + if (directory.isDirectory()) { + val children = directory.list() + if (children != null) { + for (i in children.indices) { + val success = deleteFilesByDirectory(File(directory, children[i])) + if (!success) { + return false + } + } + } + } + return !deleteDir || directory.delete() + } + + /** + * 清除本应用SharedPreference(/data/data/com.xxx.xxx/shared_prefs) + * + */ + @SuppressLint("SdCardPath") + fun cleanSharedPreference() { + deleteFilesByDirectory(File("/data/data/${Utils.getApp().packageName}/shared_prefs")) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/DateUtils.kt b/app/src/main/java/com/cheng/bole/utils/DateUtils.kt new file mode 100644 index 0000000..ffc27c5 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/DateUtils.kt @@ -0,0 +1,274 @@ +package com.cheng.bole.utils + +import android.text.TextUtils +import com.example.base.extensions.getYYYYMMDD +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +object DateUtils { + //获取今天的开始时间 + fun getDayStartTime(time: Long = System.currentTimeMillis()): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${time.getYYYYMMDD()} 00:00:00") + val calendar = Calendar.getInstance() + calendar.time = date!! + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + //获取今天的结束时间 + fun getDayEndTime(time: Long = System.currentTimeMillis()): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${time.getYYYYMMDD()} 23:59:59") + val calendar = Calendar.getInstance() + calendar.time = date!! + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + //获取昨天的开始时间 + fun getYesterdayStartTime(time: Long = System.currentTimeMillis()): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${time.getYYYYMMDD()} 00:00:00") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.add(Calendar.DAY_OF_MONTH, -1) + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + //获取昨天的结束时间 + fun getYesterdayEndTime(time: Long = System.currentTimeMillis()): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${time.getYYYYMMDD()} 23:59:59") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.add(Calendar.DAY_OF_MONTH, -1) + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + //获取本周第一天开始时间 + fun getWeekFirstDayTime(): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${System.currentTimeMillis().getYYYYMMDD()} 00:00:00") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.firstDayOfWeek = Calendar.MONDAY + calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY) +// L.d("weekFirst>>${simpleDateFormat.format(calendar.time)}") + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + //获取本周最后一天时间 + fun getWeekEndDayTime(): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${System.currentTimeMillis().getYYYYMMDD()} 23:59:59") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.firstDayOfWeek = Calendar.MONDAY + calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY) +// L.d("weekEnd>>${simpleDateFormat.format(calendar.time)}") + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + //获取本月第一天开始时间 + fun getMonthFirstDayTime(time: Long = System.currentTimeMillis()): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${time.getYYYYMMDD()} 00:00:00") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.set(Calendar.DAY_OF_MONTH, 1) + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + //获取本月最后一天时间 + fun getMonthEndDayTime(time: Long = System.currentTimeMillis()): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${time.getYYYYMMDD()} 23:59:59") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + /** + * @desc 获取以当前时间为结束时间的之前一周的开始时间 + */ + fun getBeforeWholeWeekTime(endTime: Long): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${endTime.getYYYYMMDD()} 00:00:00") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.add(Calendar.DAY_OF_MONTH, -6) + return calendar.timeInMillis / 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + /** + * @desc 获取以当前时间为开始时间的之后一周的每一天时间 + * @param startTime 开始时间 + * @param day 第几天 + */ + fun getDayOfWeekTimeMillisByStartTime(startTime: Long, day: Int): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${startTime.getYYYYMMDD()} 00:00:00") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.add(Calendar.DAY_OF_MONTH, day - 1) + return calendar.timeInMillis + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + /** + * @desc 获取以当前时间为结束时间的之前一周的每一天时间 + * @param endTime 结束时间 + * @param day 第几天 + */ + fun getDayOfWeekTimeMillisByEndTime(endTime: Long, day: Int): Long { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${endTime.getYYYYMMDD()} 23:59:59") + val calendar = Calendar.getInstance() + calendar.time = date!! + calendar.add(Calendar.DAY_OF_MONTH, day - 7) + return calendar.timeInMillis + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + /** + * @desc 获取当前月份的天数 + */ + fun getDayOfMonthCount(time: Long): Int { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + try { + val date = simpleDateFormat.parse("${time.getYYYYMMDD()} 00:00:00") + val calendar = Calendar.getInstance() + calendar.time = date!! + return calendar.getActualMaximum(Calendar.DAY_OF_MONTH) + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + /** + * 通过指定的年份和月份获取当月有多少天. + * + * @param year 年. + * @param month 月. + * @return 天数. + */ + fun getMonthDays(year: Int, month: Int): Int { + val calendar = Calendar.getInstance() + calendar.clear() + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + return calendar.getActualMaximum(Calendar.DAY_OF_MONTH) + } + + /** + * 获取指定年月的 1 号位于周几. + * @param year 年. + * @param month 月. + * @return 周. + */ + fun getFirstDayWeek(year: Int, month: Int): Int { + val calendar = Calendar.getInstance() + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, 1) + return calendar.get(Calendar.DAY_OF_WEEK) + } + + /** + * 获取指定年月的最后一天位于周几. + * + * @param year 年. + * @param month 月. + * @return 周. + */ + fun getLastDayWeek(year: Int, month: Int): Int { + val calendar = Calendar.getInstance(Locale.CHINA) + calendar.clear() + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) + return calendar.get(Calendar.DAY_OF_WEEK) + } + + fun getHour(): Int { + val calendar = Calendar.getInstance() + return calendar.get(Calendar.HOUR_OF_DAY) + } + + fun getMinute(): Int { + val calendar = Calendar.getInstance() + return calendar.get(Calendar.MINUTE) + } + + //日期转时间戳 + fun strDateToLong(str: String, pattern: String = "yyyy-MM-dd HH:mm"): Long { + if (TextUtils.isEmpty(str)) return 0 + + val simpleDateFormat = SimpleDateFormat(pattern, Locale.CHINA) + try { + val date = simpleDateFormat.parse(str) + val calendar = Calendar.getInstance() + calendar.time = date!! + return calendar.timeInMillis + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/DecimalDigitsInputFilter.kt b/app/src/main/java/com/cheng/bole/utils/DecimalDigitsInputFilter.kt new file mode 100644 index 0000000..6ec9caa --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/DecimalDigitsInputFilter.kt @@ -0,0 +1,35 @@ +package com.cheng.bole.utils + +import android.text.InputFilter +import android.text.Spanned + +class DecimalDigitsInputFilter(private val decimalDigits: Int) : InputFilter { + + override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? { + var dotPos = -1 + val len = dest.length + for (i in 0 until len) { + val c = dest[i] + if (c == '.' || c == ',') { + dotPos = i + break + } + } + if (source == "." && dstart == 0 && dend == 0) { + return "" + } + if (dotPos >= 0) { + if (source == "." || source == ",") { + return "" + } + if (dend <= dotPos) { + return null + } + if (len - dotPos > decimalDigits) { + return "" + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/DownLoadUtils.kt b/app/src/main/java/com/cheng/bole/utils/DownLoadUtils.kt new file mode 100644 index 0000000..c7ced8c --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/DownLoadUtils.kt @@ -0,0 +1,386 @@ +package com.cheng.bole.utils + +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.example.base.utils.L +import okhttp3.* +import okhttp3.Headers.Companion.toHeaders +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.io.* +import java.net.URL +import java.util.concurrent.CancellationException +import java.util.concurrent.TimeUnit + + +class DownLoadUtils private constructor() { + + companion object { + private const val TAG = "DownLoadUtils" + private val downLoadHttpUtils: DownLoadUtils by lazy { + DownLoadUtils() + } + + @JvmStatic + @Synchronized + fun getInstance(): DownLoadUtils { + return downLoadHttpUtils + } + } + + private var downloadSizeInfo = mutableMapOf() + private var cancelledList = mutableListOf() + + private var buffSize = 4096//建议设置为2048 + fun setBuffSize(size: Int): DownLoadUtils { + this.buffSize = size + return this + } + + private var interceptor: Interceptor? = null + fun setInterceptor(interceptor: Interceptor?): DownLoadUtils { + this.interceptor = interceptor + return this + } + + private var readTimeOut = 30L + fun setReadTImeOut(read: Long): DownLoadUtils { + this.readTimeOut = read + return this + } + + private var writeTimeout = 30L + fun setWriteTimeOut(write: Long): DownLoadUtils { + this.writeTimeout = write + return this + } + + private var connectTimeout = 30L + fun setConnectTimeOut(connect: Long): DownLoadUtils { + this.connectTimeout = connect + return this + } + + var filePath = "" + fun setFilePath(path: String): DownLoadUtils { + this.filePath = path + return this + } + + private var fileName = "" + fun setFileName(name: String): DownLoadUtils { + this.fileName = name + return this + } + + private var deleteWhenException = true + fun setDeleteWhenException(dele: Boolean): DownLoadUtils { + this.deleteWhenException = dele + return this + } + + private val requestBuilder: Request.Builder = Request.Builder() + private var urlBuilder: HttpUrl.Builder? = null + + private val okHttpClient = lazy { + OkHttpClient.Builder() + .readTimeout(readTimeOut, TimeUnit.SECONDS) + .writeTimeout(writeTimeout, TimeUnit.SECONDS) + .connectTimeout(connectTimeout, TimeUnit.SECONDS) + .addInterceptor(interceptor ?: LoggingInterceptor()) + .build() + } + private var actionGetTotal: (total: Long) -> Unit? = { _ -> } + private var actionProgress: (position: Long) -> Unit? = { _ -> } + private var actionSuccess: (file: File) -> Unit? = { _ -> } + private var actionCancel: () -> Unit? = { } + private var actionFail: (msg: String) -> Unit? = { _ -> } + + fun setActionCallBack( + actionGetTotal: (total: Long) -> Unit, + actionProgress: (position: Long) -> Unit, + actionSuccess: (file: File) -> Unit, + actionFail: (msg: String) -> Unit, + ): DownLoadUtils { + this.actionGetTotal = actionGetTotal + this.actionProgress = actionProgress + this.actionSuccess = actionSuccess + this.actionFail = actionFail + + return this + } + + private var downCallBack: DownCallBack? = null + fun setDownCallBack(callBack: DownCallBack): DownLoadUtils { + this.downCallBack = callBack + return this + } + + fun initUrl(url: String, params: Map?): DownLoadUtils { + urlBuilder = url.toHttpUrlOrNull()?.newBuilder() + if (params.isNullOrEmpty()) { + return this + } else { + for ((k, v) in params) { + checkName(k) + urlBuilder?.setQueryParameter(k, v) + } + } + return this + } + + fun addHeader(map: Map): DownLoadUtils { + requestBuilder.headers(map.toHeaders()) + return this + } + + private fun checkName(name: String) { + require(name.isNotEmpty()) { "name is empty" } + } + + fun down() { + if (urlBuilder == null) { + throw IllegalStateException("url not init") + } else { + doDown() + } + } + + private fun doDown() { + val startTime = System.currentTimeMillis() + Log.i(TAG, "startTime=$startTime") + val url = urlBuilder?.build() + if (url == null) { + doException("url is null") + return + } + if (isDowning(filePath + fileName)) { + return + } + cancelledList.remove(fileName) + + val currentLen = downloadSizeInfo[fileName] ?: 0L + if (isCanContinueDownload(url.toUrl(), currentLen)) { + requestBuilder.removeHeader("RANGE") + requestBuilder.addHeader("RANGE", "bytes=${currentLen}-") + } + val request = requestBuilder.url(url).tag(filePath + fileName).build() + + var `is`: InputStream? = null + var raf: RandomAccessFile? = null + var file: File? = null + try { + val response = okHttpClient.value.newCall(request).execute() + val total = response.body?.contentLength() ?: 0 + doGetTotal(currentLen + total) + + val buf = ByteArray(buffSize) + var len: Int + + file = if (fileName.isEmpty()) { + File(filePath) + } else { + val fileDir = File(filePath) + if (!fileDir.exists() || !fileDir.isDirectory) { + fileDir.mkdirs() + } + File(filePath, fileName) + } + + `is` = response.body?.byteStream() ?: FileInputStream("") + raf = RandomAccessFile(file, "rw") + raf.seek(currentLen) + + var sum: Long = currentLen + while (`is`.read(buf).also { len = it } != -1) { + if (isCancelled()) throw CancellationException() + raf.write(buf, 0, len) + sum += len.toLong() + downloadSizeInfo[fileName] = sum + Log.e(TAG, "download progress : $sum") + doProgress(sum) + } + Log.e(TAG, "download success") + + if (!file.exists()) { + throw FileNotFoundException("file create err,not exists") + } else { + doSuccess(file) + } + Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime)) + } catch (e: Exception) { + if (deleteWhenException && file?.exists() == true) file.delete() + Log.e(TAG, "download failed : " + e.message) + doException(e.message.toString()) + } finally { + try { + `is`?.close() + } catch (e: IOException) { + L.d(e) + } + try { + raf?.close() + } catch (e: IOException) { + L.d(e) + } + } + } + + private fun isCanContinueDownload(url: URL, start: Long): Boolean { //是否支持断点下载 + val requestBuilder = Request.Builder() + requestBuilder.addHeader("RANGE", "bytes=$start-") + requestBuilder.addHeader("Connection", "close") + val request: Request = requestBuilder.url(url).head().build() + val response: Response = okHttpClient.value.newCall(request).execute() + return if (response.isSuccessful) { + if (response.code == 206) { //支持 + response.close() + true + } else { //不支持 + response.close() + false + } + } else { + response.close() + false + } + } + + private fun isDowning(tag: String): Boolean { + for (call in okHttpClient.value.dispatcher.runningCalls()) { + if (call.request().tag() == tag) { + return true + } + } + return false + } + + private fun isCancelled(): Boolean { + return cancelledList.contains(fileName) + } + + fun cancel() { + cancel(filePath + fileName) + } + + fun cancel(tag: String) { + cancelledList.add(fileName) + if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) { + for (call in okHttpClient.value.dispatcher.runningCalls()) { + if (call.request().tag() == tag) { + call.cancel() + } + } + } + doCancel() + } + + private fun doException(err: String) { + runOnUiThread { + if (downCallBack == null) { + actionFail.invoke(err) + } else { + downCallBack?.fail(err) + } + mainThread = null + } + } + + private fun doSuccess(file: File?) { + runOnUiThread { + if (file == null) { + doException("file not exit") + } else { + if (downCallBack == null) { + actionSuccess.invoke(file) + } else { + downCallBack?.success(file) + } + } + mainThread = null + } + } + + private fun doGetTotal(total: Long) { + runOnUiThread { + if (downCallBack == null) { + actionGetTotal.invoke(total) + } else { + downCallBack?.total(total) + } + } + mainThread = null + } + + private fun doProgress(progress: Long) { + runOnUiThread { + if (downCallBack == null) { + actionProgress.invoke(progress) + } else { + downCallBack?.progress(progress) + } + } + } + + private fun doCancel() { + runOnUiThread { + if (downCallBack == null) { + actionCancel.invoke() + } else { + downCallBack?.cancel() + } + } + mainThread = null + } + + private var mainThread: Handler? = null + private fun runOnUiThread(action: () -> Unit) { + if (Looper.myLooper() != Looper.getMainLooper()) { // If we finish marking off of the main thread, we need to + // actually do it on the main thread to ensure correct ordering. + if (mainThread == null) { + mainThread = Handler(Looper.getMainLooper()) + } + mainThread?.post { + action.invoke() + } + return + } + action.invoke() + } + + + class LoggingInterceptor : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + val startTime = System.nanoTime() + Log.d( + TAG, String.format( + "Sending request %s on %s%n%s", + request.url, chain.connection(), request.headers + ) + ) + val response: Response = chain.proceed(request) + val endTime = System.nanoTime() + Log.d( + TAG, String.format( + "Received response for %s in %.1fms%n%s", + response.request.url, (endTime - startTime) / 1e6, response.headers + ) + ) + return response + } + + companion object { + private const val TAG = "LoggingInterceptor" + } + } + + interface DownCallBack { + fun success(file: File) + fun fail(str: String) + fun progress(position: Long) + fun total(total: Long) + fun cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/FileUtils.java b/app/src/main/java/com/cheng/bole/utils/FileUtils.java new file mode 100644 index 0000000..c4d0b86 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/FileUtils.java @@ -0,0 +1,349 @@ +package com.cheng.bole.utils; + + +import android.content.Context; +import android.content.Intent; +import android.os.Environment; +import android.os.StatFs; +import android.text.TextUtils; +import android.util.Log; + +import com.example.base.utils.Utils; + +import java.io.File; +import java.io.FileInputStream; +import java.text.DecimalFormat; + +/** + * @Date: 2021/8/5 17:08 + * @Des: + */ +public class FileUtils { + + private static FileUtils instance; + private static String packageName = "devcon"; + + // 文件缓存路径 + private String CACHE_DIR; + + // 下载目录 + private File downloadDir; + // 缓存目录 + private File cacheDir; + // 图片缓存目录 + private File cacheImageDir; + + private File cacheOriginalImageDir; + + private File cacheEditImageDir; + + private File cachePuzzleImageDir; + + public static FileUtils getInstance() { + if (instance == null) { + synchronized (FileUtils.class) { + if (instance == null) { + instance = new FileUtils(Utils.INSTANCE.getApp()); + } + } + } + return instance; + } + + public FileUtils() { + + } + + private FileUtils(Context context) { + CACHE_DIR = context.getExternalFilesDir(null).getAbsolutePath() + File.separator + packageName + File.separator; + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + cacheDir = new File(CACHE_DIR, "/cache"); + } else { + cacheDir = context.getCacheDir(); + } + if (!cacheDir.exists()) + cacheDir.mkdirs(); + cacheImageDir = new File(cacheDir, "/image/"); + if (!cacheImageDir.exists()) + cacheImageDir.mkdirs(); + + cacheOriginalImageDir = new File(cacheDir, "/originalImage/"); + if (!cacheOriginalImageDir.exists()) + cacheOriginalImageDir.mkdirs(); + + cacheEditImageDir = new File(cacheDir, "/cacheEditImage/"); + if (!cacheEditImageDir.exists()) + cacheEditImageDir.mkdirs(); + + cachePuzzleImageDir = new File(cacheDir, "/cachePuzzleImage/"); + if (!cachePuzzleImageDir.exists()) + cachePuzzleImageDir.mkdirs(); + + downloadDir = new File(cacheDir, "/download/"); + if (!downloadDir.exists()) + downloadDir.mkdirs(); + } + + public String getCACHE_DIR() { + return CACHE_DIR; + } + + /** + * 获取缓存目录 + * + * @return + * @Description: + */ + public File getCacheDir() { + return cacheDir; + } + + /** + * 获取下载目录 + * + * @return + */ + public File getCacheDownLoderDir() { + return downloadDir; + } + + /** + * 获取缓存图片目录 + * + * @return + * @Description: + */ + public File getCacheImageDir() { + return cacheImageDir; + } + + /** + * 水印照片编辑后的路径 + * + * @return + */ + public File getCacheEditImageDir() { + return cacheEditImageDir; + } + + public File getCacheOriginalImageDir() { + return cacheOriginalImageDir; + } + + /** + * 拼图路径 + * + * @return + */ + public File getCachePuzzleImageDir() { + return cachePuzzleImageDir; + } + + + /** + * 创建一个临时图片文件 + * + * @return + * @Description: + */ + public File newTempImageFile() { + File file = new File(cacheImageDir, System.currentTimeMillis() + ".jpg"); + return file; + } + + /** + * 判断是否安装SD卡 + * + * @return + */ + public static boolean checkSaveLocationExists() { + String sDCardStatus = Environment.getExternalStorageState(); + boolean status; + if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)) { + status = true; + } else + status = false; + return status; + } + + /** + * 删除指定目录下文件及目录 + * + * @param filePath + * @return + */ + public static void deleteFolderFile(String filePath) { + if (!TextUtils.isEmpty(filePath)) { + try { + File file = new File(filePath); + if (file.isDirectory()) {// 处理目录 + File files[] = file.listFiles(); + for (int i = 0; i < files.length; i++) { + deleteFolderFile(files[i].getAbsolutePath()); + } + } + if (!file.isDirectory()) {// 如果是文件,删除 + file.delete(); + } else {// 目录 + if (file.listFiles().length == 0) {// 目录下没有文件或者目录,删除 + file.delete(); + } + } + } catch (Exception e) { + Log.e("FileUtils", e.getMessage()); + } + } + } + + /** + * 删除文件 + * + * @param path + * @return + */ + public boolean deleteFile(String path) { + boolean status; + SecurityManager checker = new SecurityManager(); + + if (!path.equals("")) { + File newPath = new File(path); + checker.checkDelete(newPath.toString()); + if (newPath.isFile()) { + try { + + newPath.delete(); + status = true; + } catch (SecurityException se) { + Log.e("FileUtils", se.getMessage()); + status = false; + } + } else + status = false; + } else + status = false; + return status; + } + + /** + * 获取目录文件大小 + * + * @param dir + * @return + */ + public static long getDirSize(File dir) { + if (dir == null) { + return 0; + } + if (!dir.isDirectory()) { + return 0; + } + long dirSize = 0; + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isFile()) { + dirSize += file.length(); + } else if (file.isDirectory()) { + dirSize += file.length(); + dirSize += getDirSize(file); // 递归调用继续统计 + } + } + return dirSize; + } + + /** + * 获取指定文件大小 + */ + public static long getFileSize(File file) throws Exception { + long size = 0; + if (file.exists()) { + FileInputStream fis = null; + fis = new FileInputStream(file); + size = fis.available(); + } else { + + Log.e("获取文件大小", "文件不存在!"); + } + return size; + } + + /** + * 获取指定文件夹 + */ + public static long getFileSizes(File f) throws Exception { + long size = 0; + File flist[] = f.listFiles(); + for (int i = 0; i < flist.length; i++) { + if (flist[i].isDirectory()) { + size = size + getFileSizes(flist[i]); + } else { + size = size + getFileSize(flist[i]); + } + } + return size; + } + + /** + * 转换文件大小 + */ + public static String toFileSize(long fileS) { + DecimalFormat df = new DecimalFormat("#.00"); + String fileSizeString; + String wrongSize = "0M"; + if (fileS == 0) { + return wrongSize; + } + if (fileS < 1024) { + fileSizeString = df.format((double) fileS) + "B"; + } else if (fileS < 1048576) { + fileSizeString = df.format((double) fileS / 1024) + "K"; + } else if (fileS < 1073741824) { + fileSizeString = df.format((double) fileS / 1048576) + "M"; + } else { + fileSizeString = df.format((double) fileS / 1073741824) + "G"; + } + return fileSizeString; + } + + //判断文件是否存在 + public boolean fileIsExists(String strFile) { + try { + File f = new File(strFile); + if (!f.exists()) { + return false; + } + } catch (Exception e) { + return false; + } + return true; + } + + //外部存储空间 + public static long getExternalStorageSpace() { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + StatFs externalStatFs = new StatFs(Environment.getExternalStorageDirectory().getAbsolutePath()); + long externalBlockSize = externalStatFs.getBlockSizeLong(); + long externalTotalSize = externalStatFs.getBlockCountLong() * externalBlockSize; + long externalAvailableSize = externalStatFs.getAvailableBlocksLong() * externalBlockSize; + Log.d("FileUtils", "当前外部空间总大小----" + externalTotalSize); + Log.d("FileUtils", "当前外部空间可用大小----" + externalAvailableSize); + return externalAvailableSize; + } else { + return 0; + } + } + + /** + * 打开图库 + * @param context + */ + public void openGallery(Context context) { + try { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_APP_GALLERY); + context.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/GlideEngine.java b/app/src/main/java/com/cheng/bole/utils/GlideEngine.java new file mode 100644 index 0000000..2d38ce4 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/GlideEngine.java @@ -0,0 +1,96 @@ +package com.cheng.bole.utils; + +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.widget.ImageView; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.huantansheng.easyphotos.engine.ImageEngine; + + +public class GlideEngine implements ImageEngine { + + //单例 + private static GlideEngine instance = null; + //单例模式,私有构造方法 + private GlideEngine() { + } + //获取单例 + public static GlideEngine getInstance() { + if (null == instance) { + synchronized (GlideEngine.class) { + if (null == instance) { + instance = new GlideEngine(); + } + } + } + return instance; + } + + /** + * 加载图片到ImageView + * + * @param context 上下文 + * @param uri 图片路径Uri + * @param imageView 加载到的ImageView + */ + //安卓10推荐uri,并且path的方式不再可用 + @Override + public void loadPhoto(@NonNull Context context, @NonNull Uri uri, @NonNull ImageView imageView) { + Glide.with(context).load(uri).transition(withCrossFade()).into(imageView); + } + + /** + * 加载gif动图图片到ImageView,gif动图不动 + * + * @param context 上下文 + * @param gifUri gif动图路径Uri + * @param imageView 加载到的ImageView + *

+ * 备注:不支持动图显示的情况下可以不写 + */ + //安卓10推荐uri,并且path的方式不再可用 + @Override + public void loadGifAsBitmap(@NonNull Context context, @NonNull Uri gifUri, @NonNull ImageView imageView) { + Glide.with(context).asBitmap().load(gifUri).into(imageView); + } + + /** + * 加载gif动图到ImageView,gif动图动 + * + * @param context 上下文 + * @param gifUri gif动图路径Uri + * @param imageView 加载动图的ImageView + *

+ * 备注:不支持动图显示的情况下可以不写 + */ + //安卓10推荐uri,并且path的方式不再可用 + @Override + public void loadGif(@NonNull Context context, @NonNull Uri gifUri, @NonNull ImageView imageView) { + Glide.with(context).asGif().load(gifUri).transition(withCrossFade()).into(imageView); + } + + + /** + * 获取图片加载框架中的缓存Bitmap,不用拼图功能可以直接返回null + * + * @param context 上下文 + * @param uri 图片路径 + * @param width 图片宽度 + * @param height 图片高度 + * @return Bitmap + * @throws Exception 异常直接抛出,EasyPhotos内部处理 + */ + //安卓10推荐uri,并且path的方式不再可用 + @Override + public Bitmap getCacheBitmap(@NonNull Context context, @NonNull Uri uri, int width, int height) throws Exception { + return Glide.with(context).asBitmap().load(uri).submit(width, height).get(); + } + + +} diff --git a/app/src/main/java/com/cheng/bole/utils/ImageUtils.kt b/app/src/main/java/com/cheng/bole/utils/ImageUtils.kt new file mode 100644 index 0000000..bd26846 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/ImageUtils.kt @@ -0,0 +1,129 @@ +package com.cheng.bole.utils + +import android.Manifest +import android.app.Activity +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.example.base.extensions.toast +import com.example.base.utils.L +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +object ImageUtils { + private const val TAG = "ImageUtils" + @RequiresApi(api = Build.VERSION_CODES.Q) + fun saveImageToGallery(context: Context, bitmap: Bitmap, folderName: String) { + val contentResolver = context.contentResolver + val contentValues = ContentValues() + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, generateFileName()) + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") + contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + File.separator + folderName) + var outputStream: OutputStream? = null + try { + // 将图片插入相册 + val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + if (uri != null) { + outputStream = contentResolver.openOutputStream(uri) + if (outputStream != null) { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + outputStream.close() + L.d("Image saved to gallery") + return + } + } + } catch (e: Exception) { + L.e("Failed to save image to gallery: " + e.message) + } finally { + try { + outputStream?.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + Log.e(TAG, "Failed to save image to gallery") + } + + fun generateFileName(): String { + val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + return ("IMG_" + sdf.format(Date())) + ".jpeg" + } + + fun scanImageFile(context: Context?, filePath: String) { + MediaScannerConnection.scanFile(context, arrayOf(filePath), null, null) + } + + fun saveBitmap(mContext:Context,image:Bitmap,path:String):Boolean{ + // 首先保存图片 + + val appDir = File(path) + if (!appDir.exists()) { + appDir.mkdir() + } + val fileName = System.currentTimeMillis().toString() + ".jpeg" + val file = File(appDir, fileName) + try { + val fos = FileOutputStream(file) + // 通过io流的方式来压缩保存图片 + val isSuccess: Boolean = image.compress(Bitmap.CompressFormat.JPEG, 100, fos) + fos.flush() + fos.close() + + // 保存图片后发送广播通知更新数据库 + val uri = Uri.fromFile(file) + mContext.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)) + if (isSuccess) toast("保存成功") + return isSuccess + } catch (e: IOException) { + e.printStackTrace() + } + return false + } + + fun checkPermission(context: Context?): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(context!!, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ContextCompat.checkSelfPermission(context!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + } else { + (ContextCompat.checkSelfPermission(context!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) + } + } + + fun requestPermission(activity: Activity?, requestCode: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityCompat.requestPermissions( + activity!!, arrayOf(Manifest.permission.READ_MEDIA_IMAGES), + requestCode + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ActivityCompat.requestPermissions( + activity!!, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + requestCode + ) + } else { + ActivityCompat.requestPermissions( + activity!!, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), + requestCode + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/JsonUtils.kt b/app/src/main/java/com/cheng/bole/utils/JsonUtils.kt new file mode 100644 index 0000000..9d03677 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/JsonUtils.kt @@ -0,0 +1,60 @@ +package com.cheng.bole.utils + +import android.content.Context +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader + + +object JsonUtils { + + fun loadJsonArrayFromAsset(context: Context, fileName: String): JSONArray? { + var jsonArray: JSONArray? = null + try { + //获取本地的Json文件 + val open: InputStream = context.assets.open(fileName) + val isr = InputStreamReader(open, "UTF-8") + val br = BufferedReader(isr) + var line: String? + val builder = StringBuilder() + while (br.readLine().also { line = it } != null) { + builder.append(line) + } + br.close() + isr.close() + jsonArray = JSONArray(builder.toString()) + } catch (e: IOException) { + e.printStackTrace() + } catch (e: JSONException) { + e.printStackTrace() + } + return jsonArray + } + + fun loadJsonFromAsset(context: Context, fileName: String): JSONObject? { + var jsonObject: JSONObject? = null + try { + //获取本地的Json文件 + val open: InputStream = context.assets.open(fileName) + val isr = InputStreamReader(open, "UTF-8") + val br = BufferedReader(isr) + var line: String? + val builder = StringBuilder() + while (br.readLine().also { line = it } != null) { + builder.append(line) + } + br.close() + isr.close() + jsonObject = JSONObject(builder.toString()) + } catch (e: IOException) { + e.printStackTrace() + } catch (e: JSONException) { + e.printStackTrace() + } + return jsonObject + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/KeyboardUtils.kt b/app/src/main/java/com/cheng/bole/utils/KeyboardUtils.kt new file mode 100644 index 0000000..53d44c5 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/KeyboardUtils.kt @@ -0,0 +1,345 @@ +package com.cheng.bole.utils + +import android.R +import android.app.Activity +import android.content.Context +import android.graphics.Rect +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.ResultReceiver +import android.os.SystemClock +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.view.Window +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.FrameLayout +import com.example.base.utils.BarUtils.getNavBarHeight +import com.example.base.utils.BarUtils.getStatusBarHeight +import com.example.base.utils.Utils.getApp +import kotlin.math.abs + +/** + *

+ * author: Blankj
+ * blog  : http://blankj.com
+ * time  : 2016/08/02
+ * desc  : utils about keyboard
+
* + */ +class KeyboardUtils private constructor() { + + interface OnSoftInputChangedListener { + fun onSoftInputChanged(height: Int) + } + + companion object { + private const val TAG_ON_GLOBAL_LAYOUT_LISTENER = -8 + + /** + * Show the soft input. + */ + fun showSoftInput() { + val imm = getApp() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + ?: return + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) + } + + /** + * Show the soft input. + */ + fun showSoftInput(activity: Activity?) { + if (activity == null) { + return + } + if (!isSoftInputVisible(activity)) { + toggleSoftInput() + } + } + /** + * Show the soft input. + * + * @param view The view. + * @param flags Provides additional operating flags. Currently may be + * 0 or have the [InputMethodManager.SHOW_IMPLICIT] bit set. + */ + /** + * Show the soft input. + * + * @param view The view. + */ + @JvmOverloads + fun showSoftInput(view: View, flags: Int = 0) { + val imm = getApp().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + ?: return + view.isFocusable = true + view.setFocusableInTouchMode(true) + view.requestFocus() + imm.showSoftInput(view, flags, object : ResultReceiver(Handler()) { + override fun onReceiveResult(resultCode: Int, resultData: Bundle) { + if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN + || resultCode == InputMethodManager.RESULT_HIDDEN + ) { + toggleSoftInput() + } + } + }) + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) + } + + /** + * Hide the soft input. + * + * @param activity The activity. + */ + fun hideSoftInput(activity: Activity?) { + if (activity == null) { + return + } + hideSoftInput(activity.window) + } + + /** + * Hide the soft input. + * + * @param window The window. + */ + fun hideSoftInput(window: Window?) { + if (window == null) { + return + } + var view = window.currentFocus + if (view == null) { + val decorView = window.decorView + val focusView = decorView.findViewWithTag("keyboardTagView") + if (focusView == null) { + view = EditText(window.context) + view.setTag("keyboardTagView") + (decorView as ViewGroup).addView(view, 0, 0) + } else { + view = focusView + } + view.requestFocus() + } + hideSoftInput(view) + } + + /** + * Hide the soft input. + * + * @param view The view. + */ + fun hideSoftInput(view: View) { + val imm = getApp() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + ?: return + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + + private var millis: Long = 0 + + /** + * Hide the soft input. + * + * @param activity The activity. + */ + fun hideSoftInputByToggle(activity: Activity?) { + if (activity == null) { + return + } + val nowMillis = SystemClock.elapsedRealtime() + val delta = nowMillis - millis + if (abs(delta.toDouble()) > 500 && isSoftInputVisible(activity)) { + toggleSoftInput() + } + millis = nowMillis + } + + /** + * Toggle the soft input display or not. + */ + fun toggleSoftInput() { + val imm = getApp() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + ?: return + imm.toggleSoftInput(0, 0) + } + + private var sDecorViewDelta = 0 + + /** + * Return whether soft input is visible. + * + * @param activity The activity. + * @return `true`: yes

`false`: no + */ + fun isSoftInputVisible(activity: Activity): Boolean { + return getDecorViewInvisibleHeight(activity.window) > 0 + } + + private fun getDecorViewInvisibleHeight(window: Window): Int { + val decorView = window.decorView + val outRect = Rect() + decorView.getWindowVisibleDisplayFrame(outRect) + Log.d( + "KeyboardUtils", + "getDecorViewInvisibleHeight: " + (decorView.bottom - outRect.bottom) + ) + val delta = abs((decorView.bottom - outRect.bottom).toDouble()).toInt() + if (delta <= getNavBarHeight() + getStatusBarHeight()) { + sDecorViewDelta = delta + return 0 + } + return delta - sDecorViewDelta + } + + /** + * Register soft input changed listener. + * + * @param activity The activity. + * @param listener The soft input changed listener. + */ + fun registerSoftInputChangedListener( + activity: Activity, + listener: OnSoftInputChangedListener + ) { + registerSoftInputChangedListener(activity.window, listener) + } + + /** + * Register soft input changed listener. + * + * @param window The window. + * @param listener The soft input changed listener. + */ + fun registerSoftInputChangedListener( + window: Window, + listener: OnSoftInputChangedListener + ) { + val flags = window.attributes.flags + if (flags and WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS != 0) { + window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } + val contentView = window.findViewById(R.id.content) + val decorViewInvisibleHeightPre = intArrayOf(getDecorViewInvisibleHeight(window)) + val onGlobalLayoutListener = OnGlobalLayoutListener { + val height = getDecorViewInvisibleHeight(window) + if (decorViewInvisibleHeightPre[0] != height) { + listener.onSoftInputChanged(height) + decorViewInvisibleHeightPre[0] = height + } + } + contentView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener) + contentView.setTag(TAG_ON_GLOBAL_LAYOUT_LISTENER, onGlobalLayoutListener) + } + + /** + * Unregister soft input changed listener. + * + * @param window The window. + */ + fun unregisterSoftInputChangedListener(window: Window) { + val contentView = window.findViewById(R.id.content) ?: return + val tag = contentView.getTag(TAG_ON_GLOBAL_LAYOUT_LISTENER) + if (tag is OnGlobalLayoutListener) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + contentView.getViewTreeObserver().removeOnGlobalLayoutListener(tag) + //这里会发生内存泄漏 如果不设置为null + contentView.setTag(TAG_ON_GLOBAL_LAYOUT_LISTENER, null) + } + } + } + + /** + * Fix the bug of 5497 in Android. + * + * Don't set adjustResize + * + * @param activity The activity. + */ + fun fixAndroidBug5497(activity: Activity) { + fixAndroidBug5497(activity.window) + } + + /** + * Fix the bug of 5497 in Android. + * + * It will clean the adjustResize + * + * @param window The window. + */ + fun fixAndroidBug5497(window: Window) { + val softInputMode = window.attributes.softInputMode + window.setSoftInputMode( + softInputMode and WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE.inv() + ) + val contentView = window.findViewById(R.id.content) + val contentViewChild = contentView.getChildAt(0) + val paddingBottom = contentViewChild.paddingBottom + val contentViewInvisibleHeightPre5497 = intArrayOf(getContentViewInvisibleHeight(window)) + contentView.getViewTreeObserver().addOnGlobalLayoutListener { + val height = getContentViewInvisibleHeight(window) + if (contentViewInvisibleHeightPre5497[0] != height) { + contentViewChild.setPadding( + contentViewChild.getPaddingLeft(), + contentViewChild.paddingTop, contentViewChild.getPaddingRight(), + paddingBottom + getDecorViewInvisibleHeight(window) + ) + contentViewInvisibleHeightPre5497[0] = height + } + } + } + + private fun getContentViewInvisibleHeight(window: Window): Int { + val contentView = window.findViewById(R.id.content) ?: return 0 + val outRect = Rect() + contentView.getWindowVisibleDisplayFrame(outRect) + Log.d( + "KeyboardUtils", + "getContentViewInvisibleHeight: " + (contentView.bottom - outRect.bottom) + ) + val delta = abs((contentView.bottom - outRect.bottom).toDouble()).toInt() + return if (delta <= getStatusBarHeight() + getNavBarHeight()) { + 0 + } else delta + } + + /** + * Fix the leaks of soft input. + * + * @param activity The activity. + */ + fun fixSoftInputLeaks(activity: Activity) { + fixSoftInputLeaks(activity.window) + } + + /** + * Fix the leaks of soft input. + * + * @param window The window. + */ + fun fixSoftInputLeaks(window: Window) { + val imm = getApp().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + ?: return + val leakViews = arrayOf("mLastSrvView", "mCurRootView", "mServedView", "mNextServedView") + for (leakView in leakViews) { + try { + val leakViewField = InputMethodManager::class.java.getDeclaredField(leakView) + if (!leakViewField.isAccessible) { + leakViewField.isAccessible = true + } + val obj = leakViewField[imm] as? View ?: continue + if (obj.getRootView() === window.decorView.getRootView()) { + leakViewField[imm] = null + } + } catch (ignore: Throwable) { /**/ + } + } + } + } +} diff --git a/app/src/main/java/com/cheng/bole/utils/MD5Utils.kt b/app/src/main/java/com/cheng/bole/utils/MD5Utils.kt new file mode 100644 index 0000000..3fc8d8b --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/MD5Utils.kt @@ -0,0 +1,41 @@ +package com.cheng.bole.utils + +import java.io.FileInputStream +import java.io.IOException +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +object MD5Utils { + + fun getFileMD5(filePath: String?): String { + val sb = StringBuilder() + var md: MessageDigest? = null + var fis: FileInputStream? = null + val buffer = ByteArray(1024) + var len: Int + try { + md = MessageDigest.getInstance("MD5") + fis = FileInputStream(filePath) + while (fis.read(buffer, 0, 1024).also { len = it } != -1) { + md.update(buffer, 0, len) + } + val digest = md.digest() + for (b in digest) { + sb.append(String.format("%02x", b)) + } + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } finally { + if (fis != null) { + try { + fis.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + return sb.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/NaviUtils.kt b/app/src/main/java/com/cheng/bole/utils/NaviUtils.kt new file mode 100644 index 0000000..caf0a0d --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/NaviUtils.kt @@ -0,0 +1,68 @@ +package com.cheng.bole.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.example.base.extensions.toast +import com.example.base.utils.AppUtils +import com.example.base.utils.Utils + +object NaviUtils { + + fun goAMapNavi(context: Context, address: String, lng: String, lat: String) { + if (isAMapInstall()) { + val uri = "amapuri://route/plan/?did=&dlat=${lat}&dlon=${lng}&dname=${address}&dev=0&t=1" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) + context.startActivity(intent) + } else { + toast("请安装高德地图后再试") + } + } + + fun goTxMapNavi(context: Context, address: String, lng: String, lat: String) { + if (isTxMapInstall()) { + val uri = "qqmap://map/routeplan?type=bus&to=${address}&tocoord=${lat},${lng}&referer=${AppUtils.getAppName()}" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) + context.startActivity(intent) + } else { + toast("请安装腾讯地图后再试") + } + } + + fun goBaiduMapNavi(context: Context, address: String, lng: String, lat: String) { + if (isBaiduMapInstall()) { + val uri = "baidumap://map/direction?destination=name:${address}|latlng:${lat},${lng}&coord_type=gcj02&mode=transit&sy=0&src=${context.packageName}" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) + context.startActivity(intent) + } else { + toast("请安装百度地图后再试") + } + } + + private fun isAMapInstall(): Boolean { + return try { + Utils.getApp().packageManager.getPackageInfo("com.autonavi.minimap", 0) + true + } catch (e: Exception) { + false + } + } + + private fun isBaiduMapInstall(): Boolean { + return try { + Utils.getApp().packageManager.getPackageInfo("com.baidu.BaiduMap", 0) + true + } catch (e: Exception) { + false + } + } + + private fun isTxMapInstall(): Boolean { + return try { + Utils.getApp().packageManager.getPackageInfo("com.tencent.map", 0) + true + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/PermissionUtils.kt b/app/src/main/java/com/cheng/bole/utils/PermissionUtils.kt new file mode 100644 index 0000000..c22fea5 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/PermissionUtils.kt @@ -0,0 +1,187 @@ +package com.cheng.bole.utils + +import android.Manifest +import android.app.Activity +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.fragment.app.FragmentManager +import com.cheng.bole.manager.NotificationHelper +import com.cheng.bole.ui.dialog.PermissionTipDialog +import com.example.base.extensions.toast +import com.hjq.permissions.OnPermissionCallback +import com.hjq.permissions.XXPermissions + +object PermissionUtils { + + /** + * 相册和相机权限 + */ + val PHOTO_CAMERA_PERMISSION = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mutableListOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.CAMERA) + } else { + mutableListOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) + } + + /** + * 相册权限 + */ + val PHOTO_PERMISSION = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mutableListOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO) + } else { + mutableListOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + /** + * 音频权限 + */ + val AUDIO_PERMISSION = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mutableListOf(Manifest.permission.READ_MEDIA_AUDIO) + } else { + mutableListOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + /** + * 本地读取权限 + */ + val STORAGE_PERMISSION = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mutableListOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_AUDIO) + } else { + mutableListOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + /** + * 通知权限 + */ + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + val NOTIFICATION_PERMISSION = mutableListOf(Manifest.permission.POST_NOTIFICATIONS) + + fun checkStoragePermission(activity: Activity, fm: FragmentManager, callback: ((isGranted: Boolean) -> Unit)? = null) { + if (XXPermissions.isGranted(activity, STORAGE_PERMISSION)) { + callback?.invoke(true) + } else { + var showTip = true + var showToast = false + var f : PermissionTipDialog? = null + XXPermissions.with(activity).permission(STORAGE_PERMISSION).request(object : OnPermissionCallback { + override fun onGranted(list: MutableList, isGranted: Boolean) { + showTip = false + if (isGranted) { + callback?.invoke(true) + } else { + if (!showToast) { + toast("使用文件查看删除和保存功能请允许储存权限") + callback?.invoke(false) + showToast = true + } + } + f?.dismiss() + } + + override fun onDenied(permissions: MutableList, doNotAskAgain: Boolean) { + showTip = false + if (!showToast) { + toast("使用文件查看删除和保存功能请允许储存权限") + callback?.invoke(false) + showToast = true + } + f?.dismiss() + } + }) + activity.window.decorView.postDelayed({ + if (showTip && !activity.isDestroyed) { + f = PermissionTipDialog.newInstance("此功能需要您授权允许储存权限,以便正常查看删除文件或保存文件到本地") + f!!.show(fm, null) + } + }, 300) + } + } + + fun checkPhotoPermission(activity: Activity, fm: FragmentManager, needCamera: Boolean = false, callback: ((Boolean) -> Unit)? = null) { + if (XXPermissions.isGranted(activity, if (needCamera) PHOTO_CAMERA_PERMISSION else PHOTO_PERMISSION)) { + callback?.invoke(true) + } else { + var showTip = true + var f : PermissionTipDialog? = null + XXPermissions.with(activity).permission(if (needCamera) PHOTO_CAMERA_PERMISSION else PHOTO_PERMISSION).request(object : OnPermissionCallback { + override fun onGranted(list: MutableList, isGranted: Boolean) { + showTip = false + if (isGranted) { + callback?.invoke(true) + } else { + toast("使用本地视频或图像添加、查看、分享和删除等功能请允许相册,相机和储存权限") + callback?.invoke(false) + } + f?.dismiss() + } + + override fun onDenied(permissions: MutableList, doNotAskAgain: Boolean) { + showTip = false + toast("使用本地视频或图像添加、查看、分享和删除等功能请允许相册,相机和储存权限") + callback?.invoke(false) + f?.dismiss() + } + }) + activity.window.decorView.postDelayed({ + if (showTip && !activity.isDestroyed) { + f = PermissionTipDialog.newInstance("此功能需要您授权允许相册,相机和储存权限,以便正常使用相册和相机添加、查看、分享和删除本地视频或图像") + f!!.show(fm, null) + } + }, 300) + } + } + + fun checkAudioPermission(activity: Activity, fm: FragmentManager, callback: ((Boolean) -> Unit)? = null) { + if (XXPermissions.isGranted(activity, AUDIO_PERMISSION)) { + callback?.invoke(true) + } else { + var showTip = true + var f : PermissionTipDialog? = null + XXPermissions.with(activity).permission(AUDIO_PERMISSION).request(object : OnPermissionCallback { + override fun onGranted(list: MutableList, isGranted: Boolean) { + showTip = false + if (isGranted) { + callback?.invoke(true) + } else { + toast("使用本地音频查看、分享和删除等功能请允许音频读写权限") + callback?.invoke(false) + } + f?.dismiss() + } + + override fun onDenied(permissions: MutableList, doNotAskAgain: Boolean) { + showTip = false + toast("使用本地音频查看、分享和删除等功能请允许音频读写权限") + callback?.invoke(false) + f?.dismiss() + } + }) + activity.window.decorView.postDelayed({ + if (showTip && !activity.isDestroyed) { + f = PermissionTipDialog.newInstance("此功能需要您授权允许音频读写权限,以便正常使用本地音频查看、分享和删除等功能") + f!!.show(fm, null) + } + }, 300) + } + } + + fun checkNotificationPermission(activity: Activity, callback: ((isGranted: Boolean) -> Unit)? = null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (XXPermissions.isGranted(activity, NOTIFICATION_PERMISSION)) { + callback?.invoke(true) + } else { + XXPermissions.with(activity).permission(NOTIFICATION_PERMISSION).request(object : OnPermissionCallback { + override fun onGranted(list: MutableList, isGranted: Boolean) { + callback?.invoke(isGranted) + } + + override fun onDenied(permissions: MutableList, doNotAskAgain: Boolean) { + callback?.invoke(false) + } + }) + } + } else { + callback?.invoke(NotificationHelper.isNotificationEnabled(activity)) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/SaveUtils.kt b/app/src/main/java/com/cheng/bole/utils/SaveUtils.kt new file mode 100644 index 0000000..f0f7f59 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/SaveUtils.kt @@ -0,0 +1,318 @@ +package com.cheng.bole.utils + +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import androidx.annotation.RequiresApi +import com.example.base.utils.AppUtils +import com.example.base.utils.L +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.nio.file.Files + + +object SaveUtils { + private const val TAG = "SaveUtils" + + /** + * 将图片文件保存到系统相册 + */ + fun saveImgFileToAlbum(context: Context, imageFilePath: String, record: Boolean = true): Boolean { + Log.d(TAG, "saveImgToAlbum() imageFile = [$imageFilePath]") + return try { + val bitmap = BitmapFactory.decodeFile(imageFilePath) + saveBitmapToAlbum(context, bitmap, record) + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + /** + * 将bitmap保存到系统相册 + */ + fun saveBitmapToAlbum(context: Context, bitmap: Bitmap?, record: Boolean = true): Boolean { + if (bitmap == null) return false + val fileName = "jk_${System.currentTimeMillis()}.jpeg" + val saved = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + saveBitmapToAlbumBeforeQ(context, bitmap, fileName) + } else { + saveBitmapToAlbumAfterQ(context, bitmap, fileName) + } + return saved + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + private fun saveBitmapToAlbumAfterQ(context: Context, bitmap: Bitmap, fileName: String): Boolean { + val contentUri: Uri = if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } else { + MediaStore.Images.Media.INTERNAL_CONTENT_URI + } + val contentValues = getImageContentValues(fileName) + val uri = context.contentResolver.insert(contentUri, contentValues) ?: return false + var os: OutputStream? = null + return try { + os = context.contentResolver.openOutputStream(uri) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os!!) + contentValues.clear() + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) + context.contentResolver.update(uri, contentValues, null, null) + true + } catch (e: Exception) { + context.contentResolver.delete(uri, null, null) + e.printStackTrace() + false + } finally { + try { + os?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + private fun saveBitmapToAlbumBeforeQ(context: Context, bitmap: Bitmap, fileName: String): Boolean { + val picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + val destDir = File(picDir, AppUtils.getAppName()) + if (!destDir.exists() || !destDir.isDirectory) destDir.mkdirs() + val destFile = File(destDir, fileName) + var os: OutputStream? = null + var result = false + try { + os = BufferedOutputStream(FileOutputStream(destFile)) + result = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os) + if (!bitmap.isRecycled) bitmap.recycle() + } catch (e: IOException) { + e.printStackTrace() + } finally { + try { + os?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + MediaScannerConnection.scanFile(context, arrayOf(destFile.absolutePath), arrayOf("image/*")) { path: String, uri: Uri -> + Log.d(TAG, "saveImgToAlbum: $path $uri") + } + return result + } + + /** + * 获取图片的ContentValue + * + * @param context + */ + @RequiresApi(api = Build.VERSION_CODES.Q) + private fun getImageContentValues(fileName: String): ContentValues { + val contentValues = ContentValues() + contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM + File.separator + AppUtils.getAppName()) + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1) + contentValues.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + contentValues.put(MediaStore.Images.Media.DATE_MODIFIED, System.currentTimeMillis()) + contentValues.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) + return contentValues + } + + /** + * 将视频保存到系统相册 + */ + fun saveVideoToAlbum(context: Context, videoFile: String): Boolean { + Log.d(TAG, "saveVideoToAlbum() videoFile = [$videoFile]") + val saved = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + saveVideoToAlbumBeforeQ(context, videoFile) + } else { + saveVideoToAlbumAfterQ(context, videoFile) + } + return saved + } + + private fun saveVideoToAlbumAfterQ(context: Context, videoFile: String): Boolean { + return try { + val contentResolver = context.contentResolver + val tempFile = File(videoFile) + val contentValues = getVideoContentValues(tempFile, System.currentTimeMillis()) + val uri = contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues) + copyFileAfterQ(context, contentResolver, tempFile, uri) + contentValues.clear() + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) + context.contentResolver.update(uri!!, contentValues, null, null) + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + private fun saveVideoToAlbumBeforeQ(context: Context, videoFile: String): Boolean { + val picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + val tempFile = File(videoFile) + val destDir = File(picDir, AppUtils.getAppName()) + if (!destDir.exists() || !destDir.isDirectory) destDir.mkdirs() + val destFile = File(destDir, tempFile.getName()) + var ins: FileInputStream? = null + var ous: BufferedOutputStream? = null + return try { + ins = FileInputStream(tempFile) + ous = BufferedOutputStream(FileOutputStream(destFile)) + var nread = 0L + val buf = ByteArray(1024) + var n: Int + while (ins.read(buf).also { n = it } > 0) { + ous.write(buf, 0, n) + nread += n.toLong() + } + MediaScannerConnection.scanFile( + context, arrayOf(destFile.absolutePath), arrayOf("video/*") + ) { path: String, uri: Uri -> + L.d("saveVideoToAlbum: $path $uri") + } + true + } catch (e: Exception) { + e.printStackTrace() + false + } finally { + try { + ins?.close() + ous?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + @Throws(IOException::class) + private fun copyFileAfterQ(context: Context, localContentResolver: ContentResolver, tempFile: File, localUri: Uri?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + context.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.Q + ) { + //拷贝文件到相册的uri,android10及以上得这么干,否则不会显示。可以参考ScreenMediaRecorder的save方法 + val os = localContentResolver.openOutputStream(localUri!!) + Files.copy(tempFile.toPath(), os) + os!!.close() + } + } + + /** + * 获取视频的contentValue + */ + private fun getVideoContentValues(paramFile: File, timestamp: Long): ContentValues { + val localContentValues = ContentValues() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + localContentValues.put( + MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM + + File.separator + AppUtils.getAppName() + ) + } + localContentValues.put(MediaStore.Video.Media.TITLE, paramFile.getName()) + localContentValues.put(MediaStore.Video.Media.DISPLAY_NAME, paramFile.getName()) + localContentValues.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") + localContentValues.put(MediaStore.Video.Media.DATE_TAKEN, timestamp) + localContentValues.put(MediaStore.Video.Media.DATE_MODIFIED, timestamp) + localContentValues.put(MediaStore.Video.Media.DATE_ADDED, timestamp) + localContentValues.put(MediaStore.Video.Media.SIZE, paramFile.length()) + return localContentValues + } + + /** + * 将视频音频到系统 + */ + fun saveAudioToAlbum(context: Context, audioFile: String): Boolean { + Log.d(TAG, "saveAudioToAlbum() audioFile = [$audioFile]") + val saved = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + saveAudioToAlbumBeforeQ(context, audioFile) + } else { + saveAudioToAlbumAfterQ(context, audioFile) + } + return saved + } + + private fun saveAudioToAlbumAfterQ(context: Context, audioFile: String): Boolean { + return try { + val contentResolver = context.contentResolver + val tempFile = File(audioFile) + val contentValues = getAudioContentValues(tempFile, System.currentTimeMillis()) + val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues) + copyFileAfterQ(context, contentResolver, tempFile, uri) + contentValues.clear() + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) + context.contentResolver.update(uri!!, contentValues, null, null) + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + private fun saveAudioToAlbumBeforeQ(context: Context, audioFile: String): Boolean { + val picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) + val tempFile = File(audioFile) + val destDir = File(picDir, AppUtils.getAppName()) + if (!destDir.exists() || !destDir.isDirectory) destDir.mkdirs() + val destFile = File(destDir, tempFile.getName()) + var ins: FileInputStream? = null + var ous: BufferedOutputStream? = null + return try { + ins = FileInputStream(tempFile) + ous = BufferedOutputStream(FileOutputStream(destFile)) + var nread = 0L + val buf = ByteArray(1024) + var n: Int + while (ins.read(buf).also { n = it } > 0) { + ous.write(buf, 0, n) + nread += n.toLong() + } + MediaScannerConnection.scanFile( + context, arrayOf(destFile.absolutePath), arrayOf("audio/*") + ) { path: String, uri: Uri -> + L.d("saveAudioToAlbum: $path $uri") + } + true + } catch (e: Exception) { + e.printStackTrace() + false + } finally { + try { + ins?.close() + ous?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + /** + * 获取音频的contentValue + */ + private fun getAudioContentValues(paramFile: File, timestamp: Long): ContentValues { + val localContentValues = ContentValues() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + localContentValues.put( + MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_MUSIC + + File.separator + AppUtils.getAppName() + ) + } + localContentValues.put(MediaStore.Audio.Media.TITLE, paramFile.getName()) + localContentValues.put(MediaStore.Audio.Media.DISPLAY_NAME, paramFile.getName()) + localContentValues.put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp3") + localContentValues.put(MediaStore.Audio.Media.DATE_TAKEN, timestamp) + localContentValues.put(MediaStore.Audio.Media.DATE_MODIFIED, timestamp) + localContentValues.put(MediaStore.Audio.Media.DATE_ADDED, timestamp) + localContentValues.put(MediaStore.Audio.Media.SIZE, paramFile.length()) + return localContentValues + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/StringUtils.kt b/app/src/main/java/com/cheng/bole/utils/StringUtils.kt new file mode 100644 index 0000000..8421c57 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/StringUtils.kt @@ -0,0 +1,495 @@ +package com.cheng.bole.utils + +import android.text.TextUtils +import com.example.base.utils.SpanUtils +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.Hashtable +import java.util.Locale +import java.util.UUID +import java.util.concurrent.ThreadLocalRandom +import java.util.regex.Matcher +import java.util.regex.Pattern + + +object StringUtils { + /** + * 功能:身份证的有效验证 + * + * @param IDStr 身份证号 + * @return 有效:返回"" 无效:返回String信息 + * @throws ParseException + */ + @Throws(ParseException::class) + fun IDCardValidate(IDStr: String): Boolean { + var errorInfo = "" // 记录错误信息 + val ValCodeArr = arrayOf( + "1", "0", "x", "9", "8", "7", "6", "5", "4", + "3", "2" + ) + val Wi = arrayOf( + "7", "9", "10", "5", "8", "4", "2", "1", "6", "3", "7", + "9", "10", "5", "8", "4", "2" + ) + var Ai = "" + // ================号码的长度 15位或18位 ================ + if (IDStr.length != 15 && IDStr.length != 18) { + errorInfo = "身份证号码长度应该为15位或18位。" + return false + } + // =======================(end)======================== + + // ================ 数字 除最后以为都为数字================ + if (IDStr.length == 18) { + Ai = IDStr.substring(0, 17) + } else if (IDStr.length == 15) { + Ai = IDStr.substring(0, 6) + "19" + IDStr.substring(6, 15) + } + if (isNumeric(Ai) == false) { + errorInfo = "身份证15位号码都应为数字 ; 18位号码除最后一位外,都应为数字。" + return false + } + // =======================(end)======================== + + // ================ 出生年月是否有效 ================ + val strYear = Ai.substring(6, 10) // 年份 + val strMonth = Ai.substring(10, 12) // 月份 + val strDay = Ai.substring(12, 14) // 月份 + if (isDataFormat("$strYear-$strMonth-$strDay") == false) { + errorInfo = "身份证生日无效。" + return false + } + val gc = GregorianCalendar() + val s = SimpleDateFormat("yyyy-MM-dd") + try { + if (gc[Calendar.YEAR] - strYear.toInt() > 150 + || gc.time.time - s.parse( + "$strYear-$strMonth-$strDay" + ).time < 0 + ) { + errorInfo = "身份证生日不在有效范围。" + return false + } + } catch (e: NumberFormatException) { + // TODO Auto-generated catch block + e.printStackTrace() + } catch (e: ParseException) { + // TODO Auto-generated catch block + e.printStackTrace() + } + if (strMonth.toInt() > 12 || strMonth.toInt() == 0) { + errorInfo = "身份证月份无效" + return false + } + if (strDay.toInt() > 31 || strDay.toInt() == 0) { + errorInfo = "身份证日期无效" + return false + } + // =====================(end)===================== + + // ================ 地区码时候有效================ + val h = GetAreaCode() + if (h[Ai.substring(0, 2)] == null) { + errorInfo = "身份证地区编码错误。" + return false + } + // ============================================== + + // ================ 判断最后一位的值================ + var TotalmulAiWi = 0 + for (i in 0..16) { + TotalmulAiWi = (TotalmulAiWi + + Ai[i].toString().toInt() * Wi[i].toInt()) + } + val modValue = TotalmulAiWi % 11 + val strVerifyCode = ValCodeArr[modValue] + Ai = Ai + strVerifyCode + if (IDStr.length == 18) { + if (Ai == IDStr == false) { + errorInfo = "身份证无效,不是合法的身份证号码" + return false + } + } else { + return true + } + // =====================(end)===================== + return true + } + + /** + * 功能:判断字符串是否为数字 + * + * @param str + * @return + */ + private fun isNumeric(str: String): Boolean { + val pattern = Pattern.compile("[0-9]*") + val isNum = pattern.matcher(str) + return if (isNum.matches()) { + true + } else { + false + } + } + + /** + * 功能:设置地区编码 + * + * @return Hashtable 对象 + */ + private fun GetAreaCode(): Hashtable { + val hashtable = Hashtable() + hashtable["11"] = "北京" + hashtable["12"] = "天津" + hashtable["13"] = "河北" + hashtable["14"] = "山西" + hashtable["15"] = "内蒙古" + hashtable["21"] = "辽宁" + hashtable["22"] = "吉林" + hashtable["23"] = "黑龙江" + hashtable["31"] = "上海" + hashtable["32"] = "江苏" + hashtable["33"] = "浙江" + hashtable["34"] = "安徽" + hashtable["35"] = "福建" + hashtable["36"] = "江西" + hashtable["37"] = "山东" + hashtable["41"] = "河南" + hashtable["42"] = "湖北" + hashtable["43"] = "湖南" + hashtable["44"] = "广东" + hashtable["45"] = "广西" + hashtable["46"] = "海南" + hashtable["50"] = "重庆" + hashtable["51"] = "四川" + hashtable["52"] = "贵州" + hashtable["53"] = "云南" + hashtable["54"] = "西藏" + hashtable["61"] = "陕西" + hashtable["62"] = "甘肃" + hashtable["63"] = "青海" + hashtable["64"] = "宁夏" + hashtable["65"] = "新疆" + hashtable["71"] = "台湾" + hashtable["81"] = "香港" + hashtable["82"] = "澳门" + hashtable["91"] = "国外" + return hashtable + } + + /** + * 验证日期字符串是否是YYYY-MM-DD格式 + * + * @param str + * @return + */ + private fun isDataFormat(str: String): Boolean { + var flag = false + // String + // regxStr="[1-9][0-9]{3}-[0-1][0-2]-((0[1-9])|([12][0-9])|(3[01]))"; + val regxStr = + "^((\\d{2}(([02468][048])|([13579][26]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])))))|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))(\\s(((0?[0-9])|([1-2][0-3]))\\:([0-5]?[0-9])((\\s)|(\\:([0-5]?[0-9])))))?$" + val pattern1 = Pattern.compile(regxStr) + val isNo = pattern1.matcher(str) + if (isNo.matches()) { + flag = true + } + return flag + } + //2.判断字符串是否是邮箱: + /** + * 描述:是否是邮箱. + * + * @param str 指定的字符串 + * @return 是否是邮箱:是为true,否则false + */ + fun isEmail(str: String): Boolean { + var isEmail = false + val expr = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$" + if (str.matches(expr.toRegex())) { + isEmail = true + } + return isEmail + } + //3.判断字符串是否是银行卡 + /** + * 判断是否是银行卡号 + * + * @param cardId + * @return + */ + fun checkBankCard(cardId: String): Boolean { + val bit = getBankCardCheckCode( + cardId + .substring(0, cardId.length - 1) + ) + return if (bit == 'N') { + false + } else cardId[cardId.length - 1] == bit + } + + private fun getBankCardCheckCode(nonCheckCodeCardId: String?): Char { + if (nonCheckCodeCardId == null || nonCheckCodeCardId.trim { it <= ' ' }.length == 0 || !nonCheckCodeCardId.matches("\\d+".toRegex())) { + // 如果传的不是数据返回N + return 'N' + } + val chs = nonCheckCodeCardId.trim { it <= ' ' }.toCharArray() + var luhmSum = 0 + var i = chs.size - 1 + var j = 0 + while (i >= 0) { + var k = chs[i].code - '0'.code + if (j % 2 == 0) { + k *= 2 + k = k / 10 + k % 10 + } + luhmSum += k + i-- + j++ + } + return if (luhmSum % 10 == 0) '0' else (10 - luhmSum % 10 + '0'.code).toChar() + } + //4、判断字符串是否是手机号 + /** + * 判断是否是手机号 + * + * @param phone + * @return + */ + fun checkPhone(phone: String?): Boolean { + val pattern = Pattern + .compile("^(13[0-9]|15[0-3]|15[5-9]|18[0-9]|14[57]|17[0678])\\d{8}$") + val matcher = pattern.matcher(phone) + return if (matcher.matches()) { + true + } else false + } + //5.判断字符串是否是中文或者包含中文 + /** + * 描述:判断一个字符串是否为null或空值. + * + * @param str 指定的字符串 + * @return true or false + */ + fun isEmpty(str: String?): Boolean { + return str == null || str.trim { it <= ' ' }.length == 0 + } + + /** + * 描述:是否是中文. + * + * @param str 指定的字符串 + * @return 是否是中文:是为true,否则false + */ + fun isChinese(str: String): Boolean { + var isChinese = true + val chinese = "[\u0391-\uFFE5]" + if (!isEmpty(str)) { + //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 + for (i in 0 until str.length) { + //获取一个字符 + val temp = str.substring(i, i + 1) + //判断是否为中文字符 + if (temp.matches(chinese.toRegex())) { + } else { + isChinese = false + } + } + } + return isChinese + } + + /** + * 描述:是否包含中文. + * + * @param str 指定的字符串 + * @return 是否包含中文:是为true,否则false + */ + fun isContainChinese(str: String): Boolean { + var isChinese = false + val chinese = "[\u0391-\uFFE5]" + if (!isEmpty(str)) { + //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 + for (i in 0 until str.length) { + //获取一个字符 + val temp = str.substring(i, i + 1) + //判断是否为中文字符 + if (temp.matches(chinese.toRegex())) { + isChinese = true + } else { + } + } + } + return isChinese + } + + /** + * 比较两个String的list 是否改变过 + * + * @param listNew + * @param listOld + * @return + */ + fun compareList(listNew: List?, listOld: List?): Boolean { + return if (listOld == null && listNew == null) { + false + } else if (listOld != null && listNew != null) { + if (listNew.size != listOld.size) { + true + } else !(listOld.containsAll(listNew) && listNew.containsAll(listOld)) + } else { + true + } + } + + /** + * 比较两个String 是否改变过 + * + * @param newStr + * @param oldStr + * @return + */ + fun compareString(newStr: String?, oldStr: String?): Boolean { + return if (newStr == null && oldStr == null) { + false + } else if (newStr != null && oldStr != null) { + newStr != oldStr + } else { + true + } + } + + /** + * 比较签名 是否改变过 + * + * @param newStr + * @param oldStr + * @return + */ + fun compareSign(newStr: String, oldStr: String): Boolean { + return if (TextUtils.isEmpty(oldStr)) { //当原始值没有的时候 无需验证 因为上报时需验证是否已签过字 而且当原始值有的时候 依据现有业务新值不可能清空 故无需再判断其他情况 + true + } else newStr != oldStr + } + + /** + * 随机生成一个UUID + */ + fun createUUID(): String { + return UUID.randomUUID().toString() + } + + fun createUUIDFromLong(): String { + return UUID(ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong()).toString() + } + + /** + * MD5加密 + */ + fun getMD5String(data: String): String? { + try { + val md = MessageDigest.getInstance("MD5") + md.update(data.toByteArray()) + val result = md.digest() + val stringBuffer = StringBuffer() + for (i in result.indices) { + val hex = Integer.toHexString(0xff and result[i].toInt()) + if (hex.length == 1) stringBuffer.append('0') + stringBuffer.append(hex) + } + return stringBuffer.toString() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return null + } + + /** + * 对数组进行MD5加密 + * + * @param bytes + * @return + */ + fun getMD5Byte(bytes: ByteArray?): String? { + try { + val md = MessageDigest.getInstance("MD5") + md.update(bytes) + val result = md.digest() + val stringBuffer = StringBuffer() + for (i in result.indices) { + val hex = Integer.toHexString(0xff and result[i].toInt()) + if (hex.length == 1) stringBuffer.append('0') + stringBuffer.append(hex) + } + return stringBuffer.toString() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return null + } + + //两个数组进行相加 + fun addByte(array1: ByteArray, array2: ByteArray): ByteArray { + val combined = ByteArray(array1.size + array2.size) + System.arraycopy(array1, 0, combined, 0, array1.size) + System.arraycopy(array2, 0, combined, array1.size, array2.size) + return combined + } + + //格式化时间yyyy-mm-dd + fun getSimpleYYYYMMDD(str: String): String { + if (TextUtils.isEmpty(str)) return "" + + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA) + val date = simpleDateFormat.parse(str) + return simpleDateFormat.format(date!!) + + } + + /** + * @desc 格式化数量 + * + * @param str 后台格式化后返回数量字符串 + * @param prefixUnit 前单位 + * @param suffixUnit 后单位 + */ + fun formatCount(str: String, prefixUnit: String = "", suffixUnit: String? = ""): CharSequence { + val spanUtils = SpanUtils() + .append(prefixUnit).setFontSize(12, true) + if (str.endsWith("千")) { + spanUtils.append(str.replace("千", "")).append("千$suffixUnit").setFontSize(12, true) + } else if (str.endsWith("万")) { + spanUtils.append(str.replace("万", "")).append("万$suffixUnit").setFontSize(12, true) + } else { + spanUtils.append(str).append("$suffixUnit").setFontSize(12, true) + } + return spanUtils.create() + } + + /** + * 将字符串中的unicode字符转换为中文字符 + */ + fun convertUnicodeToCh(str: String): String { + var newStr = str + val pattern: Pattern = Pattern.compile("(\\\\u(\\w{4}))") + val matcher: Matcher = pattern.matcher(newStr) + + // 迭代,将str中的所有unicode转换为正常字符 + while (matcher.find()) { + val unicodeFull = matcher.group(1) // 匹配出的每个字的unicode,比如\u83b7 + val unicodeNum = matcher.group(2) // 匹配出每个字的数字,比如\u83b7,会匹配出u83b7 + + // 将匹配出的数字按照16进制转换为10进制,转换为char类型,就是对应的正常字符了 + val singleChar = unicodeNum.toInt(16).toChar() + + // 替换原始字符串中的unicode码 + newStr = newStr.replace(unicodeFull, singleChar.toString() + "") + } + return newStr + } +} + diff --git a/app/src/main/java/com/cheng/bole/utils/UIUtils.kt b/app/src/main/java/com/cheng/bole/utils/UIUtils.kt new file mode 100644 index 0000000..bb8b750 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/UIUtils.kt @@ -0,0 +1,35 @@ +package com.cheng.bole.utils + +import android.text.TextUtils +import com.example.base.utils.L + +object UIUtils { + + fun checkVersion(version: String, origin: String): Boolean { + if (TextUtils.isEmpty(version) || TextUtils.isEmpty(origin)) { + return false + } + val versions = version.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val origins = origin.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (versions.size != 3) { + return false + } + if (origins.size != 3) { + return false + } + for (i in 0..2) { + L.e(versions[i]) + L.e(origins[i]) + if (versions[i].toInt() > origins[i].toInt()) { + return true + } else if (versions[i].toInt() < origins[i].toInt()) { + return false + } + } + return false + } + + fun getFileNameFromUrl(url: String): String { + return url.substring(url.lastIndexOf('/') + 1) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/UrlHelper.kt b/app/src/main/java/com/cheng/bole/utils/UrlHelper.kt new file mode 100644 index 0000000..295fb90 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/UrlHelper.kt @@ -0,0 +1,20 @@ +package com.cheng.bole.utils + +import android.content.Context +import com.cheng.bole.common.Constants +import com.example.base.browser.BrowserActivity + +object UrlHelper { + + fun startUserAgreement(context: Context, title: String? = null) { + BrowserActivity.start(context, title ?: "用户协议", Constants.userAgreement, false) + } + + fun startPrivacyPolicy(context: Context) { + BrowserActivity.start(context, "隐私政策", Constants.privacyPolicy, false) + } + + fun startRenewAgreement(context: Context) { + BrowserActivity.start(context, "自动续费服务规则", Constants.renewAgreement) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/WxUtils.kt b/app/src/main/java/com/cheng/bole/utils/WxUtils.kt new file mode 100644 index 0000000..feb36dc --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/WxUtils.kt @@ -0,0 +1,29 @@ +package com.cheng.bole.utils + +import com.cheng.bole.common.Constants +import com.example.base.utils.Utils +import com.tencent.mm.opensdk.constants.ConstantsAPI.WXApp +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory + +object WxUtils { + var api: IWXAPI? = null + + init { + api = WXAPIFactory.createWXAPI(Utils.getApp(), Constants.WechatAppId) + } + + fun isWxInstalled(): Boolean { + return api!!.isWXAppInstalled + } + + fun getWxVersionName(): String { + try { + val packageInfo = Utils.getApp().packageManager.getPackageInfo(WXApp.WXAPP_PACKAGE_NAME, 0) + return packageInfo.versionName + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/appwalle/ApkUtil.java b/app/src/main/java/com/cheng/bole/utils/appwalle/ApkUtil.java new file mode 100644 index 0000000..ebd4d90 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/appwalle/ApkUtil.java @@ -0,0 +1,186 @@ +package com.cheng.bole.utils.appwalle; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.util.LinkedHashMap; +import java.util.Map; + +final class ApkUtil { + private ApkUtil() { + } + + public static long getCommentLength(FileChannel fileChannel) throws IOException { + long archiveSize = fileChannel.size(); + if (archiveSize < 22L) { + throw new IOException("APK too small for ZIP End of Central Directory (EOCD) record"); + } else { + long maxCommentLength = Math.min(archiveSize - 22L, 65535L); + long eocdWithEmptyCommentStartPosition = archiveSize - 22L; + for(int expectedCommentLength = 0; (long)expectedCommentLength <= maxCommentLength; ++expectedCommentLength) { + long eocdStartPos = eocdWithEmptyCommentStartPosition - (long)expectedCommentLength; + ByteBuffer byteBuffer = ByteBuffer.allocate(4); + fileChannel.position(eocdStartPos); + fileChannel.read(byteBuffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + if (byteBuffer.getInt(0) == 101010256) { + ByteBuffer commentLengthByteBuffer = ByteBuffer.allocate(2); + fileChannel.position(eocdStartPos + 20L); + fileChannel.read(commentLengthByteBuffer); + commentLengthByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + int actualCommentLength = commentLengthByteBuffer.getShort(0); + if (actualCommentLength == expectedCommentLength) { + return actualCommentLength; + } + } + } + + throw new IOException("ZIP End of Central Directory (EOCD) record not found"); + } + } + + public static long findCentralDirStartOffset(FileChannel fileChannel) throws IOException { + return findCentralDirStartOffset(fileChannel, getCommentLength(fileChannel)); + } + + public static long findCentralDirStartOffset(FileChannel fileChannel, long commentLength) throws IOException { + ByteBuffer zipCentralDirectoryStart = ByteBuffer.allocate(4); + zipCentralDirectoryStart.order(ByteOrder.LITTLE_ENDIAN); + fileChannel.position(fileChannel.size() - commentLength - 6L); + fileChannel.read(zipCentralDirectoryStart); + return zipCentralDirectoryStart.getInt(0); + } + + public static Pair findApkSigningBlock(FileChannel fileChannel) throws IOException, SignatureNotFoundException { + long centralDirOffset = findCentralDirStartOffset(fileChannel); + return findApkSigningBlock(fileChannel, centralDirOffset); + } + + public static Pair findApkSigningBlock(FileChannel fileChannel, long centralDirOffset) throws IOException, SignatureNotFoundException { + if (centralDirOffset < 32L) { + throw new SignatureNotFoundException("APK too small for APK Signing Block. ZIP Central Directory offset: " + centralDirOffset); + } else { + fileChannel.position(centralDirOffset - 24L); + ByteBuffer footer = ByteBuffer.allocate(24); + fileChannel.read(footer); + footer.order(ByteOrder.LITTLE_ENDIAN); + if (footer.getLong(8) == 2334950737559900225L && footer.getLong(16) == 3617552046287187010L) { + long apkSigBlockSizeInFooter = footer.getLong(0); + if (apkSigBlockSizeInFooter >= (long)footer.capacity() && apkSigBlockSizeInFooter <= 2147483639L) { + int totalSize = (int)(apkSigBlockSizeInFooter + 8L); + long apkSigBlockOffset = centralDirOffset - (long)totalSize; + if (apkSigBlockOffset < 0L) { + throw new SignatureNotFoundException("APK Signing Block offset out of range: " + apkSigBlockOffset); + } else { + fileChannel.position(apkSigBlockOffset); + ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); + fileChannel.read(apkSigBlock); + apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); + long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); + if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { + throw new SignatureNotFoundException("APK Signing Block sizes in header and footer do not match: " + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); + } else { + return Pair.of(apkSigBlock, apkSigBlockOffset); + } + } + } else { + throw new SignatureNotFoundException("APK Signing Block size out of range: " + apkSigBlockSizeInFooter); + } + } else { + throw new SignatureNotFoundException("No APK Signing Block before ZIP Central Directory"); + } + } + } + + public static Map findIdValues(ByteBuffer apkSigningBlock) throws SignatureNotFoundException { + checkByteOrderLittleEndian(apkSigningBlock); + ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); + Map idValues = new LinkedHashMap(); + int entryCount = 0; + while(pairs.hasRemaining()) { + ++entryCount; + if (pairs.remaining() < 8) { + throw new SignatureNotFoundException("Insufficient data to read size of APK Signing Block entry #" + entryCount); + } + + long lenLong = pairs.getLong(); + if (lenLong < 4L || lenLong > 2147483647L) { + throw new SignatureNotFoundException("APK Signing Block entry #" + entryCount + " size out of range: " + lenLong); + } + + int len = (int)lenLong; + int nextEntryPos = pairs.position() + len; + if (len > pairs.remaining()) { + throw new SignatureNotFoundException("APK Signing Block entry #" + entryCount + " size out of range: " + len + ", available: " + pairs.remaining()); + } + int id = pairs.getInt(); + idValues.put(id, getByteBuffer(pairs, len - 4)); + pairs.position(nextEntryPos); + } + return idValues; + } + + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } else if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } else { + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } else { + int originalLimit = source.limit(); + int originalPosition = source.position(); + ByteBuffer var7; + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + var7 = result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + return var7; + } + } + } + + private static ByteBuffer getByteBuffer(ByteBuffer source, int size) throws BufferUnderflowException { + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } else { + int originalLimit = source.limit(); + int position = source.position(); + int limit = position + size; + if (limit >= position && limit <= originalLimit) { + source.limit(limit); + ByteBuffer var6; + try { + ByteBuffer result = source.slice(); + result.order(source.order()); + source.position(limit); + var6 = result; + } finally { + source.limit(originalLimit); + } + + return var6; + } else { + throw new BufferUnderflowException(); + } + } + } + + private static void checkByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/appwalle/ChannelReader.java b/app/src/main/java/com/cheng/bole/utils/appwalle/ChannelReader.java new file mode 100644 index 0000000..21555ed --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/appwalle/ChannelReader.java @@ -0,0 +1,61 @@ +package com.cheng.bole.utils.appwalle; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.File; + +/** + * 封装读取逻辑 + */ +public final class ChannelReader { + private ChannelReader() { + } + + /** + * 读取注入的内容 + * @param context + * @return + */ + public static String get(@NonNull Context context) { + String apkPath = getApkPath(context); + String raw = TextUtils.isEmpty(apkPath) ? null : getRaw(new File(apkPath)); + if (!TextUtils.isEmpty(raw)) { + raw = raw.replaceAll("\"", ""); + raw = raw.replaceAll("#", ""); + } + + try { + if (TextUtils.isEmpty(raw)) { + return ""; + } + return new String(Base64.decode(raw.getBytes(), Base64.NO_WRAP)); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + public static String getApkPath(@NonNull Context context) { + String apkPath = null; + try { + ApplicationInfo applicationInfo = context.getApplicationInfo(); + if (applicationInfo == null) { + return null; + } + apkPath = applicationInfo.sourceDir; + } catch (Exception var3) { + Log.d("异常", var3.getMessage()); + } + return apkPath; + } + + public static String getRaw(File apkFile) { + return PayloadReader.getString(apkFile, 1896449981); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/appwalle/Pair.java b/app/src/main/java/com/cheng/bole/utils/appwalle/Pair.java new file mode 100644 index 0000000..1a342ee --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/appwalle/Pair.java @@ -0,0 +1,53 @@ +package com.cheng.bole.utils.appwalle; + +final class Pair { + private final A mFirst; + private final B mSecond; + + private Pair(A first, B second) { + this.mFirst = first; + this.mSecond = second; + } + + public static Pair of(A first, B second) { + return new Pair(first, second); + } + + public A getFirst() { + return this.mFirst; + } + + public B getSecond() { + return this.mSecond; + } + + public int hashCode() { + int result = 1; + result = 31 * result + (this.mFirst == null ? 0 : this.mFirst.hashCode()); + result = 31 * result + (this.mSecond == null ? 0 : this.mSecond.hashCode()); + return result; + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (this.getClass() != obj.getClass()) { + return false; + } else { + Pair other = (Pair) obj; + if (this.mFirst == null) { + if (other.mFirst != null) { + return false; + } + } else if (!this.mFirst.equals(other.mFirst)) { + return false; + } + + if (this.mSecond == null) { + return other.mSecond == null; + } else return this.mSecond.equals(other.mSecond); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/appwalle/PayloadReader.java b/app/src/main/java/com/cheng/bole/utils/appwalle/PayloadReader.java new file mode 100644 index 0000000..98fc31d --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/appwalle/PayloadReader.java @@ -0,0 +1,88 @@ +package com.cheng.bole.utils.appwalle; + +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Arrays; +import java.util.Map; + +public final class PayloadReader { + private PayloadReader() { + } + + /** + * 读取指定ID的数据 + * + * @param apkFile + * @param id + * @return + */ + public static String getString(File apkFile, int id) { + byte[] bytes = get(apkFile, id); + if (bytes == null) { + return null; + } else { + try { + return new String(bytes, "UTF-8"); + } catch (UnsupportedEncodingException var4) { + var4.printStackTrace(); + return null; + } + } + } + + public static byte[] get(File apkFile, int id) { + Map idValues = getAll(apkFile); + if (idValues == null) { + return null; + } else { + ByteBuffer byteBuffer = (ByteBuffer) idValues.get(id); + return byteBuffer == null ? null : getBytes(byteBuffer); + } + } + + private static byte[] getBytes(ByteBuffer byteBuffer) { + byte[] array = byteBuffer.array(); + int arrayOffset = byteBuffer.arrayOffset(); + return Arrays.copyOfRange(array, arrayOffset + byteBuffer.position(), arrayOffset + byteBuffer.limit()); + } + + private static Map getAll(File apkFile) { + Map idValues = null; + try { + RandomAccessFile randomAccessFile = null; + FileChannel fileChannel = null; + try { + randomAccessFile = new RandomAccessFile(apkFile, "r"); + fileChannel = randomAccessFile.getChannel(); + ByteBuffer apkSigningBlock2 = (ByteBuffer) ApkUtil.findApkSigningBlock(fileChannel).getFirst(); + idValues = ApkUtil.findIdValues(apkSigningBlock2); + } catch (IOException var18) { + Log.d("异常", var18.getMessage()); + } finally { + try { + if (fileChannel != null) { + fileChannel.close(); + } + } catch (IOException var17) { + Log.d("异常", var17.getMessage()); + } + try { + if (randomAccessFile != null) { + randomAccessFile.close(); + } + } catch (IOException var16) { + Log.d("异常", var16.getMessage()); + } + } + } catch (SignatureNotFoundException var20) { + Log.d("异常", var20.getMessage()); + } + return idValues; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/appwalle/SignatureNotFoundException.java b/app/src/main/java/com/cheng/bole/utils/appwalle/SignatureNotFoundException.java new file mode 100644 index 0000000..3bdd344 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/appwalle/SignatureNotFoundException.java @@ -0,0 +1,9 @@ +package com.cheng.bole.utils.appwalle; + +public class SignatureNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public SignatureNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/pay/AuthResult.java b/app/src/main/java/com/cheng/bole/utils/pay/AuthResult.java new file mode 100644 index 0000000..6b6217f --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/pay/AuthResult.java @@ -0,0 +1 @@ +package com.cheng.bole.utils.pay; import android.text.TextUtils; import java.util.Map; public class AuthResult { private String resultStatus; private String result; private String memo; private String resultCode; private String authCode; private String alipayOpenId; public AuthResult(Map rawResult, boolean removeBrackets) { if (rawResult == null) { return; } for (String key : rawResult.keySet()) { if (TextUtils.equals(key, "resultStatus")) { resultStatus = rawResult.get(key); } else if (TextUtils.equals(key, "result")) { result = rawResult.get(key); } else if (TextUtils.equals(key, "memo")) { memo = rawResult.get(key); } } String[] resultValue = result.split("&"); for (String value : resultValue) { if (value.startsWith("alipay_open_id")) { alipayOpenId = removeBrackets(getValue("alipay_open_id=", value), removeBrackets); continue; } if (value.startsWith("auth_code")) { authCode = removeBrackets(getValue("auth_code=", value), removeBrackets); continue; } if (value.startsWith("result_code")) { resultCode = removeBrackets(getValue("result_code=", value), removeBrackets); continue; } } } private String removeBrackets(String str, boolean remove) { if (remove) { if (!TextUtils.isEmpty(str)) { if (str.startsWith("\"")) { str = str.replaceFirst("\"", ""); } if (str.endsWith("\"")) { str = str.substring(0, str.length() - 1); } } } return str; } @Override public String toString() { return "authCode={" + authCode + "}; resultStatus={" + resultStatus + "}; memo={" + memo + "}; result={" + result + "}"; } private String getValue(String header, String data) { return data.substring(header.length(), data.length()); } /** * @return the resultStatus */ public String getResultStatus() { return resultStatus; } /** * @return the memo */ public String getMemo() { return memo; } /** * @return the result */ public String getResult() { return result; } /** * @return the resultCode */ public String getResultCode() { return resultCode; } /** * @return the authCode */ public String getAuthCode() { return authCode; } /** * @return the alipayOpenId */ public String getAlipayOpenId() { return alipayOpenId; } } \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/pay/Base64.java b/app/src/main/java/com/cheng/bole/utils/pay/Base64.java new file mode 100644 index 0000000..5216273 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/pay/Base64.java @@ -0,0 +1,268 @@ +package com.cheng.bole.utils.pay; + +public final class Base64 { + + private static final int BASELENGTH = 128; + private static final int LOOKUPLENGTH = 64; + private static final int TWENTYFOURBITGROUP = 24; + private static final int EIGHTBIT = 8; + private static final int SIXTEENBIT = 16; + private static final int FOURBYTE = 4; + private static final int SIGN = -128; + private static char PAD = '='; + private static byte[] base64Alphabet = new byte[BASELENGTH]; + private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; ++i) { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData + * Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 + : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) + : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded + * string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) + || !isData((d4 = base64Data[dataIndex++]))) { + return null; + }// if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++]))) { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data + * the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/app/src/main/java/com/cheng/bole/utils/pay/OrderInfoUtil2_0.java b/app/src/main/java/com/cheng/bole/utils/pay/OrderInfoUtil2_0.java new file mode 100644 index 0000000..31b8a67 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/pay/OrderInfoUtil2_0.java @@ -0,0 +1,192 @@ +package com.cheng.bole.utils.pay; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; + +/** + * 2.0 订单串本地签名逻辑 + * 注意:本 Demo 仅作为展示用途,实际项目中不能将 RSA_PRIVATE 和签名逻辑放在客户端进行! + */ + +public class OrderInfoUtil2_0 { + + /** + * 构造授权参数列表 + * + * @param pid + * @param app_id + * @param target_id + * @return + */ + public static Map buildAuthInfoMap(String pid, String app_id, String target_id, boolean rsa2) { + Map keyValues = new HashMap(); + + // 商户签约拿到的app_id,如:2013081700024223 + keyValues.put("app_id", app_id); + + // 商户签约拿到的pid,如:2088102123816631 + keyValues.put("pid", pid); + + // 服务接口名称, 固定值 + keyValues.put("apiname", "com.alipay.account.auth"); + + // 服务接口名称, 固定值 + keyValues.put("methodname", "alipay.open.auth.sdk.code.get"); + + // 商户类型标识, 固定值 + keyValues.put("app_name", "mc"); + + // 业务类型, 固定值 + keyValues.put("biz_type", "openservice"); + + // 产品码, 固定值 + keyValues.put("product_id", "APP_FAST_LOGIN"); + + // 授权范围, 固定值 + keyValues.put("scope", "kuaijie"); + + // 商户唯一标识,如:kkkkk091125 + keyValues.put("target_id", target_id); + + // 授权类型, 固定值 + keyValues.put("auth_type", "AUTHACCOUNT"); + + // 签名类型 + keyValues.put("sign_type", rsa2 ? "RSA2" : "RSA"); + + return keyValues; + } + + /** + * 构造支付订单参数列表 + */ + public static Map buildOrderParamMap(String app_id, boolean rsa2) { + Map keyValues = new HashMap(); + + keyValues.put("app_id", app_id); + + keyValues.put("biz_content", "{\"timeout_express\":\"30m\",\"product_code\":\"QUICK_MSECURITY_PAY\",\"total_amount\":\"0.01\",\"subject\":\"1\",\"body\":\"我是测试数据\",\"out_trade_no\":\"" + getOutTradeNo() + "\"}"); + + keyValues.put("charset", "utf-8"); + + keyValues.put("method", "alipay.trade.app.pay"); + + keyValues.put("sign_type", rsa2 ? "RSA2" : "RSA"); + + keyValues.put("timestamp", "2016-07-29 16:55:53"); + + keyValues.put("version", "1.0"); + + return keyValues; + } + + /** + * 构造支付订单参数信息 + * + * @param map + * 支付订单参数 + * @return + */ + public static String buildOrderParam(Map map) { + List keys = new ArrayList(map.keySet()); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keys.size() - 1; i++) { + String key = keys.get(i); + String value = map.get(key); + sb.append(buildKeyValue(key, value, true)); + sb.append("&"); + } + + String tailKey = keys.get(keys.size() - 1); + String tailValue = map.get(tailKey); + sb.append(buildKeyValue(tailKey, tailValue, true)); + + return sb.toString(); + } + + /** + * 拼接键值对 + * + * @param key + * @param value + * @param isEncode + * @return + */ + private static String buildKeyValue(String key, String value, boolean isEncode) { + StringBuilder sb = new StringBuilder(); + sb.append(key); + sb.append("="); + if (isEncode) { + try { + sb.append(URLEncoder.encode(value, "UTF-8")); + } catch (UnsupportedEncodingException e) { + sb.append(value); + } + } else { + sb.append(value); + } + return sb.toString(); + } + + /** + * 对支付参数信息进行签名 + * + * @param map + * 待签名授权信息 + * + * @return + */ + public static String getSign(Map map, String rsaKey, boolean rsa2) { + List keys = new ArrayList(map.keySet()); + // key排序 + Collections.sort(keys); + + StringBuilder authInfo = new StringBuilder(); + for (int i = 0; i < keys.size() - 1; i++) { + String key = keys.get(i); + String value = map.get(key); + authInfo.append(buildKeyValue(key, value, false)); + authInfo.append("&"); + } + + String tailKey = keys.get(keys.size() - 1); + String tailValue = map.get(tailKey); + authInfo.append(buildKeyValue(tailKey, tailValue, false)); + + String oriSign = SignUtils.sign(authInfo.toString(), rsaKey, rsa2); + String encodedSign = ""; + + try { + encodedSign = URLEncoder.encode(oriSign, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return "sign=" + encodedSign; + } + + /** + * 要求外部订单号必须唯一。 + * @return + */ + private static String getOutTradeNo() { + SimpleDateFormat format = new SimpleDateFormat("MMddHHmmss", Locale.getDefault()); + Date date = new Date(); + String key = format.format(date); + + Random r = new Random(); + key = key + r.nextInt(); + key = key.substring(0, 15); + return key; + } + +} diff --git a/app/src/main/java/com/cheng/bole/utils/pay/PayResult.java b/app/src/main/java/com/cheng/bole/utils/pay/PayResult.java new file mode 100644 index 0000000..d7977ed --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/pay/PayResult.java @@ -0,0 +1 @@ +package com.cheng.bole.utils.pay; import android.text.TextUtils; import java.util.Map; public class PayResult { private String resultStatus; private String result; private String memo; public PayResult(Map rawResult) { if (rawResult == null) { return; } for (String key : rawResult.keySet()) { if (TextUtils.equals(key, "resultStatus")) { resultStatus = rawResult.get(key); } else if (TextUtils.equals(key, "result")) { result = rawResult.get(key); } else if (TextUtils.equals(key, "memo")) { memo = rawResult.get(key); } } } @Override public String toString() { return "resultStatus={" + resultStatus + "};memo={" + memo + "};result={" + result + "}"; } /** * @return the resultStatus */ public String getResultStatus() { return resultStatus; } /** * @return the memo */ public String getMemo() { return memo; } /** * @return the result */ public String getResult() { return result; } } \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/utils/pay/PayUtils.kt b/app/src/main/java/com/cheng/bole/utils/pay/PayUtils.kt new file mode 100644 index 0000000..a0f6eed --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/pay/PayUtils.kt @@ -0,0 +1,115 @@ +package com.cheng.bole.utils.pay + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.os.Handler +import android.os.Message +import android.text.TextUtils +import com.alipay.sdk.app.PayTask +import com.cheng.bole.common.Constants +import com.example.base.common.RxBus +import com.example.base.extensions.toast +import com.example.base.utils.L +import com.tencent.mm.opensdk.modelpay.PayReq +import com.tencent.mm.opensdk.openapi.WXAPIFactory + +@SuppressLint("StaticFieldLeak") +object PayUtils { + + var context: Context? = null + + /** + * 微信支付 + * + * @param result + */ + fun toWXPay(mActivity: Activity, data: com.cheng.bole.bean.OrderPayEntity) { + if (data == null) { + toast("未获取到支付信息") + return + } + val api = WXAPIFactory.createWXAPI(mActivity, Constants.WechatAppId) + val request = PayReq() + request.appId = data.appId + request.partnerId = data.partnerId + request.prepayId = data.prepayId + request.packageValue = data.`package` + request.nonceStr = data.nonceStr + request.timeStamp = data.timeStamp + request.sign = data.sign + api.sendReq(request) + } + + /** + * 支付宝 + */ + fun toAliPay(mActivity: Activity, res: String, orderId: String) { + context = mActivity + val payRunnable = Runnable { + val alipay = PayTask(mActivity) + val result = alipay.payV2(res, true) + val msg = Message() + msg.what = 1001//支付宝返回参数 + msg.obj = result +// val bundle = Bundle() +// bundle.putString("orderId", orderId) +// msg.data = bundle + mHandler.sendMessage(msg) + } + // 必须异步调用 + val payThread = Thread(payRunnable) + payThread.start() + } + + /** + * 支付宝状态 + * 9000 订单支付成功 + * 8000 正在处理中,支付结果未知(有可能已经支付成功),请查询商户订单列表中订单的支付状态 + * 4000 订单支付失败 + * 5000 重复请求 + * 6001 用户中途取消 + * 6002 网络连接出错 + * 6004 支付结果未知(有可能已经支付成功),请查询商户订单列表中订单的支付状态 + * 其它 其它支付错误 + */ + @SuppressLint("HandlerLeak") + private val mHandler: Handler = object : Handler() { + override fun handleMessage(msg: Message) { + when (msg.what) { + 1001 -> {//支付宝返回参数 + val payResult = com.cheng.bole.utils.pay.PayResult(msg.obj as MutableMap?) + + /** + * 对于支付结果,请商户依赖服务端的异步通知结果。同步通知结果,仅作为支付结束的通知。 + */ + //{resultStatus=4000, result=, memo=系统繁忙,请稍后再试} + val resultInfo = payResult.result // 同步返回需要验证的信息 + val resultStatus = payResult.resultStatus + // 判断resultStatus 为9000则代表支付成功 + when { + TextUtils.equals(resultStatus, "9000") -> { + // 该笔订单是否真实支付成功,需要依赖服务端的异步通知。 + toast("支付成功") + RxBus.defaultInstance.post(com.cheng.bole.event.PayStatusEvent(com.cheng.bole.event.PayStatusEnum.PAY_SUCCESS)) + } + + TextUtils.equals(resultStatus, "6001") -> { + // 该笔订单是否真实支付成功,需要依赖服务端的异步通知。 + toast("支付取消") + RxBus.defaultInstance.post(com.cheng.bole.event.PayStatusEvent(com.cheng.bole.event.PayStatusEnum.PAY_CANCEL)) + } + + else -> { + // 该笔订单真实的支付结果,需要依赖服务端的异步通知。 + toast("支付失败") + L.d("resultStatus=${resultStatus}") + RxBus.defaultInstance.post(com.cheng.bole.event.PayStatusEvent(com.cheng.bole.event.PayStatusEnum.PAY_ERROR, resultInfo)) + } + } + } + } + } + } + +} diff --git a/app/src/main/java/com/cheng/bole/utils/pay/SignUtils.java b/app/src/main/java/com/cheng/bole/utils/pay/SignUtils.java new file mode 100644 index 0000000..e0f33de --- /dev/null +++ b/app/src/main/java/com/cheng/bole/utils/pay/SignUtils.java @@ -0,0 +1,44 @@ +package com.cheng.bole.utils.pay; + +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; + +public class SignUtils { + + private static final String ALGORITHM = "RSA"; + + private static final String SIGN_ALGORITHMS = "SHA1WithRSA"; + + private static final String SIGN_SHA256RSA_ALGORITHMS = "SHA256WithRSA"; + + private static final String DEFAULT_CHARSET = "UTF-8"; + + private static String getAlgorithms(boolean rsa2) { + return rsa2 ? SIGN_SHA256RSA_ALGORITHMS : SIGN_ALGORITHMS; + } + + public static String sign(String content, String privateKey, boolean rsa2) { + try { + PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec( + Base64.decode(privateKey)); + KeyFactory keyf = KeyFactory.getInstance(ALGORITHM); + PrivateKey priKey = keyf.generatePrivate(priPKCS8); + + java.security.Signature signature = java.security.Signature + .getInstance(getAlgorithms(rsa2)); + + signature.initSign(priKey); + signature.update(content.getBytes(DEFAULT_CHARSET)); + + byte[] signed = signature.sign(); + + return Base64.encode(signed); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + +} diff --git a/app/src/main/java/com/cheng/bole/widget/CommonShapeView.kt b/app/src/main/java/com/cheng/bole/widget/CommonShapeView.kt new file mode 100644 index 0000000..16dc59e --- /dev/null +++ b/app/src/main/java/com/cheng/bole/widget/CommonShapeView.kt @@ -0,0 +1,361 @@ +package com.cheng.bole.widget + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.StateListDrawable +import android.os.Build +import android.util.AttributeSet +import android.view.Gravity +import androidx.appcompat.widget.AppCompatButton +import com.cheng.bole.R + +class CommonShapeView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatButton(context, attrs, defStyleAttr) { + + private companion object { + val TOP_LEFT = 1 + val TOP_RIGHT = 2 + val BOTTOM_RIGHT = 4 + val BOTTOM_LEFT = 8 + + val LEFT = 0 + val TOP = 1 + val RIGHT = 2 + val BOTTOM = 3 + } + + /** + * shape模式 + * 矩形(rectangle)、椭圆形(oval)、线形(line)、环形(ring) + */ + private var mShapeMode = 0 + + /** + * 填充颜色 + */ + private var mFillColor = 0 + + /** + * 按压颜色 + */ + private var mPressedColor = 0 + + /** + * 描边颜色 + */ + private var mStrokeColor = 0 + + /** + * 描边宽度 + */ + private var mStrokeWidth = 0 + + /** + * 圆角半径 + */ + private var mCornerRadius = 0 + + /** + * 圆角位置 + */ + private var mCornerPosition = 0 + + /** + * 点击动效 + */ + private var mActiveEnable = false + + /** + * 起始颜色 + */ + private var mStartColor = 0 + + /** + * 结束颜色 + */ + private var mEndColor = 0 + + /** + * 渐变方向 + * 0-GradientDrawable.Orientation.TOP_BOTTOM + * 1-GradientDrawable.Orientation.LEFT_RIGHT + */ + private var mOrientation = 0 + + /** + * drawable位置 + * -1-null、0-left、1-top、2-right、3-bottom + */ + private var mDrawablePosition = -1 + + /** + * 普通shape样式 + */ + private val normalGradientDrawable: GradientDrawable by lazy { GradientDrawable() } + + /** + * 按压shape样式 + */ + private val pressedGradientDrawable: GradientDrawable by lazy { GradientDrawable() } + + /** + * shape样式集合 + */ + private val stateListDrawable: StateListDrawable by lazy { StateListDrawable() } + + // button内容总宽度 + private var contentWidth = 0f + + // button内容总高度 + private var contentHeight = 0f + + init { + context.obtainStyledAttributes(attrs, R.styleable.CommonShapeView).apply { + mShapeMode = getInt(R.styleable.CommonShapeView_csb_shapeMode, 0) + mFillColor = getColor(R.styleable.CommonShapeView_csb_fillColor, 0xFFFFFFFF.toInt()) + mPressedColor = getColor(R.styleable.CommonShapeView_csb_pressedColor, 0xFF999999.toInt()) + mStrokeColor = getColor(R.styleable.CommonShapeView_csb_strokeColor, 0) + mStrokeWidth = getDimensionPixelSize(R.styleable.CommonShapeView_csb_strokeWidth, 0) + mCornerRadius = getDimensionPixelSize(R.styleable.CommonShapeView_csb_cornerRadius, 0) + mCornerPosition = getInt(R.styleable.CommonShapeView_csb_cornerPosition, -1) + mActiveEnable = getBoolean(R.styleable.CommonShapeView_csb_activeEnable, false) + mDrawablePosition = getInt(R.styleable.CommonShapeView_csb_drawablePosition, -1) + mStartColor = getColor(R.styleable.CommonShapeView_csb_startColor, 0xFFFFFFFF.toInt()) + mEndColor = getColor(R.styleable.CommonShapeView_csb_endColor, 0xFFFFFFFF.toInt()) + mOrientation = getColor(R.styleable.CommonShapeView_csb_orientation, 0) + recycle() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + // 初始化normal状态 + with(normalGradientDrawable) { + // 渐变色 + if (mStartColor != 0xFFFFFFFF.toInt() && mEndColor != 0xFFFFFFFF.toInt()) { + colors = intArrayOf(mStartColor, mEndColor) + when (mOrientation) { + 0 -> orientation = GradientDrawable.Orientation.TOP_BOTTOM + 1 -> orientation = GradientDrawable.Orientation.LEFT_RIGHT + } + } + // 填充色 + else { + setColor(mFillColor) + } + when (mShapeMode) { + 0 -> shape = GradientDrawable.RECTANGLE + 1 -> shape = GradientDrawable.OVAL + 2 -> shape = GradientDrawable.LINE + 3 -> shape = GradientDrawable.RING + } + // 统一设置圆角半径 + if (mCornerPosition == -1) { + cornerRadius = mCornerRadius.toFloat() + } + // 根据圆角位置设置圆角半径 + else { + cornerRadii = getCornerRadiusByPosition() + } + // 默认的透明边框不绘制,否则会导致没有阴影 + if (mStrokeColor != 0) { + setStroke(mStrokeWidth, mStrokeColor) + } + } + + // 是否开启点击动效 + background = if (mActiveEnable) { + // 5.0以上水波纹效果 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + RippleDrawable(ColorStateList.valueOf(mPressedColor), normalGradientDrawable, null) + } + // 5.0以下变色效果 + else { + // 初始化pressed状态 + with(pressedGradientDrawable) { + setColor(mPressedColor) + when (mShapeMode) { + 0 -> shape = GradientDrawable.RECTANGLE + 1 -> shape = GradientDrawable.OVAL + 2 -> shape = GradientDrawable.LINE + 3 -> shape = GradientDrawable.RING + } + cornerRadius = mCornerRadius.toFloat() + setStroke(mStrokeWidth, mStrokeColor) + } + + // 注意此处的add顺序,normal必须在最后一个,否则其他状态无效 + // 设置pressed状态 + stateListDrawable.apply { + addState(intArrayOf(android.R.attr.state_pressed), pressedGradientDrawable) + // 设置normal状态 + addState(intArrayOf(), normalGradientDrawable) + } + } + } else { + normalGradientDrawable + } + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + // 如果xml中配置了drawable则设置padding让文字移动到边缘与drawable靠在一起 + // button中配置的drawable默认贴着边缘 + if (mDrawablePosition > -1) { + compoundDrawables.let { + val drawable: Drawable? = compoundDrawables[mDrawablePosition] + drawable?.let { + // 图片间距 + val drawablePadding = compoundDrawablePadding + when (mDrawablePosition) { + // 左右drawable + 0, 2 -> { + // 图片宽度 + val drawableWidth = it.intrinsicWidth + // 获取文字宽度 + val textWidth = paint.measureText(text.toString()) + // 内容总宽度 + contentWidth = textWidth + drawableWidth + drawablePadding + val rightPadding = (width - contentWidth).toInt() + // 图片和文字全部靠在左侧 + setPadding(0, 0, rightPadding, 0) + } + // 上下drawable + 1, 3 -> { + // 图片高度 + val drawableHeight = it.intrinsicHeight + // 获取文字高度 + val fm = paint.fontMetrics + // 单行高度 + val singeLineHeight = Math.ceil(fm.descent.toDouble() - fm.ascent.toDouble()).toFloat() + // 总的行间距 + val totalLineSpaceHeight = (lineCount - 1) * lineSpacingExtra + val textHeight = singeLineHeight * lineCount + totalLineSpaceHeight + // 内容总高度 + contentHeight = textHeight + drawableHeight + drawablePadding + // 图片和文字全部靠在上侧 + val bottomPadding = (height - contentHeight).toInt() + setPadding(0, 0, 0, bottomPadding) + } + } + } + + } + } + // 内容居中 + gravity = Gravity.CENTER + // 可点击 + if (mActiveEnable) { + isClickable = true + } + changeTintContextWrapperToActivity() + } + + override fun onDraw(canvas: Canvas) { + // 让图片和文字居中 + when { + contentWidth > 0 && (mDrawablePosition == 0 || mDrawablePosition == 2) -> canvas.translate((width - contentWidth) / 2, 0f) + contentHeight > 0 && (mDrawablePosition == 1 || mDrawablePosition == 3) -> canvas.translate(0f, (height - contentHeight) / 2) + } + super.onDraw(canvas) + } + + /** + * 从support23.3.0开始View中的getContext方法返回的是TintContextWrapper而不再是Activity + * 如果使用xml注册onClick属性,就会通过反射到Activity中去找对应的方法 + * 5.0以下系统会反射到TintContextWrapper中去找对应的方法,程序直接crash + * 所以这里需要针对5.0以下系统单独处理View中的getContext返回值 + */ + private fun changeTintContextWrapperToActivity() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + getActivity()?.let { + var clazz: Class<*>? = this::class.java + while (clazz != null) { + try { + val field = clazz.getDeclaredField("mContext") + field.isAccessible = true + field.set(this, it) + break + } catch (e: Exception) { + e.printStackTrace() + } + clazz = clazz.superclass + } + } + } + } + + /** + * 从context中得到真正的activity + */ + private fun getActivity(): Activity? { + var context = context + while (context is ContextWrapper) { + if (context is Activity) { + return context + } + context = context.baseContext + } + return null + } + + /** + * 根据圆角位置获取圆角半径 + */ + private fun getCornerRadiusByPosition(): FloatArray { + val result = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) + val cornerRadius = mCornerRadius.toFloat() + if (containsFlag(mCornerPosition, TOP_LEFT)) { + result[0] = cornerRadius + result[1] = cornerRadius + } + if (containsFlag(mCornerPosition, TOP_RIGHT)) { + result[2] = cornerRadius + result[3] = cornerRadius + } + if (containsFlag(mCornerPosition, BOTTOM_RIGHT)) { + result[4] = cornerRadius + result[5] = cornerRadius + } + if (containsFlag(mCornerPosition, BOTTOM_LEFT)) { + result[6] = cornerRadius + result[7] = cornerRadius + } + return result + } + + /** + * 是否包含对应flag + * 按位或 + */ + private fun containsFlag(flagSet: Int, flag: Int): Boolean { + return flagSet or flag == flagSet + } + + fun setStartColor(color: Int) { + mStartColor = color + } + + fun setEndColor(color: Int) { + mEndColor = color + } + + fun setStrokeColor(color: Int){ + mStrokeColor = color + } + + fun setBgColor(color: Int) { + mFillColor = color + postInvalidate() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/widget/NoScrollViewPager.kt b/app/src/main/java/com/cheng/bole/widget/NoScrollViewPager.kt new file mode 100644 index 0000000..04453d5 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/widget/NoScrollViewPager.kt @@ -0,0 +1,26 @@ +package com.cheng.bole.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.viewpager.widget.ViewPager + +class NoScrollViewPager @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) { + private var scrollable: Boolean = false + + fun setScrollable(scrollable: Boolean) { + this.scrollable = scrollable + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + if (!scrollable) return false + return super.onInterceptTouchEvent(ev) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent?): Boolean { + if (!scrollable) return false + return super.onTouchEvent(ev) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/widget/StrokeTextView.kt b/app/src/main/java/com/cheng/bole/widget/StrokeTextView.kt new file mode 100644 index 0000000..b589bc4 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/widget/StrokeTextView.kt @@ -0,0 +1,70 @@ +package com.cheng.bole.widget + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.ViewGroup +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatTextView + + +class StrokeTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatTextView(context, attrs, defStyleAttr) { + private val outlineTextView = AppCompatTextView(context, attrs, defStyleAttr) + + init { + initView() + } + + private fun initView() { + val paint = outlineTextView.paint + paint.strokeWidth = 3f //描边宽度 + + paint.style = Paint.Style.STROKE + outlineTextView.setTextColor(Color.WHITE) //描边颜色 + outlineTextView.setGravity(gravity) + } + + override fun setLayoutParams(params: ViewGroup.LayoutParams?) { + super.setLayoutParams(params) + outlineTextView.setLayoutParams(params) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + //设置轮廓文字 + val outlineText = outlineTextView.getText() + if (outlineText == null || outlineText != getText()) { + outlineTextView.text = getText() + postInvalidate() + } + outlineTextView.measure(widthMeasureSpec, heightMeasureSpec) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + outlineTextView.layout(left, top, right, bottom) + } + + override fun onDraw(canvas: Canvas) { + outlineTextView.typeface = typeface + outlineTextView.draw(canvas) + super.onDraw(canvas) + } + + fun setStrokeColor(@ColorInt color: Int) { + outlineTextView.setTextColor(color) + postInvalidate() + } + + fun setStrokeWidth(width: Float) { + outlineTextView.paint.strokeWidth = width + postInvalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/wxapi/WXEntryActivity.kt b/app/src/main/java/com/cheng/bole/wxapi/WXEntryActivity.kt new file mode 100644 index 0000000..4ba43f2 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/wxapi/WXEntryActivity.kt @@ -0,0 +1,28 @@ +package com.cheng.bole.wxapi + +import com.example.base.common.RxBus +import com.tencent.mm.opensdk.constants.ConstantsAPI +import com.tencent.mm.opensdk.modelbase.BaseResp +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.umeng.socialize.weixin.view.WXCallbackActivity +import com.cheng.bole.event.WxLoginEvent + + +class WXEntryActivity : WXCallbackActivity() { + + override fun onResp(resp: BaseResp?) { + if (resp?.type == ConstantsAPI.COMMAND_SENDAUTH) { + when (resp.errCode) { + BaseResp.ErrCode.ERR_OK -> { + val authResp = resp as SendAuth.Resp + RxBus.defaultInstance.post(WxLoginEvent(authResp.code, authResp.state)) + } + + else -> RxBus.defaultInstance.post(WxLoginEvent("", "")) + } + finish() + } else { + finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cheng/bole/wxapi/WXPayEntryActivity.kt b/app/src/main/java/com/cheng/bole/wxapi/WXPayEntryActivity.kt new file mode 100644 index 0000000..507fbf8 --- /dev/null +++ b/app/src/main/java/com/cheng/bole/wxapi/WXPayEntryActivity.kt @@ -0,0 +1,70 @@ +package com.cheng.bole.wxapi + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.cheng.bole.common.Constants +import com.cheng.bole.event.PayStatusEnum +import com.cheng.bole.event.PayStatusEvent +import com.example.base.common.RxBus +import com.example.base.extensions.toast +import com.tencent.mm.opensdk.constants.ConstantsAPI +import com.tencent.mm.opensdk.modelbase.BaseReq +import com.tencent.mm.opensdk.modelbase.BaseResp +import com.tencent.mm.opensdk.modelbiz.WXLaunchMiniProgram +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler +import com.tencent.mm.opensdk.openapi.WXAPIFactory + +class WXPayEntryActivity : AppCompatActivity(), IWXAPIEventHandler { + + private var api: IWXAPI? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + api = WXAPIFactory.createWXAPI(this, Constants.WechatAppId) + api?.handleIntent(intent, this) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + api?.handleIntent(intent, this) + } + + override fun onReq(p0: BaseReq?) { + + } + + override fun onResp(resp: BaseResp) { + if (resp.type == ConstantsAPI.COMMAND_LAUNCH_WX_MINIPROGRAM) { + val launchMiniProgramResp = resp as WXLaunchMiniProgram.Resp + val text = String.format( + "openid=%s\nextMsg=%s\nerrStr=%s", + launchMiniProgramResp.openId, launchMiniProgramResp.extMsg, launchMiniProgramResp.errStr + ) + toast(text) + } + if (resp.type == ConstantsAPI.COMMAND_PAY_BY_WX) { + when (resp.errCode) { + 0 -> { + RxBus.defaultInstance.post(PayStatusEvent(PayStatusEnum.PAY_SUCCESS)) + finish() + } + + -2 -> { + RxBus.defaultInstance.post(PayStatusEvent(PayStatusEnum.PAY_CANCEL)) + finish() + } + + else -> { + RxBus.defaultInstance.post(PayStatusEvent(PayStatusEnum.PAY_ERROR, resp.errStr ?: "")) + finish() + } + } + } else { + finish() + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/anim/button_click.xml b/app/src/main/res/anim/button_click.xml new file mode 100644 index 0000000..fc198b9 --- /dev/null +++ b/app/src/main/res/anim/button_click.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/dialog_enter.xml b/app/src/main/res/anim/dialog_enter.xml new file mode 100644 index 0000000..86d147c --- /dev/null +++ b/app/src/main/res/anim/dialog_enter.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/dialog_exit.xml b/app/src/main/res/anim/dialog_exit.xml new file mode 100644 index 0000000..7660b9d --- /dev/null +++ b/app/src/main/res/anim/dialog_exit.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/focusview_show.xml b/app/src/main/res/anim/focusview_show.xml new file mode 100644 index 0000000..dcc95dc --- /dev/null +++ b/app/src/main/res/anim/focusview_show.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_left_in.xml b/app/src/main/res/anim/slide_left_in.xml new file mode 100644 index 0000000..4119d19 --- /dev/null +++ b/app/src/main/res/anim/slide_left_in.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_left_out.xml b/app/src/main/res/anim/slide_left_out.xml new file mode 100644 index 0000000..94f132a --- /dev/null +++ b/app/src/main/res/anim/slide_left_out.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_right_in.xml b/app/src/main/res/anim/slide_right_in.xml new file mode 100644 index 0000000..d95b55e --- /dev/null +++ b/app/src/main/res/anim/slide_right_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_right_out.xml b/app/src/main/res/anim/slide_right_out.xml new file mode 100644 index 0000000..e2ba4c8 --- /dev/null +++ b/app/src/main/res/anim/slide_right_out.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/exo_styled_controls_pause.png b/app/src/main/res/drawable/exo_styled_controls_pause.png new file mode 100644 index 0000000..b219f83 Binary files /dev/null and b/app/src/main/res/drawable/exo_styled_controls_pause.png differ diff --git a/app/src/main/res/drawable/exo_styled_controls_play.png b/app/src/main/res/drawable/exo_styled_controls_play.png new file mode 100644 index 0000000..899637a Binary files /dev/null and b/app/src/main/res/drawable/exo_styled_controls_play.png differ diff --git a/app/src/main/res/drawable/ic_back_black.xml b/app/src/main/res/drawable/ic_back_black.xml new file mode 100644 index 0000000..605be83 --- /dev/null +++ b/app/src/main/res/drawable/ic_back_black.xml @@ -0,0 +1,9 @@ + + + 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..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/layer_placeholder.xml b/app/src/main/res/drawable/layer_placeholder.xml new file mode 100644 index 0000000..b7118b6 --- /dev/null +++ b/app/src/main/res/drawable/layer_placeholder.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/layer_progress_z_grey_28.xml b/app/src/main/res/drawable/layer_progress_z_grey_28.xml new file mode 100644 index 0000000..ececf3f --- /dev/null +++ b/app/src/main/res/drawable/layer_progress_z_grey_28.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/layer_splash.xml b/app/src/main/res/drawable/layer_splash.xml new file mode 100644 index 0000000..eb596ed --- /dev/null +++ b/app/src/main/res/drawable/layer_splash.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_default_check.xml b/app/src/main/res/drawable/selector_default_check.xml new file mode 100644 index 0000000..194dc8a --- /dev/null +++ b/app/src/main/res/drawable/selector_default_check.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_feedback_text_color.xml b/app/src/main/res/drawable/selector_feedback_text_color.xml new file mode 100644 index 0000000..2052438 --- /dev/null +++ b/app/src/main/res/drawable/selector_feedback_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_feedback_type_bg.xml b/app/src/main/res/drawable/selector_feedback_type_bg.xml new file mode 100644 index 0000000..b35aecb --- /dev/null +++ b/app/src/main/res/drawable/selector_feedback_type_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_home_navigation_icon.xml b/app/src/main/res/drawable/selector_home_navigation_icon.xml new file mode 100644 index 0000000..10a94a3 --- /dev/null +++ b/app/src/main/res/drawable/selector_home_navigation_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_main_navigation_color.xml b/app/src/main/res/drawable/selector_main_navigation_color.xml new file mode 100644 index 0000000..c7f95b7 --- /dev/null +++ b/app/src/main/res/drawable/selector_main_navigation_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_mine_navigation_icon.xml b/app/src/main/res/drawable/selector_mine_navigation_icon.xml new file mode 100644 index 0000000..06b54b0 --- /dev/null +++ b/app/src/main/res/drawable/selector_mine_navigation_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_10466afd_cor4.xml b/app/src/main/res/drawable/shape_10466afd_cor4.xml new file mode 100644 index 0000000..c8bbfb6 --- /dev/null +++ b/app/src/main/res/drawable/shape_10466afd_cor4.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_10d8d8d8_cor2.xml b/app/src/main/res/drawable/shape_10d8d8d8_cor2.xml new file mode 100644 index 0000000..b687e03 --- /dev/null +++ b/app/src/main/res/drawable/shape_10d8d8d8_cor2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_10fb7528_cor2.xml b/app/src/main/res/drawable/shape_10fb7528_cor2.xml new file mode 100644 index 0000000..f8b1967 --- /dev/null +++ b/app/src/main/res/drawable/shape_10fb7528_cor2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_10ff4c4c_cor2.xml b/app/src/main/res/drawable/shape_10ff4c4c_cor2.xml new file mode 100644 index 0000000..1bf2c9d --- /dev/null +++ b/app/src/main/res/drawable/shape_10ff4c4c_cor2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_10ff4c4c_cor4.xml b/app/src/main/res/drawable/shape_10ff4c4c_cor4.xml new file mode 100644 index 0000000..abb7498 --- /dev/null +++ b/app/src/main/res/drawable/shape_10ff4c4c_cor4.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_10ff9b2f_cor4.xml b/app/src/main/res/drawable/shape_10ff9b2f_cor4.xml new file mode 100644 index 0000000..537e258 --- /dev/null +++ b/app/src/main/res/drawable/shape_10ff9b2f_cor4.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_222222_cor12.xml b/app/src/main/res/drawable/shape_222222_cor12.xml new file mode 100644 index 0000000..4edf640 --- /dev/null +++ b/app/src/main/res/drawable/shape_222222_cor12.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_466afd_cor25.xml b/app/src/main/res/drawable/shape_466afd_cor25.xml new file mode 100644 index 0000000..2c269e2 --- /dev/null +++ b/app/src/main/res/drawable/shape_466afd_cor25.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_466afd_cor4.xml b/app/src/main/res/drawable/shape_466afd_cor4.xml new file mode 100644 index 0000000..c8a4b77 --- /dev/null +++ b/app/src/main/res/drawable/shape_466afd_cor4.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_466afd_line_cor6.xml b/app/src/main/res/drawable/shape_466afd_line_cor6.xml new file mode 100644 index 0000000..79effde --- /dev/null +++ b/app/src/main/res/drawable/shape_466afd_line_cor6.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_bottom_sheet_bg.xml b/app/src/main/res/drawable/shape_bottom_sheet_bg.xml new file mode 100644 index 0000000..2548501 --- /dev/null +++ b/app/src/main/res/drawable/shape_bottom_sheet_bg.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_cccccc_line_cor4.xml b/app/src/main/res/drawable/shape_cccccc_line_cor4.xml new file mode 100644 index 0000000..e7ba432 --- /dev/null +++ b/app/src/main/res/drawable/shape_cccccc_line_cor4.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_coupon_indicator.xml b/app/src/main/res/drawable/shape_coupon_indicator.xml new file mode 100644 index 0000000..a92b13d --- /dev/null +++ b/app/src/main/res/drawable/shape_coupon_indicator.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_default_date.xml b/app/src/main/res/drawable/shape_default_date.xml new file mode 100644 index 0000000..8799c46 --- /dev/null +++ b/app/src/main/res/drawable/shape_default_date.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_dfdfdf_line_cor8.xml b/app/src/main/res/drawable/shape_dfdfdf_line_cor8.xml new file mode 100644 index 0000000..4c7baa5 --- /dev/null +++ b/app/src/main/res/drawable/shape_dfdfdf_line_cor8.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_f3f5f9_cor10.xml b/app/src/main/res/drawable/shape_f3f5f9_cor10.xml new file mode 100644 index 0000000..b212de1 --- /dev/null +++ b/app/src/main/res/drawable/shape_f3f5f9_cor10.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_f3f5f9_top_cor16.xml b/app/src/main/res/drawable/shape_f3f5f9_top_cor16.xml new file mode 100644 index 0000000..ef0e366 --- /dev/null +++ b/app/src/main/res/drawable/shape_f3f5f9_top_cor16.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_f6f6f6_cor6.xml b/app/src/main/res/drawable/shape_f6f6f6_cor6.xml new file mode 100644 index 0000000..3d8d7e2 --- /dev/null +++ b/app/src/main/res/drawable/shape_f6f6f6_cor6.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fbfbfd_top_cor14.xml b/app/src/main/res/drawable/shape_fbfbfd_top_cor14.xml new file mode 100644 index 0000000..2a093f3 --- /dev/null +++ b/app/src/main/res/drawable/shape_fbfbfd_top_cor14.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fdf4f2_cor6.xml b/app/src/main/res/drawable/shape_fdf4f2_cor6.xml new file mode 100644 index 0000000..596a7ef --- /dev/null +++ b/app/src/main/res/drawable/shape_fdf4f2_cor6.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_ff8340_circle_dp4.xml b/app/src/main/res/drawable/shape_ff8340_circle_dp4.xml new file mode 100644 index 0000000..744cbc1 --- /dev/null +++ b/app/src/main/res/drawable/shape_ff8340_circle_dp4.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_home_notice_bg.xml b/app/src/main/res/drawable/shape_home_notice_bg.xml new file mode 100644 index 0000000..899651d --- /dev/null +++ b/app/src/main/res/drawable/shape_home_notice_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_indicator_default.xml b/app/src/main/res/drawable/shape_indicator_default.xml new file mode 100644 index 0000000..c6b7c3f --- /dev/null +++ b/app/src/main/res/drawable/shape_indicator_default.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_indicator_select.xml b/app/src/main/res/drawable/shape_indicator_select.xml new file mode 100644 index 0000000..b79bbfd --- /dev/null +++ b/app/src/main/res/drawable/shape_indicator_select.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_mine_menu_bg.xml b/app/src/main/res/drawable/shape_mine_menu_bg.xml new file mode 100644 index 0000000..3c96d8a --- /dev/null +++ b/app/src/main/res/drawable/shape_mine_menu_bg.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_mine_nologin_tip.xml b/app/src/main/res/drawable/shape_mine_nologin_tip.xml new file mode 100644 index 0000000..1abd087 --- /dev/null +++ b/app/src/main/res/drawable/shape_mine_nologin_tip.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vip_bottom_bg.xml b/app/src/main/res/drawable/shape_vip_bottom_bg.xml new file mode 100644 index 0000000..39c17ad --- /dev/null +++ b/app/src/main/res/drawable/shape_vip_bottom_bg.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vip_meal_check_false.xml b/app/src/main/res/drawable/shape_vip_meal_check_false.xml new file mode 100644 index 0000000..40c70db --- /dev/null +++ b/app/src/main/res/drawable/shape_vip_meal_check_false.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vip_meal_check_true.xml b/app/src/main/res/drawable/shape_vip_meal_check_true.xml new file mode 100644 index 0000000..f270e15 --- /dev/null +++ b/app/src/main/res/drawable/shape_vip_meal_check_true.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vip_meal_checked_bottom_bg.xml b/app/src/main/res/drawable/shape_vip_meal_checked_bottom_bg.xml new file mode 100644 index 0000000..3ac53ce --- /dev/null +++ b/app/src/main/res/drawable/shape_vip_meal_checked_bottom_bg.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vip_meal_default_bottom_bg.xml b/app/src/main/res/drawable/shape_vip_meal_default_bottom_bg.xml new file mode 100644 index 0000000..79ba8b4 --- /dev/null +++ b/app/src/main/res/drawable/shape_vip_meal_default_bottom_bg.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vip_tag_bg.xml b/app/src/main/res/drawable/shape_vip_tag_bg.xml new file mode 100644 index 0000000..c898ad7 --- /dev/null +++ b/app/src/main/res/drawable/shape_vip_tag_bg.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vip_tips_bg.xml b/app/src/main/res/drawable/shape_vip_tips_bg.xml new file mode 100644 index 0000000..c1fa0a5 --- /dev/null +++ b/app/src/main/res/drawable/shape_vip_tips_bg.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_white_cor16.xml b/app/src/main/res/drawable/shape_white_cor16.xml new file mode 100644 index 0000000..b230529 --- /dev/null +++ b/app/src/main/res/drawable/shape_white_cor16.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_white_cor6.xml b/app/src/main/res/drawable/shape_white_cor6.xml new file mode 100644 index 0000000..27f5d35 --- /dev/null +++ b/app/src/main/res/drawable/shape_white_cor6.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_white_top_cor20.xml b/app/src/main/res/drawable/shape_white_top_cor20.xml new file mode 100644 index 0000000..07ec514 --- /dev/null +++ b/app/src/main/res/drawable/shape_white_top_cor20.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_launcher.xml b/app/src/main/res/layout/activity_launcher.xml new file mode 100644 index 0000000..8b06ec1 --- /dev/null +++ b/app/src/main/res/layout/activity_launcher.xml @@ -0,0 +1,7 @@ + + + + \ 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..d829e29 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_account_list.xml b/app/src/main/res/layout/dialog_account_list.xml new file mode 100644 index 0000000..d82c75b --- /dev/null +++ b/app/src/main/res/layout/dialog_account_list.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_bind_phone.xml b/app/src/main/res/layout/dialog_bind_phone.xml new file mode 100644 index 0000000..29df3d5 --- /dev/null +++ b/app/src/main/res/layout/dialog_bind_phone.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_edit_text.xml b/app/src/main/res/layout/dialog_edit_text.xml new file mode 100644 index 0000000..ab77036 --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_text.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_login_tip.xml b/app/src/main/res/layout/dialog_login_tip.xml new file mode 100644 index 0000000..258f74d --- /dev/null +++ b/app/src/main/res/layout/dialog_login_tip.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_pay_tip.xml b/app/src/main/res/layout/dialog_pay_tip.xml new file mode 100644 index 0000000..49249cd --- /dev/null +++ b/app/src/main/res/layout/dialog_pay_tip.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_permission_tip.xml b/app/src/main/res/layout/dialog_permission_tip.xml new file mode 100644 index 0000000..c740d39 --- /dev/null +++ b/app/src/main/res/layout/dialog_permission_tip.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_privacy_policy.xml b/app/src/main/res/layout/dialog_privacy_policy.xml new file mode 100644 index 0000000..f5835ba --- /dev/null +++ b/app/src/main/res/layout/dialog_privacy_policy.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_select_coupon.xml b/app/src/main/res/layout/dialog_select_coupon.xml new file mode 100644 index 0000000..a5690a0 --- /dev/null +++ b/app/src/main/res/layout/dialog_select_coupon.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_share.xml b/app/src/main/res/layout/dialog_share.xml new file mode 100644 index 0000000..5a3a957 --- /dev/null +++ b/app/src/main/res/layout/dialog_share.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_simple_tip.xml b/app/src/main/res/layout/dialog_simple_tip.xml new file mode 100644 index 0000000..cf01216 --- /dev/null +++ b/app/src/main/res/layout/dialog_simple_tip.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_tip.xml b/app/src/main/res/layout/dialog_tip.xml new file mode 100644 index 0000000..7c5c732 --- /dev/null +++ b/app/src/main/res/layout/dialog_tip.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_update_verison.xml b/app/src/main/res/layout/dialog_update_verison.xml new file mode 100644 index 0000000..23539b0 --- /dev/null +++ b/app/src/main/res/layout/dialog_update_verison.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_vip_login_tip.xml b/app/src/main/res/layout/dialog_vip_login_tip.xml new file mode 100644 index 0000000..4ac9164 --- /dev/null +++ b/app/src/main/res/layout/dialog_vip_login_tip.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml new file mode 100644 index 0000000..64b0aa5 --- /dev/null +++ b/app/src/main/res/layout/fragment_about.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_account_manage.xml b/app/src/main/res/layout/fragment_account_manage.xml new file mode 100644 index 0000000..666129a --- /dev/null +++ b/app/src/main/res/layout/fragment_account_manage.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_app_config.xml b/app/src/main/res/layout/fragment_app_config.xml new file mode 100644 index 0000000..9943f8d --- /dev/null +++ b/app/src/main/res/layout/fragment_app_config.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bind_account.xml b/app/src/main/res/layout/fragment_bind_account.xml new file mode 100644 index 0000000..0c6d359 --- /dev/null +++ b/app/src/main/res/layout/fragment_bind_account.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_coupon.xml b/app/src/main/res/layout/fragment_coupon.xml new file mode 100644 index 0000000..425ae75 --- /dev/null +++ b/app/src/main/res/layout/fragment_coupon.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_coupon_list.xml b/app/src/main/res/layout/fragment_coupon_list.xml new file mode 100644 index 0000000..ddd80ef --- /dev/null +++ b/app/src/main/res/layout/fragment_coupon_list.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feedback.xml b/app/src/main/res/layout/fragment_feedback.xml new file mode 100644 index 0000000..2fae928 --- /dev/null +++ b/app/src/main/res/layout/fragment_feedback.xml @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_guide_content.xml b/app/src/main/res/layout/fragment_guide_content.xml new file mode 100644 index 0000000..9205c5a --- /dev/null +++ b/app/src/main/res/layout/fragment_guide_content.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_guide_item.xml b/app/src/main/res/layout/fragment_guide_item.xml new file mode 100644 index 0000000..57b78f2 --- /dev/null +++ b/app/src/main/res/layout/fragment_guide_item.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file 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..3728533 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000..8650bec --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..766b6ce --- /dev/null +++ b/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_mine.xml b/app/src/main/res/layout/fragment_mine.xml new file mode 100644 index 0000000..6be9c9a --- /dev/null +++ b/app/src/main/res/layout/fragment_mine.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_onekey_login.xml b/app/src/main/res/layout/fragment_onekey_login.xml new file mode 100644 index 0000000..da0c136 --- /dev/null +++ b/app/src/main/res/layout/fragment_onekey_login.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_photo_view.xml b/app/src/main/res/layout/fragment_photo_view.xml new file mode 100644 index 0000000..ca7e8a4 --- /dev/null +++ b/app/src/main/res/layout/fragment_photo_view.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000..cbbab43 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_user_setting.xml b/app/src/main/res/layout/fragment_user_setting.xml new file mode 100644 index 0000000..8566fb8 --- /dev/null +++ b/app/src/main/res/layout/fragment_user_setting.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_video_player.xml b/app/src/main/res/layout/fragment_video_player.xml new file mode 100644 index 0000000..86c6e09 --- /dev/null +++ b/app/src/main/res/layout/fragment_video_player.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_vip.xml b/app/src/main/res/layout/fragment_vip.xml new file mode 100644 index 0000000..eb07ea2 --- /dev/null +++ b/app/src/main/res/layout/fragment_vip.xml @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_video_controller.xml b/app/src/main/res/layout/layout_video_controller.xml new file mode 100644 index 0000000..758389a --- /dev/null +++ b/app/src/main/res/layout/layout_video_controller.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_account.xml b/app/src/main/res/layout/listitem_account.xml new file mode 100644 index 0000000..e528e0f --- /dev/null +++ b/app/src/main/res/layout/listitem_account.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_account_login_tip.xml b/app/src/main/res/layout/listitem_account_login_tip.xml new file mode 100644 index 0000000..e58dfb1 --- /dev/null +++ b/app/src/main/res/layout/listitem_account_login_tip.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_add_img.xml b/app/src/main/res/layout/listitem_add_img.xml new file mode 100644 index 0000000..ccf9772 --- /dev/null +++ b/app/src/main/res/layout/listitem_add_img.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_coupon.xml b/app/src/main/res/layout/listitem_coupon.xml new file mode 100644 index 0000000..95d9b4d --- /dev/null +++ b/app/src/main/res/layout/listitem_coupon.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_coupon_dialog.xml b/app/src/main/res/layout/listitem_coupon_dialog.xml new file mode 100644 index 0000000..13a7a01 --- /dev/null +++ b/app/src/main/res/layout/listitem_coupon_dialog.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_indicator.xml b/app/src/main/res/layout/listitem_indicator.xml new file mode 100644 index 0000000..a2d0a17 --- /dev/null +++ b/app/src/main/res/layout/listitem_indicator.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_photo_view.xml b/app/src/main/res/layout/listitem_photo_view.xml new file mode 100644 index 0000000..9157bcc --- /dev/null +++ b/app/src/main/res/layout/listitem_photo_view.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_vip.xml b/app/src/main/res/layout/listitem_vip.xml new file mode 100644 index 0000000..69f8907 --- /dev/null +++ b/app/src/main/res/layout/listitem_vip.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_vip_meal.xml b/app/src/main/res/layout/listitem_vip_meal.xml new file mode 100644 index 0000000..6b74a09 --- /dev/null +++ b/app/src/main/res/layout/listitem_vip_meal.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/listitem_vip_tip.xml b/app/src/main/res/layout/listitem_vip_tip.xml new file mode 100644 index 0000000..0b00f01 --- /dev/null +++ b/app/src/main/res/layout/listitem_vip_tip.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pop_about_tip.xml b/app/src/main/res/layout/pop_about_tip.xml new file mode 100644 index 0000000..270ee1f --- /dev/null +++ b/app/src/main/res/layout/pop_about_tip.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_feedback_image.webp b/app/src/main/res/mipmap-xxhdpi/ic_add_feedback_image.webp new file mode 100644 index 0000000..0864c7e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_feedback_image.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_image.webp b/app/src/main/res/mipmap-xxhdpi/ic_add_image.webp new file mode 100644 index 0000000..4869507 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_image.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_ali_pay.webp b/app/src/main/res/mipmap-xxhdpi/ic_ali_pay.webp new file mode 100644 index 0000000..76da22e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_ali_pay.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_area_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_area_bg.webp new file mode 100644 index 0000000..e2cc2e5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_area_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_arrow_dp16.webp b/app/src/main/res/mipmap-xxhdpi/ic_arrow_dp16.webp new file mode 100644 index 0000000..75775db Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_arrow_dp16.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_bind_phone.webp b/app/src/main/res/mipmap-xxhdpi/ic_bind_phone.webp new file mode 100644 index 0000000..0522119 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_bind_phone.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_bind_wx.webp b/app/src/main/res/mipmap-xxhdpi/ic_bind_wx.webp new file mode 100644 index 0000000..9b5eca6 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_bind_wx.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_change_account.webp b/app/src/main/res/mipmap-xxhdpi/ic_change_account.webp new file mode 100644 index 0000000..35f6f5e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_change_account.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_check_false.webp b/app/src/main/res/mipmap-xxhdpi/ic_check_false.webp new file mode 100644 index 0000000..fa69c45 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_check_false.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_check_true.webp b/app/src/main/res/mipmap-xxhdpi/ic_check_true.webp new file mode 100644 index 0000000..219fd23 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_check_true.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_clear_text.webp b/app/src/main/res/mipmap-xxhdpi/ic_clear_text.webp new file mode 100644 index 0000000..967aa6b Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_clear_text.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_close.webp b/app/src/main/res/mipmap-xxhdpi/ic_close.webp new file mode 100644 index 0000000..8b9bd70 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_close.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_close_dialog.webp b/app/src/main/res/mipmap-xxhdpi/ic_close_dialog.webp new file mode 100644 index 0000000..9c83439 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_close_dialog.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_coupon.webp b/app/src/main/res/mipmap-xxhdpi/ic_coupon.webp new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_coupon_check_disable.webp b/app/src/main/res/mipmap-xxhdpi/ic_coupon_check_disable.webp new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_coupon_check_false.webp b/app/src/main/res/mipmap-xxhdpi/ic_coupon_check_false.webp new file mode 100644 index 0000000..92e4b37 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_coupon_check_false.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_coupon_check_true.webp b/app/src/main/res/mipmap-xxhdpi/ic_coupon_check_true.webp new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_default_avatar.webp b/app/src/main/res/mipmap-xxhdpi/ic_default_avatar.webp new file mode 100644 index 0000000..dfc1101 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_default_avatar.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_delete_feedback_img.webp b/app/src/main/res/mipmap-xxhdpi/ic_delete_feedback_img.webp new file mode 100644 index 0000000..f15b4ea Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_delete_feedback_img.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_edit.webp b/app/src/main/res/mipmap-xxhdpi/ic_edit.webp new file mode 100644 index 0000000..09f2fc5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_edit.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_empty_data.webp b/app/src/main/res/mipmap-xxhdpi/ic_empty_data.webp new file mode 100644 index 0000000..9bebdc5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_empty_data.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_expire_coupon.webp b/app/src/main/res/mipmap-xxhdpi/ic_expire_coupon.webp new file mode 100644 index 0000000..c0777f6 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_expire_coupon.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_feedback.webp b/app/src/main/res/mipmap-xxhdpi/ic_feedback.webp new file mode 100644 index 0000000..0847615 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_feedback.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_fire_tag.webp b/app/src/main/res/mipmap-xxhdpi/ic_fire_tag.webp new file mode 100644 index 0000000..ff63d78 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_fire_tag.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_guide_1.webp b/app/src/main/res/mipmap-xxhdpi/ic_guide_1.webp new file mode 100644 index 0000000..4d72306 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_guide_1.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_guide_2.webp b/app/src/main/res/mipmap-xxhdpi/ic_guide_2.webp new file mode 100644 index 0000000..24c706a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_guide_2.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_guide_3.webp b/app/src/main/res/mipmap-xxhdpi/ic_guide_3.webp new file mode 100644 index 0000000..8663780 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_guide_3.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_guide_4.webp b/app/src/main/res/mipmap-xxhdpi/ic_guide_4.webp new file mode 100644 index 0000000..3bbaabb Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_guide_4.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_guide_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_guide_bg.webp new file mode 100644 index 0000000..ba0633a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_guide_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_guide_cover.webp b/app/src/main/res/mipmap-xxhdpi/ic_guide_cover.webp new file mode 100644 index 0000000..422d88f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_guide_cover.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_default.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_default.webp new file mode 100644 index 0000000..4a22997 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_default.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home_select.webp b/app/src/main/res/mipmap-xxhdpi/ic_home_select.webp new file mode 100644 index 0000000..2632450 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home_select.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_icon.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_icon.png new file mode 100644 index 0000000..6243470 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_icon.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_link_share.webp b/app/src/main/res/mipmap-xxhdpi/ic_link_share.webp new file mode 100644 index 0000000..c2f1cc8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_link_share.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_login_code.webp b/app/src/main/res/mipmap-xxhdpi/ic_login_code.webp new file mode 100644 index 0000000..94a90b6 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_login_code.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_login_logo.webp b/app/src/main/res/mipmap-xxhdpi/ic_login_logo.webp new file mode 100644 index 0000000..e7f8891 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_login_logo.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_login_phone.webp b/app/src/main/res/mipmap-xxhdpi/ic_login_phone.webp new file mode 100644 index 0000000..c4f06cf Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_login_phone.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_login_tip.webp b/app/src/main/res/mipmap-xxhdpi/ic_login_tip.webp new file mode 100644 index 0000000..abd381d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_login_tip.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_login_top_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_login_top_bg.webp new file mode 100644 index 0000000..c770208 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_login_top_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_default.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_default.webp new file mode 100644 index 0000000..cfc2dde Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_default.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_mine_select.webp b/app/src/main/res/mipmap-xxhdpi/ic_mine_select.webp new file mode 100644 index 0000000..4477eb3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_mine_select.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_no_data.webp b/app/src/main/res/mipmap-xxhdpi/ic_no_data.webp new file mode 100644 index 0000000..18828e5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_no_data.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_no_data_small.webp b/app/src/main/res/mipmap-xxhdpi/ic_no_data_small.webp new file mode 100644 index 0000000..357d01a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_no_data_small.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_notify_icon.webp b/app/src/main/res/mipmap-xxhdpi/ic_notify_icon.webp new file mode 100644 index 0000000..94f365d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_notify_icon.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_onekey_login.webp b/app/src/main/res/mipmap-xxhdpi/ic_onekey_login.webp new file mode 100644 index 0000000..a23110c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_onekey_login.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_pay_false.webp b/app/src/main/res/mipmap-xxhdpi/ic_pay_false.webp new file mode 100644 index 0000000..a7f8a55 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_pay_false.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_pay_true.webp b/app/src/main/res/mipmap-xxhdpi/ic_pay_true.webp new file mode 100644 index 0000000..bb6bbfe Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_pay_true.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_phone_login.webp b/app/src/main/res/mipmap-xxhdpi/ic_phone_login.webp new file mode 100644 index 0000000..eb26ff1 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_phone_login.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_placeholder.webp b/app/src/main/res/mipmap-xxhdpi/ic_placeholder.webp new file mode 100644 index 0000000..7af9197 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_placeholder.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_play_video.webp b/app/src/main/res/mipmap-xxhdpi/ic_play_video.webp new file mode 100644 index 0000000..feeefad Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_play_video.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_service.webp b/app/src/main/res/mipmap-xxhdpi/ic_service.webp new file mode 100644 index 0000000..4b317c7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_service.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_splash_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_splash_bg.webp new file mode 100644 index 0000000..7295f88 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_splash_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_splash_logo.png b/app/src/main/res/mipmap-xxhdpi/ic_splash_logo.png new file mode 100644 index 0000000..a7172fd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_splash_logo.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_star.webp b/app/src/main/res/mipmap-xxhdpi/ic_star.webp new file mode 100644 index 0000000..759c0c3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_star.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_star_dp10.webp b/app/src/main/res/mipmap-xxhdpi/ic_star_dp10.webp new file mode 100644 index 0000000..28eee3e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_star_dp10.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_star_dp12.webp b/app/src/main/res/mipmap-xxhdpi/ic_star_dp12.webp new file mode 100644 index 0000000..40ba41a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_star_dp12.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_star_white.webp b/app/src/main/res/mipmap-xxhdpi/ic_star_white.webp new file mode 100644 index 0000000..5b3fbc9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_star_white.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_tip_dialog_top_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_tip_dialog_top_bg.webp new file mode 100644 index 0000000..4651426 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_tip_dialog_top_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_update_close.webp b/app/src/main/res/mipmap-xxhdpi/ic_update_close.webp new file mode 100644 index 0000000..76c9c26 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_update_close.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_update_top_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_update_top_bg.webp new file mode 100644 index 0000000..0843204 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_update_top_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_update_version_bg_z.webp b/app/src/main/res/mipmap-xxhdpi/ic_update_version_bg_z.webp new file mode 100644 index 0000000..bdbcb56 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_update_version_bg_z.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_bg.webp new file mode 100644 index 0000000..2f207ff Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_bg1.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_bg1.webp new file mode 100644 index 0000000..23235c0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_bg1.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_bg2.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_bg2.webp new file mode 100644 index 0000000..a8475e9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_bg2.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item1.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item1.webp new file mode 100644 index 0000000..9ea591f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item1.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item10.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item10.webp new file mode 100644 index 0000000..9906ef4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item10.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item11.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item11.webp new file mode 100644 index 0000000..40b7b04 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item11.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item12.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item12.webp new file mode 100644 index 0000000..58c0cce Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item12.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item2.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item2.webp new file mode 100644 index 0000000..3ef3d3c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item2.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item3.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item3.webp new file mode 100644 index 0000000..784614c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item3.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item4.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item4.webp new file mode 100644 index 0000000..4e6f2aa Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item4.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item5.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item5.webp new file mode 100644 index 0000000..754dd69 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item5.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item6.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item6.webp new file mode 100644 index 0000000..427cce7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item6.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item7.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item7.webp new file mode 100644 index 0000000..2298e0f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item7.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item8.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item8.webp new file mode 100644 index 0000000..a97ed2c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item8.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_item9.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_item9.webp new file mode 100644 index 0000000..906b1c8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_item9.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_tip_l.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_tip_l.webp new file mode 100644 index 0000000..123ccd4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_tip_l.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_tip_r.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_tip_r.webp new file mode 100644 index 0000000..789f3f8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_tip_r.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_tips.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips.webp new file mode 100644 index 0000000..3ae3563 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_divider.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_divider.webp new file mode 100644 index 0000000..0950f33 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_divider.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_light.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_light.webp new file mode 100644 index 0000000..5128f64 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_light.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_star.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_star.webp new file mode 100644 index 0000000..ea74077 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_tips_star.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_vip_top_bg.webp b/app/src/main/res/mipmap-xxhdpi/ic_vip_top_bg.webp new file mode 100644 index 0000000..e77e1d9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_vip_top_bg.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_wx_circle_share.webp b/app/src/main/res/mipmap-xxhdpi/ic_wx_circle_share.webp new file mode 100644 index 0000000..24fac78 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_wx_circle_share.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_wx_login.webp b/app/src/main/res/mipmap-xxhdpi/ic_wx_login.webp new file mode 100644 index 0000000..94b2500 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_wx_login.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_wx_pay.webp b/app/src/main/res/mipmap-xxhdpi/ic_wx_pay.webp new file mode 100644 index 0000000..a6d1306 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_wx_pay.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_wx_share.webp b/app/src/main/res/mipmap-xxhdpi/ic_wx_share.webp new file mode 100644 index 0000000..097914a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_wx_share.webp differ diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml new file mode 100644 index 0000000..b2acfc8 --- /dev/null +++ b/app/src/main/res/values-v31/themes.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..9b0ca5d --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..5effc03 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,76 @@ + + + @color/white + @color/white + @color/black + + @color/color_f3f5f9 + @color/black + @color/black + + #00000000 + #FF000000 + #FFFFFF + #F3F5F9 + + + + @color/color_1a1a1a//3e4145 前景主色,如字体颜色、icon颜色 + @color/color_999999//前景偏暗色,如不可用状态的字体颜色 + @color/color_f0365e//578fff 前景突出色,如发送按钮的背景色,选中状态的颜色 + + + @color/color_f3f4f5//f9f9f9 背景色,如每个activity的背景色,相册的图片间隔线颜色 + + + @color/white//ffffff 操作栏主色,如相册页顶部和底部操作栏 + @color/color_f3f4f5//f9f9f9 操作栏偏暗色,操作栏间隔线颜色,如相册页返回按钮右边的竖线 + #b4ffffff//b4ffffff 操作栏带透明的色值,如预览页的底部操作栏颜色,相册页的相机按钮背景颜色。 + + + @color/white + + + #1a1a1a + #333333 + #666666 + #999999 + #BCBCBC + #2090FE + #80FFFFFF + #E5FFFFFF + #CCFFFFFF + #1AFFFFFF + + #eeeeee + #ff483e + #9C948D + #f6f6f6 + #EFF2F7 + #D8D8D8 + + #F3F4F5 + #AAAAAA + #F0365E + #222222 + #E8E8E8 + #CCCCCC + #FF3838 + #DFDFDF + #7E1E07 + #FF5846 + #FB7528 + #FDF4F2 + + #466AFD + #212226 + #727686 + #A3A7B9 + #F1F2F6 + #676E87 + #54230C + #F94747 + #896451 + #54220B + #80859B + \ 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..bcad9f1 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,89 @@ + + + 10sp + 11sp + 12sp + 13sp + 14sp + 15sp + 16sp + 17sp + 18sp + 19sp + 20sp + 21sp + 22sp + 23sp + 24sp + 30sp + 34sp + + 3dp + 4dp + 5dp + 6dp + 8dp + 10dp + 11dp + 12dp + 15dp + 20dp + 25dp + 30dp + 35dp + 40dp + 45dp + 50dp + 60dp + 70dp + 14dp + 16dp + 55dp + 22dp + 13dp + 2dp + 26sp + 18dp + 200dp + 1dp + 17dp + 100dp + 9dp + 80dp + 24dp + 56dp + 7dp + 48dp + 28dp + 26dp + 46dp + 54dp + 19dp + 38dp + 300dp + 32dp + 120dp + 76dp + 62dp + 150dp + 90dp + 140dp + 44dp + 110dp + 27dp + 36dp + 400dp + 82dp + 37dp + 65dp + 195dp + 58dp + 23dp + 34dp + 64dp + 85dp + 130dp + 42dp + 170dp + + \ 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..fe54940 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + 伯乐招标 + 双击退出应用 + + 同意 + 拒绝 + 请你务必审慎阅读、充分理解服务协议和隐私政策各条款,包括但不限于:为了更好的向你提供服务,我们需要访问你的相册、位置信息等。你可阅读《隐私政策》了解详细信息。如果你同意,请点击下面同意按钮开始接受我们的服务。 + 正在检测新版本... + 当前版本是最新版 + 立即更新 + 暂不 + 登录之前须先查看并同意《用户协议》《隐私政策》 + 检测到您已支付成为会员,请登录\n绑定手机号,以免账号丢失 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..cdeea6e --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/filepath_data.xml b/app/src/main/res/xml/filepath_data.xml new file mode 100644 index 0000000..d551921 --- /dev/null +++ b/app/src/main/res/xml/filepath_data.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/cn/zuom8/phoneloca/ExampleUnitTest.kt b/app/src/test/java/cn/zuom8/phoneloca/ExampleUnitTest.kt new file mode 100644 index 0000000..cf0e3b5 --- /dev/null +++ b/app/src/test/java/cn/zuom8/phoneloca/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.cheng.bole + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun 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..f82eb72 --- /dev/null +++ b/base/build.gradle @@ -0,0 +1,118 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace 'com.example.base' + compileSdk 34 + buildFeatures.buildConfig = true + + defaultConfig { + minSdk 26 + targetSdk 34 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + } + + buildFeatures { + viewBinding true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } +} + + +dependencies { + ext.anko_version = '0.10.8' + api "org.jetbrains.anko:anko-coroutines:$anko_version" + api "org.jetbrains.anko:anko-common:$anko_version" + api "org.jetbrains.anko:anko-sdk27:$anko_version" + api "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" + + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.10.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2' + implementation 'androidx.navigation:navigation-ui-ktx:2.5.2' + + api 'androidx.lifecycle:lifecycle-extensions:2.2.0' + + api 'androidx.databinding:viewbinding:7.4.2' + + api fileTree(dir: 'libs', include: ['*.jar']) + + + api 'androidx.activity:activity-ktx:1.6.1' + api 'com.google.code.gson:gson:2.10.1' + + api 'androidx.multidex:multidex:2.0.1' + + // RecyclerView 万能适配器 + api 'io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.14' + + //日志打印 + api 'com.orhanobut:logger:2.2.0' + + // rx系列 + api 'com.jakewharton.rxbinding4:rxbinding:4.0.0' + + // 图片加载 + api 'io.coil-kt:coil:2.2.2' + api 'io.coil-kt:coil-gif:2.2.2' + api 'io.coil-kt:coil-video:1.4.0' + api 'androidx.legacy:legacy-support-v4:1.0.0' + api 'com.github.bumptech.glide:glide:4.15.1'//Gride图片加载框架 + api 'com.github.bumptech.glide:compiler:4.13.0' + + //图片预览(超大图预览) + api 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' + + //一个简单、轻量、可随意定制 的Android版本更新库 + api 'io.github.azhon:appupdate:4.3.2' + + // AgentWeb 是一个基于的 Android WebView + api 'com.github.Justson.AgentWeb:agentweb-core:v5.0.6-androidx' // (必选) + api 'com.github.Justson.AgentWeb:agentweb-filechooser:v5.0.6-androidx' // (可选) + api 'com.github.Justson:Downloader:v5.0.4-androidx' + + // 网络请求 + api 'com.squareup.retrofit2:retrofit:2.9.0' + api 'com.squareup.retrofit2:converter-gson:2.9.0' + api 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' + api 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2' + + // 下拉刷新 + api 'io.github.scwang90:refresh-layout-kernel:3.0.0-alpha' //核心必须依赖 + api 'io.github.scwang90:refresh-header-classics:3.0.0-alpha' //经典刷新头 + api 'io.github.scwang90:refresh-header-material:3.0.0-alpha' //谷歌刷新头 + + api 'com.github.getActivity:XXPermissions:18.2' + + // tencent key value存储 + api 'com.tencent:mmkv-static:1.2.15' + + api 'com.alibaba:fastjson:1.1.70.android' + + api 'com.geyifeng.immersionbar:immersionbar:3.2.2' + api 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2' +} diff --git a/base/consumer-rules.pro b/base/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/base/libs/SaaS_AppAnalytics_Android_SDK_V4.0.67.jar b/base/libs/SaaS_AppAnalytics_Android_SDK_V4.0.67.jar new file mode 100644 index 0000000..489132e Binary files /dev/null and b/base/libs/SaaS_AppAnalytics_Android_SDK_V4.0.67.jar differ diff --git a/base/proguard-rules.pro b/base/proguard-rules.pro new file mode 100644 index 0000000..15f6399 --- /dev/null +++ b/base/proguard-rules.pro @@ -0,0 +1,25 @@ +# 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 +-keep class com.just.agentweb.** { + *; +} +-dontwarn com.just.agentweb.** \ 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..e8b8cc7 --- /dev/null +++ b/base/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/java/com/example/base/MvvmApplication.kt b/base/src/main/java/com/example/base/MvvmApplication.kt new file mode 100644 index 0000000..f9fc7eb --- /dev/null +++ b/base/src/main/java/com/example/base/MvvmApplication.kt @@ -0,0 +1,52 @@ +package com.example.base + +import android.app.Activity +import android.os.Bundle +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.multidex.MultiDexApplication +import com.example.base.global.ActivityGlobalManager + + +/** + * @date 2021-08-13 09:05 + * @desc 程序入口 + */ + +open class MvvmApplication : MultiDexApplication(), ViewModelStoreOwner { + + // 通过 Application 创建全局共享的 VM + override val viewModelStore = ViewModelStore() + + override fun onCreate() { + super.onCreate() + //注册自己的Activity的生命周期回调接口。 + registerActivityLifecycleCallbacks(activityLifecycleCallbacks) + } + + private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks { + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStarted(activity: Activity) { + } + + override fun onActivityDestroyed(activity: Activity) { + ActivityGlobalManager.removeActivity(activity) + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + ActivityGlobalManager.addActivity(activity) + } + + override fun onActivityResumed(activity: Activity) { + } + + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/bean/DialogBean.kt b/base/src/main/java/com/example/base/bean/DialogBean.kt new file mode 100644 index 0000000..3ec5d3f --- /dev/null +++ b/base/src/main/java/com/example/base/bean/DialogBean.kt @@ -0,0 +1,13 @@ +package com.example.base.bean + + +/** + * @date 2020-06-05 13:47 + * @desc 控制显示对话框的bean + */ + +class DialogBean { + var isShow: Boolean = true + var msg: String = "正在加载..." + var cancelable: Boolean = false +} diff --git a/base/src/main/java/com/example/base/browser/BrowserActivity.kt b/base/src/main/java/com/example/base/browser/BrowserActivity.kt new file mode 100644 index 0000000..ccd41ea --- /dev/null +++ b/base/src/main/java/com/example/base/browser/BrowserActivity.kt @@ -0,0 +1,178 @@ +package com.example.base.browser + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.os.Bundle +import android.view.ViewGroup +import android.view.WindowManager +import android.webkit.WebResourceRequest +import android.webkit.WebView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.example.base.R +import com.example.base.utils.DensityUtils +import com.example.base.widget.TitleBar +import com.gyf.immersionbar.ImmersionBar +import com.just.agentweb.AgentWeb +import com.just.agentweb.MiddlewareWebChromeBase +import com.just.agentweb.MiddlewareWebClientBase + +/** + * 全局浏览器Activity + * 使用了agentWeb {@link https://github.com/Justson/AgentWeb } + */ +class BrowserActivity : AppCompatActivity() { + + private val titleStr by lazy { intent.getStringExtra(com.example.base.browser.BrowserActivity.Companion.ARG_TITLE) } + private val url by lazy { intent.getStringExtra(com.example.base.browser.BrowserActivity.Companion.ARG_URL) } + + + private lateinit var mAgentWeb: AgentWeb + private lateinit var mPreAgentWeb: AgentWeb.PreAgentWeb + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.mvvm_activity_browser) + if (immersionBarEnabled()) { + initImmersionBar() + } + initView() + } + + private fun initImmersionBar() { + ImmersionBar.with(this) + .keyboardEnable(false) + .statusBarColor(android.R.color.transparent) + .navigationBarColor(android.R.color.black) + .statusBarDarkFont(true) + .navigationBarDarkIcon(true) + .keyboardMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + .init() + } + + private fun immersionBarEnabled(): Boolean { + return true + } + + private fun initView() { + setSupportActionBar(findViewById(R.id.mTitleBar)) + supportActionBar?.setHomeAsUpIndicator(R.drawable.mvvm_ic_close_black) + title = titleStr + + mPreAgentWeb = AgentWeb.with(this) + .setAgentWebParent(findViewById(R.id.content), ViewGroup.LayoutParams(-1, -1)) + .useDefaultIndicator() + .useMiddlewareWebChrome(mMiddlewareWebChromeBase) + .useMiddlewareWebClient(mMiddlewareWebClientBase) + .createAgentWeb() + .ready() + + mAgentWeb = mPreAgentWeb.go(url) + + setBackColor(android.R.color.black) + + ImmersionBar.setTitleBar(this, findViewById(R.id.mTitleBar)) + ImmersionBar.setStatusBarView(this, DensityUtils.getStatusBarHeight(this)) + ImmersionBar.with(this).statusBarDarkFont(false).init() + } + + fun setBackColor(color: Int) { + val upArrow = ContextCompat.getDrawable(this, R.drawable.ic_back) + upArrow?.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(this, color), PorterDuff.Mode.SRC_ATOP) + supportActionBar?.setHomeAsUpIndicator(upArrow) + } + + private val mMiddlewareWebChromeBase = object : MiddlewareWebChromeBase() { + override fun onReceivedTitle(view: WebView?, title: String?) { + super.onReceivedTitle(view, title) +// this@BrowserActivity.title = title + } + } + + private val mMiddlewareWebClientBase = object : MiddlewareWebClientBase() { + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val uri = request?.url + return if (uri?.scheme?.contains("http") == true) { + super.shouldOverrideUrlLoading(view, request) + } else { + view?.stopLoading() + try { + startActivity(Intent(Intent.ACTION_VIEW, uri)) + } catch (e: Exception) { + } + false + } + } + + } + + /** + * 重置标题 + */ + override fun onTitleChanged(title: CharSequence?, color: Int) { + super.onTitleChanged(title, color) + val titleBar = findViewById(R.id.mTitleBar) + titleBar?.title = title + } + + + /** + * 设置toolbar的返回键 调用onBackPressed + */ + override fun onSupportNavigateUp(): Boolean { + finish() + return true + } + + /** + * 返回键 + */ + override fun onBackPressed() { + if (!mAgentWeb.back()) { + super.onBackPressed() + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (intent?.extras != null) { + title = intent.extras!!.getString(com.example.base.browser.BrowserActivity.Companion.ARG_TITLE) + val url = intent.extras!!.getString(com.example.base.browser.BrowserActivity.Companion.ARG_URL) + mPreAgentWeb.go(url) + } + } + + override fun onDestroy() { + super.onDestroy() + mAgentWeb.destroy() + } + + + companion object { + const val ARG_TITLE = "arg_title" + const val ARG_URL = "arg_url" + const val ARG_SHOW_MENU = "arg_show_menu" + + /** + * @param title + * @param url + */ + fun start(context: Context?, title: String?, url: String?, showMenu: Boolean = true) { + val intent = Intent(context, com.example.base.browser.BrowserActivity::class.java) + intent.putExtra(com.example.base.browser.BrowserActivity.Companion.ARG_TITLE, title) + intent.putExtra(com.example.base.browser.BrowserActivity.Companion.ARG_URL, url) + intent.putExtra(com.example.base.browser.BrowserActivity.Companion.ARG_SHOW_MENU, showMenu) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context?.startActivity(intent) + } + } + +} diff --git a/base/src/main/java/com/example/base/common/ActivityManager.kt b/base/src/main/java/com/example/base/common/ActivityManager.kt new file mode 100644 index 0000000..8d0da32 --- /dev/null +++ b/base/src/main/java/com/example/base/common/ActivityManager.kt @@ -0,0 +1,46 @@ +package com.example.base.common + +import android.app.Activity +import java.util.LinkedList + + +/** + * 单例数据共享 + */ +class ActivityManager { + + companion object { + + private val mActivityList: LinkedList by lazy { LinkedList() } + + fun addActivity(activity: Activity): Boolean { + return mActivityList.add(activity) + } + + fun removeActivity(activity: Activity): Boolean { + return mActivityList.remove(activity) + } + + fun hasActivity(clazz: Class): Boolean { + return mActivityList.find { it.javaClass == clazz } != null + } + + fun lastOrNull(): Activity? { + return mActivityList.lastOrNull() + } + + fun getAllActivity(): LinkedList { + return mActivityList + } + + fun clear() { + return mActivityList.clear() + } + + fun exitApp() { + mActivityList.forEach { + it.finish() + } + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/common/RxBus.kt b/base/src/main/java/com/example/base/common/RxBus.kt new file mode 100644 index 0000000..04c5ed4 --- /dev/null +++ b/base/src/main/java/com/example/base/common/RxBus.kt @@ -0,0 +1,30 @@ +package com.example.base.common + +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject + + +/** + * 描述: 用RxJava实现事件总线(Event Bus) + * PublishSubject只会把在订阅发生的时间点之后来自原始Observable的数据发射给观察者 + * + */ +class RxBus { + + private val bus = PublishSubject.create() + + // 发送一个新的事件 + fun post(o: Any) { + bus.onNext(o) + } + + // 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者 + fun toObservable(eventType: Class): Observable { + return bus.ofType(eventType) + } + + companion object { + val defaultInstance: RxBus by lazy { RxBus() } + } + +} diff --git a/base/src/main/java/com/example/base/common/RxCountDown.kt b/base/src/main/java/com/example/base/common/RxCountDown.kt new file mode 100644 index 0000000..96336c2 --- /dev/null +++ b/base/src/main/java/com/example/base/common/RxCountDown.kt @@ -0,0 +1,21 @@ +package com.example.base.common + +import androidx.annotation.IntRange +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Flowable +import java.util.concurrent.TimeUnit + +/** + * 描述:rx倒计时 + */ + +object RxCountDown { + fun countdown(@IntRange(from = 0, to = Long.MAX_VALUE) time: Long): Flowable { + + return Flowable.interval(0, 1, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(AndroidSchedulers.mainThread()) + .map { aLong -> time - aLong } + .take(time + 1) + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/decoration/DividerItemDecoration.kt b/base/src/main/java/com/example/base/decoration/DividerItemDecoration.kt new file mode 100644 index 0000000..d35bf4c --- /dev/null +++ b/base/src/main/java/com/example/base/decoration/DividerItemDecoration.kt @@ -0,0 +1,55 @@ +package com.example.base.decoration + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.view.View +import androidx.annotation.ColorInt +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + + +/** + * recyclerView 分割线 + */ +class DividerItemDecoration( + private val leftMargin: Int = 0, + private val rightMargin: Int = 0, + private val dividerHeight: Int = 2, + @ColorInt color: Int = Color.LTGRAY + +) : ItemDecoration() { + + private val mDrawable = ColorDrawable(color) + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + // 第一个ItemView不需要在上面绘制分割线 + if (parent.getChildLayoutPosition(view) != 0) { + outRect.top = dividerHeight + } + } + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val left: Int = parent.paddingLeft + leftMargin + val right: Int = parent.width - parent.paddingRight - rightMargin + val childCount: Int = parent.childCount + for (i in 0 until childCount) { + //第一个ItemView不需要绘制 + if (i == 0) { + continue + } + val child: View = parent.getChildAt(i) + val params = child.layoutParams as RecyclerView.LayoutParams + val top = child.top + params.bottomMargin + val bottom = top + dividerHeight + mDrawable.setBounds(left, top, right, bottom) + mDrawable.draw(c) + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/decoration/FirstItemOffsetDecoration.kt b/base/src/main/java/com/example/base/decoration/FirstItemOffsetDecoration.kt new file mode 100644 index 0000000..ba49f29 --- /dev/null +++ b/base/src/main/java/com/example/base/decoration/FirstItemOffsetDecoration.kt @@ -0,0 +1,38 @@ +package com.example.base.decoration + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +/** + * 设置上下左右间隔 + */ +class FirstItemOffsetDecoration( + private val space: Int, + private val position: Int = left +) : ItemDecoration() { + + companion object { + var left = 0 + var top = 1 + var right = 2 + var bottom = 3 + } + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + if (parent.getChildLayoutPosition(view) == 0) { + when (position) { + left -> outRect.left = space + top -> outRect.top = space + right -> outRect.right = space + bottom -> outRect.bottom = space + } + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/decoration/GridSectionAverageGapItemDecoration.kt b/base/src/main/java/com/example/base/decoration/GridSectionAverageGapItemDecoration.kt new file mode 100644 index 0000000..fb5043d --- /dev/null +++ b/base/src/main/java/com/example/base/decoration/GridSectionAverageGapItemDecoration.kt @@ -0,0 +1,228 @@ +package com.example.base.decoration + +import android.graphics.Rect +import android.util.DisplayMetrics +import android.util.TypedValue +import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.chad.library.adapter.base.BaseSectionQuickAdapter +import com.chad.library.adapter.base.entity.SectionEntity +import com.chad.library.adapter.base.viewholder.BaseViewHolder + +/** + * 应用于RecyclerView的GridLayoutManager,水平方向上固定间距大小,从而使条目宽度自适应。

+ * 配合Brvah的Section使用,不对Head生效,仅对每个Head的子Grid列表生效

+ * Section Grid中Item的宽度应设为MATCH_PARAENT + * + */ +class GridSectionAverageGapItemDecoration +/** + * @param gapHorizontalDp item之间的水平间距 + * @param gapVerticalDp item之间的垂直间距 + * @param sectionEdgeHPaddingDp section左右两端的padding大小 + * @param sectionEdgeVPaddingDp section上下两端的padding大小 + */( + private val gapHorizontalDp: Float, + private val gapVerticalDp: Float, + private val sectionEdgeHPaddingDp: Float, + private val sectionEdgeVPaddingDp: Float +) : ItemDecoration() { + private inner class Section { + var startPos = 0 + var endPos = 0 + val count: Int + get() = endPos - startPos + 1 + + operator fun contains(pos: Int): Boolean { + return pos in startPos..endPos + } + + override fun toString(): String { + return "Section{" + + "startPos=" + startPos + + ", endPos=" + endPos + + '}' + } + } + + private var gapHSizePx = -1 + private var gapVSizePx = -1 + private var sectionEdgeHPaddingPx = 0 + private var eachItemHPaddingPx = 0 //每个条目应该在水平方向上加的padding 总大小,即=paddingLeft+paddingRight = 0 + private var sectionEdgeVPaddingPx = 0 + private val mSectionList: MutableList
= ArrayList() + private var mAdapter: BaseSectionQuickAdapter? = null + private val mDataObserver: AdapterDataObserver = object : AdapterDataObserver() { + override fun onChanged() { + markSections() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + markSections() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { + markSections() + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + markSections() + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + markSections() + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + markSections() + } + } + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + + if (parent.layoutManager is GridLayoutManager && parent.adapter is BaseSectionQuickAdapter<*, *>) { + val layoutManager = parent.layoutManager as GridLayoutManager + val adapter = parent.adapter as BaseSectionQuickAdapter + if (mAdapter != adapter) { + setUpWithAdapter(adapter) + } + val spanCount = layoutManager.spanCount + val position = parent.getChildAdapterPosition(view) - mAdapter!!.headerLayoutCount + val entity = adapter.getItem(position) + if (entity.isHeader) { + //不处理header + outRect[0, 0, 0] = 0 + return + } + val section = findSectionLastItemPos(position) + if (gapHSizePx < 0 || gapVSizePx < 0) { + transformGapDefinition(parent, spanCount) + } + outRect.top = gapVSizePx + outRect.bottom = 0 + + //下面的visualPos为单个Section内的视觉Pos + val visualPos = position + 1 - section.startPos + + when { + visualPos % spanCount == 1 -> { + //第一列 + outRect.left = sectionEdgeHPaddingPx + outRect.right = eachItemHPaddingPx - sectionEdgeHPaddingPx + } + visualPos % spanCount == 0 -> { + //最后一列 + outRect.left = eachItemHPaddingPx - sectionEdgeHPaddingPx + outRect.right = sectionEdgeHPaddingPx + } + else -> { + outRect.left = gapHSizePx - (eachItemHPaddingPx - sectionEdgeHPaddingPx) + outRect.right = eachItemHPaddingPx - outRect.left + } + } + + if (visualPos - spanCount <= 0) { + //第一行 + outRect.top = sectionEdgeVPaddingPx + } + + if (isLastRow(visualPos, spanCount, section.count)) { + //最后一行 + outRect.bottom = sectionEdgeVPaddingPx + } + + } else { + super.getItemOffsets(outRect, view, parent, state) + } + } + + private fun setUpWithAdapter(adapter: BaseSectionQuickAdapter?) { + mAdapter?.unregisterAdapterDataObserver(mDataObserver) + mAdapter = adapter + mAdapter?.registerAdapterDataObserver(mDataObserver) + markSections() + } + + private fun markSections() { + if (mAdapter != null) { + val adapter: BaseSectionQuickAdapter = + mAdapter as BaseSectionQuickAdapter + mSectionList.clear() + var sectionEntity: SectionEntity + var section = Section() + var i = 0 + val size = adapter.itemCount + while (i < size) { + sectionEntity = adapter.getItem(i) + if (sectionEntity.isHeader) { + //找到新Section起点 + if (i != 0) { + //已经有待添加的section + section.endPos = i - 1 + mSectionList.add(section) + } + section = Section() + section.startPos = i + 1 + } else { + section.endPos = i + } + i++ + } + //处理末尾情况 + if (!mSectionList.contains(section)) { + mSectionList.add(section) + } + } + } + + private fun transformGapDefinition(parent: RecyclerView, spanCount: Int) { + val displayMetrics = DisplayMetrics() + parent.display.getMetrics(displayMetrics) + gapHSizePx = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, gapHorizontalDp, displayMetrics) + .toInt() + + gapVSizePx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + gapVerticalDp, + displayMetrics + ).toInt() + + sectionEdgeHPaddingPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + sectionEdgeHPaddingDp, + displayMetrics + ).toInt() + + sectionEdgeVPaddingPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + sectionEdgeVPaddingDp, + displayMetrics + ).toInt() + eachItemHPaddingPx = (sectionEdgeHPaddingPx * 2 + gapHSizePx * (spanCount - 1)) / spanCount + } + + private fun findSectionLastItemPos(curPos: Int): Section { + for (section in mSectionList) { + if (section.contains(curPos)) { + return section + } + } + return Section() + } + + private fun isLastRow(visualPos: Int, spanCount: Int, sectionItemCount: Int): Boolean { + var lastRowCount = sectionItemCount % spanCount + lastRowCount = if (lastRowCount == 0) spanCount else lastRowCount + return visualPos > sectionItemCount - lastRowCount + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/decoration/GridSpaceItemDecoration.kt b/base/src/main/java/com/example/base/decoration/GridSpaceItemDecoration.kt new file mode 100644 index 0000000..9bc686d --- /dev/null +++ b/base/src/main/java/com/example/base/decoration/GridSpaceItemDecoration.kt @@ -0,0 +1,63 @@ +package com.example.base.decoration + +import android.graphics.Rect +import android.util.Log +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + + +/** + * 描述 : RecyclerView GridLayoutManager 等间距。 + * + * + * 等间距需满足两个条件: + * 1.各个模块的大小相等,即 各列的left+right 值相等; + * 2.各列的间距相等,即 前列的right + 后列的left = 列间距; + * + * + * 在[.getItemOffsets] 中针对 outRect 的left 和right 满足这两个条件即可 + * + * + * 作者 : shiguotao + * 版本 : V1 + * 创建时间 : 2020/3/19 4:54 PM + */ + +/** + * @param mSpanCount 列数 + * @param mRowSpacing 行间距 + * @param mColumnSpacing 列间距 + */ +class GridSpaceItemDecoration( + private val mSpanCount: Int,//横条目数量 + private val mRowSpacing: Int, //行间距 + private val mColumnSpacing: Int// 列间距 +) : + ItemDecoration() { + private val TAG = "GridSpaceItemDecoration" + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = parent.getChildAdapterPosition(view) // 获取view 在adapter中的位置。 + val column = position % mSpanCount // view 所在的列 + outRect.left = column * mColumnSpacing / mSpanCount // column * (列间距 * (1f / 列数)) + outRect.right = + mColumnSpacing - (column + 1) * mColumnSpacing / mSpanCount // 列间距 - (column + 1) * (列间距 * (1f /列数)) + Log.e( + TAG, "position:" + position + + " columnIndex: " + column + + " left,right ->" + outRect.left + "," + outRect.right + ) + + // 如果position > 行数,说明不是在第一行,则不指定行高,其他行的上间距为 top=mRowSpacing + if (position >= mSpanCount) { + outRect.top = mRowSpacing // item top + } + } + +} diff --git a/base/src/main/java/com/example/base/decoration/SpacesItemDecoration.kt b/base/src/main/java/com/example/base/decoration/SpacesItemDecoration.kt new file mode 100644 index 0000000..2785421 --- /dev/null +++ b/base/src/main/java/com/example/base/decoration/SpacesItemDecoration.kt @@ -0,0 +1,32 @@ +package com.example.base.decoration + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +/** + * 设置上下间隔&左右间隔 + */ +class SpacesItemDecoration( + private val space: Int, + @RecyclerView.Orientation + private val orientation: Int = RecyclerView.VERTICAL +) : ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + // 第一个ItemView不需要在上面绘制分割线 + if (parent.getChildLayoutPosition(view) != 0) { + if (orientation == RecyclerView.VERTICAL) { + outRect.top = space + } else { + outRect.left = space + } + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/decoration/StaggeredSpaceItemDecoration.kt b/base/src/main/java/com/example/base/decoration/StaggeredSpaceItemDecoration.kt new file mode 100644 index 0000000..f2f40ea --- /dev/null +++ b/base/src/main/java/com/example/base/decoration/StaggeredSpaceItemDecoration.kt @@ -0,0 +1,65 @@ +package com.example.base.decoration + +import android.R.attr.divider +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import androidx.recyclerview.widget.StaggeredGridLayoutManager + + +/** + * 描述 : RecyclerView GridLayoutManager 等间距。 + * + * + * 等间距需满足两个条件: + * 1.各个模块的大小相等,即 各列的left+right 值相等; + * 2.各列的间距相等,即 前列的right + 后列的left = 列间距; + * + * + * 在[.getItemOffsets] 中针对 outRect 的left 和right 满足这两个条件即可 + * + * + * 作者 : shiguotao + * 版本 : V1 + * 创建时间 : 2020/3/19 4:54 PM + */ + +/** + * @param mSpanCount 列数 + * @param mRowSpacing 行间距 + * @param mColumnSpacing 列间距 + */ +class StaggeredSpaceItemDecoration( + private val mSpanCount: Int,//横条目数量 + private val mRowSpacing: Int, //行间距 + private val mColumnSpacing: Int// 列间距 +) : + ItemDecoration() { + private val TAG = "StaggeredSpaceItemDecoration" + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val layoutParams = view.layoutParams as StaggeredGridLayoutManager.LayoutParams + val spanIndex = layoutParams.getSpanIndex() + if (spanIndex == 0) { + // left + outRect.left = mRowSpacing + outRect.right = mRowSpacing / 2 + + outRect.top = mColumnSpacing + outRect.bottom = mColumnSpacing / 2 + } else { + outRect.right = mRowSpacing + outRect.left = mRowSpacing / 2 + + outRect.bottom = mColumnSpacing + outRect.top = mColumnSpacing / 2 + } + } + +} diff --git a/base/src/main/java/com/example/base/dialog/CropImageContract.kt b/base/src/main/java/com/example/base/dialog/CropImageContract.kt new file mode 100644 index 0000000..f0834c3 --- /dev/null +++ b/base/src/main/java/com/example/base/dialog/CropImageContract.kt @@ -0,0 +1,92 @@ +package com.example.base.dialog + +import android.app.Activity +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.webkit.MimeTypeMap +import androidx.activity.result.contract.ActivityResultContract +import java.io.File + +class CropImageContract : ActivityResultContract() { + var outUri: Uri? = null + + //构建意图 + override fun createIntent(context: Context, input: CropImageResult): Intent { + //把CropImageResult转换成裁剪图片的意图 + val intent = Intent("com.android.camera.action.CROP") + val mimeType = context.contentResolver.getType(input.uri) + val imageName = "${System.currentTimeMillis()}.${ + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) + }" + outUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val values = ContentValues() + values.put(MediaStore.MediaColumns.DISPLAY_NAME, imageName) + values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) + values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) + context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + } else { + Uri.fromFile(File(context.externalCacheDir!!.absolutePath, imageName)) + } + context.grantUriPermission( + context.packageName, + outUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.putExtra("noFaceDetection", true) //去除默认的人脸识别,否则和剪裁匡重叠 + intent.setDataAndType(input.uri, mimeType) + intent.putExtra("crop", "true") // crop=true 有这句才能出来最后的裁剪页面. + intent.putExtra("output", outUri) + intent.putExtra("outputFormat", "JPEG") // 返回格式 + intent.putExtra("return-data", false) + intent.putExtra("scale", true) + + if (input.outputX != 0 && input.outputY != 0) { + intent.putExtra("outputX", input.outputX) + intent.putExtra("outputY", input.outputY) + } + + if (input.aspectX != 0 && input.aspectY != 0) { + if (input.aspectY == input.aspectX && Build.MANUFACTURER == "HUAWEI") { + intent.putExtra("aspectX", 9999) + intent.putExtra("aspectY", 9998) + } else { + intent.putExtra("aspectX", input.aspectX) + intent.putExtra("aspectY", input.aspectY) + } + } + return intent + } + + //接收意图并处理数据 + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return when (resultCode) { + Activity.RESULT_OK -> outUri!! + else -> null + } + } + +} + +/** + * uri:需要裁剪的图片 + * aspect:长宽比例 + * output:图片输出长宽 + * uri 要裁剪的图片 + * aspect 剪裁比例 + * output 输入图片长宽 + */ +class CropImageResult( + val uri: Uri, + val aspectX: Int = 0, + val aspectY: Int = 0, + @androidx.annotation.IntRange(from = 0, to = 1024) + val outputX: Int = 0, + @androidx.annotation.IntRange(from = 0, to = 1024) + val outputY: Int = 0 +) \ No newline at end of file diff --git a/base/src/main/java/com/example/base/dialog/DateDialogFragment.kt b/base/src/main/java/com/example/base/dialog/DateDialogFragment.kt new file mode 100644 index 0000000..7ce9f63 --- /dev/null +++ b/base/src/main/java/com/example/base/dialog/DateDialogFragment.kt @@ -0,0 +1,166 @@ +package com.example.base.dialog + +import android.app.Dialog +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CalendarView +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import com.example.base.R +import com.example.base.extensions.getYYYYMM2 +import com.example.base.extensions.getYYYYMMDD2 +import com.example.base.extensions.onClick +import com.example.base.widget.MonthView +import org.jetbrains.anko.find +import java.util.Calendar + +/** + * 这是一个日期选择控件 + */ + +class DateDialogFragment : DialogFragment() { + + annotation class DateMode { + companion object { + const val NONE = -1 + const val YEAR_MONTH_DAY = 0 + const val YEAR_MONTH = 1 + const val MONTH_DAY = 2 + } + } + + private var mCurrentTime: Long = 0 + private var mStartDate: Long = 0 + private var mEndDate: Long = 0 + private var mDateMode: Int = 0 + + private var mOnSelectedListener: ((time: Long) -> Unit)? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + dialog?.window?.setBackgroundDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.shape_white_cor8)) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + mCurrentTime = arguments?.getLong(ARG_TIME, System.currentTimeMillis()) ?: 0 + mStartDate = arguments?.getLong(START_DATE, 0)!! + mEndDate = arguments?.getLong(END_DATE, System.currentTimeMillis())!! + mDateMode = arguments?.getInt(DATE_MODE, DateMode.YEAR_MONTH_DAY)!! + + + val v = View.inflate(context, R.layout.mvvm_dialog_date, null) + initView(v) + + v.find(R.id.tv_ok).onClick { + mOnSelectedListener?.invoke(mCurrentTime) + dismiss() + } + v.find(R.id.tv_cancel).onClick { + dismiss() + } + + val mDialog = AlertDialog.Builder(requireContext()) + mDialog.setView(v) + return mDialog.create() + } + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(dm) + dialog!!.window!!.setLayout( + (dm.widthPixels * 0.8).toInt(), + dialog!!.window!!.attributes.height + ) + } + + fun setOnSelectedListener(listener: ((time: Long) -> Unit)) { + mOnSelectedListener = listener + } + + private fun initView(view: View) { + + val tvTime = view.findViewById(R.id.tvTime) + val calendarView = view.findViewById(R.id.calendarView) + val monthView = view.findViewById(R.id.monthView) + + val calendar = Calendar.getInstance() + calendar.timeInMillis = mCurrentTime + if (mDateMode == DateMode.YEAR_MONTH) { + calendarView.visibility = View.GONE + monthView.visibility = View.VISIBLE + tvTime.text = mCurrentTime.getYYYYMM2() + monthView.setRange(mStartDate, mEndDate) + monthView.setDate(mCurrentTime) + monthView.setOnMonthSelectedListener { year, month -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, 1) + mCurrentTime = calendar.time.time + tvTime.text = mCurrentTime.getYYYYMM2() + } + } else { + calendarView.visibility = View.VISIBLE + monthView.visibility = View.GONE + tvTime.text = mCurrentTime.getYYYYMMDD2() + calendarView.minDate = mStartDate + calendarView.maxDate = mEndDate + calendarView.date = mCurrentTime + calendarView.firstDayOfWeek = Calendar.MONDAY + calendarView.setOnDateChangeListener { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + mCurrentTime = calendar.time.time + tvTime.text = mCurrentTime.getYYYYMMDD2() + } + } + } + + override fun onDestroy() { + super.onDestroy() + mOnSelectedListener = null + } + + companion object { + private const val ARG_TIME = "time" + private const val START_DATE = "start_date" + private const val END_DATE = "end_date" + private const val DATE_MODE = "date_mode" + fun newInstance( + time: Long = System.currentTimeMillis(), + startDate: Long = 0, + endDate: Long = System.currentTimeMillis(), + dateMode: Int = DateMode.YEAR_MONTH_DAY + ): DateDialogFragment { + val arg = Bundle() + arg.putLong(ARG_TIME, time) + arg.putLong(START_DATE, startDate) + arg.putLong(END_DATE, endDate) + arg.putInt(DATE_MODE, dateMode) + val fragment = DateDialogFragment() + fragment.arguments = arg + return fragment + } + } + +} + + + + + + + + + + + + + diff --git a/base/src/main/java/com/example/base/dialog/DateTimeDialogFragment.kt b/base/src/main/java/com/example/base/dialog/DateTimeDialogFragment.kt new file mode 100644 index 0000000..38124d3 --- /dev/null +++ b/base/src/main/java/com/example/base/dialog/DateTimeDialogFragment.kt @@ -0,0 +1,126 @@ +package com.example.base.dialog + +import android.app.Dialog +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.View +import android.widget.DatePicker +import android.widget.TextView +import android.widget.TimePicker +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.example.base.R +import com.example.base.extensions.getYYYYMMDDHHMM +import com.example.base.extensions.onClick +import org.jetbrains.anko.find +import java.util.Calendar + +/** + * 这是一个日期选择控件 + */ + +class DateTimeDialogFragment : DialogFragment() { + + private var mCurrentTime: Long = 0 + + private var mOnSelectedListener: ((time: Long) -> Unit)? = null + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + mCurrentTime = arguments?.getLong(ARG_TIME, System.currentTimeMillis()) ?: 0 + + + val v = View.inflate(context, R.layout.mvvm_dialog_date_time, null) + initView(v) + + v.find(R.id.tv_ok).onClick { + mOnSelectedListener?.invoke(mCurrentTime) + dismiss() + } + v.find(R.id.tv_cancel).onClick { + dismiss() + } + + val mDialog = AlertDialog.Builder(requireContext()) + mDialog.setView(v) + + return mDialog.create() + } + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(dm) + dialog!!.window!!.setLayout( + (dm.widthPixels * 0.8).toInt(), + dialog!!.window!!.attributes.height + ) + } + + fun setOnSelectedListener(listener: ((time: Long) -> Unit)) { + mOnSelectedListener = listener + } + + private fun initView(view: View) { + + val tvTime = view.findViewById(R.id.tvTime) + val datePicker = view.findViewById(R.id.datePicker) + val timePicker = view.findViewById(R.id.timePicker) + + + val calendar = Calendar.getInstance() + calendar.timeInMillis = mCurrentTime + + tvTime.text = mCurrentTime.getYYYYMMDDHHMM() + + datePicker.init(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)) { _, year, monthOfYear, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, monthOfYear) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + val time = calendar.time.time + tvTime.text = time.getYYYYMMDDHHMM() + mCurrentTime = time + } + + + timePicker.setOnTimeChangedListener { _, hourOfDay, minute -> + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) + calendar.set(Calendar.MINUTE, minute) + val time = calendar.time.time + tvTime.text = time.getYYYYMMDDHHMM() + mCurrentTime = time + + } + } + + override fun onDestroy() { + super.onDestroy() + mOnSelectedListener = null + } + + companion object { + private const val ARG_TIME = "time" + fun newInstance(time: Long = System.currentTimeMillis()): DateTimeDialogFragment { + val arg = Bundle() + arg.putLong(ARG_TIME, time) + val fragment = DateTimeDialogFragment() + fragment.arguments = arg + return fragment + } + } + +} + + + + + + + + + + + + + diff --git a/base/src/main/java/com/example/base/dialog/LoadingDialog.kt b/base/src/main/java/com/example/base/dialog/LoadingDialog.kt new file mode 100644 index 0000000..6289f49 --- /dev/null +++ b/base/src/main/java/com/example/base/dialog/LoadingDialog.kt @@ -0,0 +1,63 @@ +package com.example.base.dialog + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.view.View +import android.view.Window +import android.view.WindowManager +import android.view.animation.Animation +import android.view.animation.Animation.REVERSE +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatDialog +import com.example.base.R + + +/** + * @date 2021-08-12 13:50 + * @desc 自定义进度对话框 + */ +class LoadingDialog(context: Context?) : AppCompatDialog(context!!) { + + init { + + // 禁止取消 + setCancelable(false) + + // 去掉对话框顶部栏 + requestWindowFeature(Window.FEATURE_NO_TITLE) + + setContentView(R.layout.mvvm_dialog_loading) + + //设置dialog靠近顶部 + window?.setGravity(Gravity.TOP) + + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + //设置margin为屏幕的40% + val lps: WindowManager.LayoutParams? = window?.attributes + lps?.verticalMargin = 0.4f + window?.attributes = lps + + startAnim() + } + + private fun startAnim() { + val imageView = findViewById(R.id.ivLoading) + val anim = RotateAnimation(0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) + anim.repeatCount = Animation.INFINITE + anim.duration = 1000 + anim.interpolator = LinearInterpolator() + imageView?.startAnimation(anim) + } + + fun setMessage(msg: String) { + val textView = findViewById(R.id.tvMessage) + textView?.visibility = View.VISIBLE + textView?.text = msg + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/dialog/SelectPhotoDialogFragment.kt b/base/src/main/java/com/example/base/dialog/SelectPhotoDialogFragment.kt new file mode 100644 index 0000000..e65c546 --- /dev/null +++ b/base/src/main/java/com/example/base/dialog/SelectPhotoDialogFragment.kt @@ -0,0 +1,118 @@ +package com.example.base.dialog + +import android.app.AlertDialog +import android.app.Dialog +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.DialogFragment +import com.example.base.R +import com.example.base.extensions.toGrantUri +import com.example.base.utils.Utils +import java.io.File +import java.util.UUID + +/** + * time: 2021/08/11 + * desc: 调用拍照选择单一图片 + */ +class SelectPhotoDialogFragment : DialogFragment() { + + private var mOnBackListener: ((uri: Uri) -> Unit)? = null //回调事件 + + private val isCrop by lazy { arguments?.getBoolean(ARG_IS_CROP) ?: false } + + // 拍照用的uri + private var cameraUri: Uri? = null + + private val ratio by lazy { arguments?.getString(CORP_RATIO) ?: "" } + + init { + val file = File(Utils.getApp().externalCacheDir, "${UUID.randomUUID()}.jpg") + cameraUri = file.toGrantUri() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + val view = View.inflate(requireContext(), R.layout.mvvm_dialog_select_photo, null) + val dialog = AlertDialog.Builder(requireContext()) + dialog.setView(view) + + // 拍照 + view.findViewById(R.id.tvCamera).setOnClickListener { + takePicture.launch(cameraUri) + } + // 相册 + view.findViewById(R.id.tvGallery).setOnClickListener { + getContent.launch("image/*") + } + + return dialog.create() + } + + // 拍照 + private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { + if (it) { + if (isCrop) { + if (ratio.isNotEmpty()) { + val aspectX = ratio.split("-")[0].toInt() + val aspectY = ratio.split("-")[1].toInt() + cropImageContent.launch(CropImageResult(cameraUri!!, aspectX = aspectX, aspectY = aspectY)) + } else { + cropImageContent.launch(CropImageResult(cameraUri!!)) + } + + } else { + mOnBackListener?.invoke(cameraUri!!) + dismiss() + } + } + } + + // 相册 + private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { + if (it != null) { + if (isCrop) { + if (ratio.isNotEmpty()) { + val aspectX = ratio.split("-")[0].toInt() + val aspectY = ratio.split("-")[1].toInt() + cropImageContent.launch(CropImageResult(it, aspectX = aspectX, aspectY = aspectY)) + } else { + cropImageContent.launch(CropImageResult(it)) + } + } else { + mOnBackListener?.invoke(it) + dismiss() + } + } + + } + + // 裁剪 + private val cropImageContent = registerForActivityResult(CropImageContract()) { + it?.let { + mOnBackListener?.invoke(it) + dismiss() + } + } + + // 成功回调 + fun setOnSelectListener(listener: ((uri: Uri) -> Unit)) { + mOnBackListener = listener + } + + companion object { + private const val ARG_IS_CROP = "arg_is_crop" + private const val CORP_RATIO = "ratio" + fun newInstance(isCrop: Boolean = false, format: String? = null): SelectPhotoDialogFragment { + val arg = Bundle() + arg.putBoolean(ARG_IS_CROP, isCrop) + arg.putString(CORP_RATIO, format) + val fragment = SelectPhotoDialogFragment() + fragment.arguments = arg + return fragment + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/dialog/TimeDialogFragment.kt b/base/src/main/java/com/example/base/dialog/TimeDialogFragment.kt new file mode 100644 index 0000000..2fabad0 --- /dev/null +++ b/base/src/main/java/com/example/base/dialog/TimeDialogFragment.kt @@ -0,0 +1,112 @@ +package com.example.base.dialog + +import android.app.Dialog +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.View +import android.widget.TextView +import android.widget.TimePicker +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.example.base.R +import com.example.base.extensions.getHHMM +import com.example.base.extensions.onClick +import org.jetbrains.anko.find +import java.util.Calendar + +/** + * 这是一个时间选择控件 + */ + +class TimeDialogFragment : DialogFragment() { + + private var mCurrentTime: Long = 0 + + private var mOnSelectedListener: ((time: Long) -> Unit)? = null + + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + mCurrentTime = arguments?.getLong(ARG_TIME, System.currentTimeMillis()) ?: 0 + + + val v = View.inflate(context, R.layout.mvvm_dialog_time, null) + initView(v) + + v.find(R.id.tv_ok).onClick { + mOnSelectedListener?.invoke(mCurrentTime) + dismiss() + } + v.find(R.id.tv_cancel).onClick { + dismiss() + } + + val mDialog = AlertDialog.Builder(requireContext()) + mDialog.setView(v) + + return mDialog.create() + } + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(dm) + dialog!!.window!!.setLayout( + (dm.widthPixels * 0.8).toInt(), + dialog!!.window!!.attributes.height + ) + } + + fun setOnSelectedListener(listener: ((time: Long) -> Unit)) { + mOnSelectedListener = listener + } + + private fun initView(view: View) { + + val tvTime = view.findViewById(R.id.tvTime) + val timePicker = view.findViewById(R.id.timePicker) + timePicker.setIs24HourView(true) + val calendar = Calendar.getInstance() + calendar.timeInMillis = mCurrentTime + tvTime.text = mCurrentTime.getHHMM() + + timePicker.setOnTimeChangedListener { _, hourOfDay, minute -> + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) + calendar.set(Calendar.MINUTE, minute) + val time = calendar.time.time + tvTime.text = time.getHHMM() + mCurrentTime = time + } + } + + override fun onDestroy() { + super.onDestroy() + mOnSelectedListener = null + } + + companion object { + private const val ARG_TIME = "time" + fun newInstance(time: Long = System.currentTimeMillis()): TimeDialogFragment { + val arg = Bundle() + arg.putLong(ARG_TIME, time) + val fragment = TimeDialogFragment() + fragment.arguments = arg + return fragment + } + } + +} + + + + + + + + + + + + + diff --git a/base/src/main/java/com/example/base/dialog/YearMonthDialogFragment.kt b/base/src/main/java/com/example/base/dialog/YearMonthDialogFragment.kt new file mode 100644 index 0000000..4966f5d --- /dev/null +++ b/base/src/main/java/com/example/base/dialog/YearMonthDialogFragment.kt @@ -0,0 +1,115 @@ +package com.example.base.dialog + +import android.app.Dialog +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.View +import android.widget.DatePicker +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.example.base.R +import com.example.base.extensions.getYYYYMM +import java.util.Calendar + +/** + * 这是一个日期yyyyMM选择控件 + */ + +class YearMonthDialogFragment : DialogFragment() { + + private var mCurrentTime: Long = 0 + + private var mOnSelectedListener: ((time: Long) -> Unit)? = null + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + mCurrentTime = arguments?.getLong(ARG_TIME, System.currentTimeMillis()) ?: 0 + + val v = View.inflate(context, R.layout.mvvm_dialog_date, null) + initView(v) + + val mDialog = AlertDialog.Builder(requireContext()) + mDialog.setView(v) + mDialog.setNegativeButton("取消", null) + mDialog.setPositiveButton("确定") { _, _ -> + mOnSelectedListener?.invoke(mCurrentTime) + } + + + return mDialog.create() + } + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(dm) + dialog!!.window!!.setLayout( + (dm.widthPixels * 0.8).toInt(), + dialog!!.window!!.attributes.height + ) + } + + fun setOnSelectedListener(listener: ((time: Long) -> Unit)) { + mOnSelectedListener = listener + } + + private fun initView(view: View) { + + // 隐藏日选择 + val dayPickerView = view.findViewById(resources.getIdentifier("android:id/day", null, null)) + dayPickerView?.visibility = View.GONE + + val tvTime = view.findViewById(R.id.tvTime) + val datePicker = view.findViewById(R.id.datePicker) + + val calendar = Calendar.getInstance() + calendar.timeInMillis = mCurrentTime + + tvTime.text = mCurrentTime.getYYYYMM() + + datePicker.init( + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH) + ) { _, year, monthOfYear, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, monthOfYear) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + val time = calendar.time.time + tvTime.text = time.getYYYYMM() + mCurrentTime = time + } + } + + override fun onDestroy() { + super.onDestroy() + mOnSelectedListener = null + } + + companion object { + private const val ARG_TIME = "time" + fun newInstance(time: Long = System.currentTimeMillis()): YearMonthDialogFragment { + val arg = Bundle() + arg.putLong(ARG_TIME, time) + val fragment = YearMonthDialogFragment() + fragment.arguments = arg + return fragment + } + } + +} + + + + + + + + + + + + + diff --git a/base/src/main/java/com/example/base/extensions/ActivityExtensions.kt b/base/src/main/java/com/example/base/extensions/ActivityExtensions.kt new file mode 100644 index 0000000..92dba24 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ActivityExtensions.kt @@ -0,0 +1,108 @@ +package com.example.base.extensions + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.view.View +import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity + + +fun Activity.startActivity(clazz: Class) { + val intent = Intent(this, clazz) + this.startActivity(intent) +} + +fun Fragment.observeKeyboardChange(onChange: (isShowing: Boolean) -> Unit) { + requireActivity().observeKeyboardChange(onChange) +} + +fun Activity.observeKeyboardChange(onChange: (isShowing: Boolean) -> Unit) { + val rootView = this.window.decorView + val r = Rect() + var lastHeight = 0 + rootView.viewTreeObserver.addOnGlobalLayoutListener { + rootView.getWindowVisibleDisplayFrame(r) + val height = r.height() + if (lastHeight == 0) { + lastHeight = height + } else { + val diff = lastHeight - height + if (diff > 200) { + onChange(true) + lastHeight = height + } else if (diff < -200) { + onChange(false) + lastHeight = height + } + } + } +} + +/** + * 当前键盘显示状态 + */ +fun FragmentActivity.isKeyboardShown(): Boolean { + val rootView = this.window.decorView.findViewById(android.R.id.content) + + val softKeyboardHeight = 100 + val r = Rect() + rootView.getWindowVisibleDisplayFrame(r) + val dm = rootView.resources.displayMetrics + val heightDiff = rootView.bottom - r.bottom + return heightDiff > softKeyboardHeight * dm.density + +} + +/** + * WindowInsetsController + * WindowInsetsController是一个接口类,它主要作用就是控制窗口行为,此类在Android R(API 30)添加,意在简化原先代码动态修改窗口SystemBars(StatusBar&NavigationBar)表现。 + * 链接: https://juejin.cn/post/6940048488071856164 + * fun initSystemBarsByAndroidX() { + * var controller = ViewCompat.getWindowInsetsController(window.decorView) + * // 设置状态栏反色 + * controller?.isAppearanceLightStatusBars = true + * // 取消状态栏反色 + * controller?.isAppearanceLightStatusBars = false + * // 设置导航栏反色 + * controller?.isAppearanceLightNavigationBars = true + * // 取消导航栏反色 + * controller?.isAppearanceLightNavigationBars = false + * // 隐藏状态栏 + * controller?.hide(WindowInsets.Type.statusBars()) + * // 显示状态栏 + * controller?.show(WindowInsets.Type.statusBars()) + * // 隐藏导航栏 + * controller?.hide(WindowInsets.Type.navigationBars()) + * // 显示导航栏 + * controller?.show(WindowInsets.Type.navigationBars()) + * // 同时隐藏状态栏和导航栏 + * controller?.hide(WindowInsets.Type.systemBars()) + * // 同时隐藏状态栏和导航栏 + * controller?.show(WindowInsets.Type.systemBars()) + * } + */ +@SuppressLint("WrongConstant") +fun Activity.setStatusBarLight(isLight: Boolean) { + val controller = ViewCompat.getWindowInsetsController(window.decorView) + // 设置状态栏反色 + controller?.isAppearanceLightStatusBars = isLight +} + +fun Fragment.setStatusBarLight(isLight: Boolean) { + activity?.setStatusBarLight(isLight) + +} + +/** + * 获取当前是否为深色模式 + * 深色模式的值为:0x21 + * 浅色模式的值为:0x11 + * @return true 为是深色模式 false为不是深色模式 + */ +fun Context.isDarkMode(): Boolean { + return resources.configuration.uiMode == 0x21 +} diff --git a/base/src/main/java/com/example/base/extensions/AnyExtension.kt b/base/src/main/java/com/example/base/extensions/AnyExtension.kt new file mode 100644 index 0000000..da0f902 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/AnyExtension.kt @@ -0,0 +1,68 @@ +package com.example.base.extensions + +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import androidx.annotation.ColorRes +import androidx.collection.ArrayMap +import androidx.core.content.ContextCompat +import com.example.base.utils.Utils +import java.lang.reflect.Field + + +/** + * 对象转map + */ +fun Any.toMap(): ArrayMap { + val map: ArrayMap = ArrayMap() + + val declaredFields: Array = this.javaClass.declaredFields + for (field in declaredFields) { + field.isAccessible = true + val value = field.get(this) ?: continue + map[field.name] = value.toString() + } + + return map +} + +/** + * 获取颜色 + */ +fun Any.getColor(@ColorRes resId: Int): Int { + return ContextCompat.getColor(Utils.getApp(), resId) +} + +/** + * 全局toast + */ +fun Any.longToast(msg: CharSequence?) { + if (msg == null) return + val toast = Toast.makeText(Utils.getApp(), msg, Toast.LENGTH_LONG) + toast.show() +} + +/** + * 全局toast + */ +fun Any.toast(msg: CharSequence?) { + if (msg == null) return + val toast = Toast.makeText(Utils.getApp(), msg, Toast.LENGTH_SHORT) + toast.show() +} + +fun Any.toast(msg: CharSequence?, isBack: Boolean) { + if (msg == null) return + val message = msg.split(",") + if (message.isNotEmpty()) Toast.makeText(Utils.getApp(), message[0], Toast.LENGTH_SHORT).show() +} + +private var mainThread: Handler? = null +fun Any.runOnUiThread(delayMillis: Long = 0L, action: () -> Unit) { + if (mainThread == null) { + mainThread = Handler(Looper.getMainLooper()) + } + mainThread?.postDelayed({ + action.invoke() + }, delayMillis) +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/BitmapExtensions.kt b/base/src/main/java/com/example/base/extensions/BitmapExtensions.kt new file mode 100644 index 0000000..98c4de2 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/BitmapExtensions.kt @@ -0,0 +1,87 @@ +package com.example.base.extensions + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.ImageFormat +import android.graphics.Matrix +import android.graphics.Rect +import android.graphics.YuvImage +import android.media.Image +import android.view.View +import android.widget.LinearLayout +import java.io.ByteArrayOutputStream + +fun View.toBitmap(): Bitmap? { + // 获取布局的宽和高 + val width = measuredWidth + val height = measuredHeight + + // 创建一个与FrameLayout大小相同的空Bitmap + if (width <= 0 || height <= 0) return null // 防止宽度或高度为0时发生错误 + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + + // 创建一个新的Canvas用于将布局内容绘制到Bitmap上 + val canvas = Canvas(bitmap) + + // 将当前View绘制到Canvas上,从而将其内容渲染到Bitmap中 + draw(canvas) + + // 返回包含布局内容的Bitmap + return bitmap +} + +fun Image.toBitmaps(): Bitmap { + val yBuffer = planes[0].buffer // Y + val vuBuffer = planes[2].buffer // VU + + val ySize = yBuffer.remaining() + val vuSize = vuBuffer.remaining() + + val nv21 = ByteArray(ySize + vuSize) + + yBuffer.get(nv21, 0, ySize) + vuBuffer.get(nv21, ySize, vuSize) + + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) + val out = ByteArrayOutputStream() + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out) + val imageBytes = out.toByteArray() + return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) +} + +fun rotateBitmap(bitmap: Bitmap, rotationDegrees: Int): Bitmap { + val matrix = Matrix().apply { + postRotate(rotationDegrees.toFloat()) + } + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) +} + +/** + * 水印转bitmap + */ +fun renderWatermarkViewToBitmap(watermarkView: LinearLayout, width: Int, height: Int): Bitmap { + watermarkView.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY) + ) + watermarkView.layout(0, 0, watermarkView.measuredWidth, watermarkView.measuredHeight) + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + watermarkView.draw(canvas) + return bitmap +} + +/** + * 添加水印 + */ +fun addWatermarkToImage(originalImage: Bitmap, watermarkBitmap: Bitmap, markX: Float, markY: Float): Bitmap { + // 创建一个新的Bitmap,大小和原始图片一样 + val result = Bitmap.createBitmap(originalImage.width, originalImage.height, originalImage.config) + val canvas = Canvas(result) + // 绘制原始图片 + canvas.drawBitmap(originalImage, 0f, 0f, null) + // 绘制水印 + canvas.drawBitmap(watermarkBitmap, markX, markY, null) + return result +} diff --git a/base/src/main/java/com/example/base/extensions/BundleExtensions.kt b/base/src/main/java/com/example/base/extensions/BundleExtensions.kt new file mode 100644 index 0000000..87f860b --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/BundleExtensions.kt @@ -0,0 +1,40 @@ +package com.example.base.extensions + +import android.os.Bundle +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + + +/** + * intent 传object 数据 + */ +fun Bundle.putObject(any: Any?) { + any ?: return + this.putString(any::class.java.simpleName, Gson().toJson(any)) +} + +inline fun Bundle.getObject(): T? { + return try { + Gson().fromJson(this.getString(T::class.java.simpleName), T::class.java) + } catch (e: Exception) { + null + } +} + +/** + * intent 传list 数据 + */ +inline fun Bundle.putList(list: List?) { + this.putString("list${T::class.java.simpleName}", Gson().toJson(list)) +} + + +inline fun Bundle.getList(): List { + return try { + val str = this.getString("list${T::class.java.simpleName}") + Gson().fromJson(str, object : TypeToken?>() {}.type) + } catch (e: java.lang.Exception) { + emptyList() + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/ClickExtensions.kt b/base/src/main/java/com/example/base/extensions/ClickExtensions.kt new file mode 100644 index 0000000..67c2c17 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ClickExtensions.kt @@ -0,0 +1,16 @@ +package com.example.base.extensions + +import android.view.View + +fun View.onClick(debounceTime: Long = 500, action: (View) -> Unit) { + this.setOnClickListener(object : View.OnClickListener { + var lastClickTime = 0L + + override fun onClick(v: View) { + if (System.currentTimeMillis() - lastClickTime >= debounceTime) { + action(v) + lastClickTime = System.currentTimeMillis() + } + } + }) +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/CloseableExtensions.kt b/base/src/main/java/com/example/base/extensions/CloseableExtensions.kt new file mode 100644 index 0000000..ac74266 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/CloseableExtensions.kt @@ -0,0 +1,25 @@ +package com.example.base.extensions + +import java.io.Closeable +import java.io.IOException + +/** + * 安全关闭 + */ +fun Closeable.safeClose() { + try { + this.close() + } catch (e: IOException) { + e.printStackTrace() + } +} + +fun Closeable.closeQuietly() { + try { + this.close() + } catch (rethrown: RuntimeException) { + throw rethrown + } catch (ignored: Exception) { + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/ColorExtension.kt b/base/src/main/java/com/example/base/extensions/ColorExtension.kt new file mode 100644 index 0000000..c801799 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ColorExtension.kt @@ -0,0 +1,37 @@ +package com.example.base.extensions + +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.OvalShape + + +/** + * 返回圆形 drawable + * * + * @return + */ +fun Int.createOvalDrawable(): ShapeDrawable { + + val drawable = ShapeDrawable(OvalShape()) + drawable.paint.color = this + + return drawable +} + +fun Int.isLightColor(): Boolean { + val r = 0.299 * Color.red(this) + val g = 0.587 * Color.green(this) + val b = 0.114 * Color.blue(this) + val darkness = 1 - (r + g + b) / 255 + return darkness < 0.4 +} + +fun Int.toColorStateList(): ColorStateList { + val states = arrayOfNulls(2) + states[0] = intArrayOf() + states[1] = intArrayOf() + + val colors = intArrayOf(this, this) + return ColorStateList(states, colors) +} diff --git a/base/src/main/java/com/example/base/extensions/DoubleExtensions.kt b/base/src/main/java/com/example/base/extensions/DoubleExtensions.kt new file mode 100644 index 0000000..61ec644 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/DoubleExtensions.kt @@ -0,0 +1,32 @@ +package com.example.base.extensions + +import java.math.RoundingMode +import java.text.DecimalFormat + +fun Double.toTwoDecimal(): String { + val df = DecimalFormat() + df.applyPattern("#0.00") + df.roundingMode = RoundingMode.FLOOR + return df.format(this) +} + +fun Double.toTwoDecimalUP(): String { + val df = DecimalFormat() + df.applyPattern("#0.00") + df.roundingMode = RoundingMode.HALF_UP + return df.format(this) +} + +fun Float.toTwoDecimal(): String { + val df = DecimalFormat() + df.applyPattern("#0.00") + df.roundingMode = RoundingMode.FLOOR + return df.format(this) +} + +fun Float.toTwoDecimalUP(): String { + val df = DecimalFormat() + df.applyPattern("#0.00") + df.roundingMode = RoundingMode.HALF_UP + return df.format(this) +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/DrawableExtensions.kt b/base/src/main/java/com/example/base/extensions/DrawableExtensions.kt new file mode 100644 index 0000000..4929f97 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/DrawableExtensions.kt @@ -0,0 +1,47 @@ +package com.example.base.extensions + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.PixelFormat +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable + +/** + * drawable扩展工具类 + */ + + +/** + * drawable转bitmap + * + * @return bitmap + */ +fun Drawable.toBitmap(): Bitmap { + + if (this is BitmapDrawable) { + return this.bitmap + } + // 获取drawable长度、宽度 + val width = this.intrinsicWidth + val height = this.intrinsicHeight + + // 获取drawable的颜色 + val config = + if (this.opacity != PixelFormat.OPAQUE) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 + + + // 创建bitmap + val bitmap: Bitmap = if (width <= 0 || height <= 0) { + Bitmap.createBitmap(1, 1, config) + } else { + Bitmap.createBitmap(width, height, config) + } + // 创建bitmap画布 + val canvas = Canvas(bitmap) + this.setBounds(0, 0, canvas.width, canvas.height) + + // 将drawable 内容画到画布中 + this.draw(canvas) + return bitmap +} + diff --git a/base/src/main/java/com/example/base/extensions/EditTextExtensions.kt b/base/src/main/java/com/example/base/extensions/EditTextExtensions.kt new file mode 100644 index 0000000..7c417e0 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/EditTextExtensions.kt @@ -0,0 +1,103 @@ +package com.example.base.extensions + +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import java.math.RoundingMode +import java.text.DecimalFormat + + +/** + * 显示输入法 + */ +fun EditText.showKeyboard() { + this.requestFocus() + val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(this, InputMethodManager.SHOW_FORCED) +} + +fun EditText.hideKeyboard() { + this.requestFocus() + val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, InputMethodManager.SHOW_FORCED) +} + +/** + * 限制只能输入两位小数 + */ +fun EditText.setTwoDecimalLimit() { + + val df = DecimalFormat() + df.applyPattern("#0.00") + df.roundingMode = RoundingMode.FLOOR + + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + val length = s?.split(".")?.last()?.length ?: 0 + if (length > 2 && s?.contains(".") == true) { + val t = df.format(s.toString().toDouble()) + this@setTwoDecimalLimit.text = Editable.Factory.getInstance().newEditable(t) + this@setTwoDecimalLimit.setSelection(s.length - 1) + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + +} + +/** + * 把输入手机号格式化有空格的 + */ +fun EditText.setPhoneInput() { + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged( + charSequence: CharSequence?, + start: Int, + before: Int, + count: Int + ) { + if (charSequence == null || charSequence.trim().isEmpty()) return + val stringBuilder = StringBuilder() + for (i in charSequence.indices) { + if (i != 3 && i != 8 && charSequence[i] == ' ') { + continue + } else { + stringBuilder.append(charSequence[i]) + if ((stringBuilder.length == 4 || stringBuilder.length == 9) + && stringBuilder[stringBuilder.length - 1] != ' ' + ) { + stringBuilder.insert(stringBuilder.length - 1, ' ') + } + } + } + if (stringBuilder.toString() != charSequence.toString()) { + var index = start + 1 + if (stringBuilder[start] == ' ') { + if (before == 0) { + index++ + } else { + index-- + } + } else { + if (before == 1) { + index-- + } + } + this@setPhoneInput.setText(stringBuilder.toString()) + this@setPhoneInput.setSelection(index) + } + } + }) +} diff --git a/base/src/main/java/com/example/base/extensions/FileExtensions.kt b/base/src/main/java/com/example/base/extensions/FileExtensions.kt new file mode 100644 index 0000000..cf0b275 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/FileExtensions.kt @@ -0,0 +1,209 @@ +package com.example.base.extensions + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.graphics.Bitmap +import android.graphics.BitmapFactory.decodeFile +import android.net.Uri +import android.provider.MediaStore +import android.text.TextUtils +import androidx.core.content.FileProvider +import com.example.base.utils.Utils +import java.io.File +import java.util.Locale + + +/** + * file扩展工具类 + */ + +fun File?.rename(newName: String): Boolean { + // file is null then return false + if (this == null) return false + // file doesn't exist then return false + if (!this.exists()) return false + // the new name equals old name then return true + if (newName == this.name) return true + val newFile = File("${this.parent}${File.separator}$newName") + // the new name of file exists then return false + return (!newFile.exists() && this.renameTo(newFile)) +} + + +/** + * file 转 uri 7。0授权 + * + * @return Uri + */ +fun File.toGrantUri(): Uri { + + return FileProvider.getUriForFile( + Utils.getApp(), + "${Utils.getApp().packageName}.fileProvider", + this + ) +} + +/** + * 获取bitmap + * + * @return bitmap + */ +fun File.getBitmap(): Bitmap? = decodeFile(this.absolutePath) + + +/** + * 自动压缩图片压缩 + * @return File? + */ +/*fun File.compressImage(): File? { + + // /storage/emulated/0/Android/data/当前应用包名/cache/Pictures/UUID随机数.jpg + val cacheDir = Utils.getApp().externalCacheDir + val resultDir = File(cacheDir, Environment.DIRECTORY_PICTURES) + if (!resultDir.mkdirs() && (!resultDir.exists() || !resultDir.isDirectory)) { + return null + } + val tagImg = File(resultDir, "${UUID.randomUUID()}.jpg") + val bool = ImageCompressEngine(this, tagImg).compress() + return if (bool) tagImg else null +}*/ + +fun File.isJpeg(): Boolean = this.absolutePath.contains("jpeg") || this.absolutePath.contains("jpg") + + +fun File.getMIMEType(): String { + var type = "*/*" + val fName = this.name + + //获取后缀名前的分隔符"."在fName中的位置。 + val dotIndex = fName.lastIndexOf(".") + if (dotIndex < 0) { + return type + } + + /* 获取文件的后缀名*/ + val end = fName.substring(dotIndex, fName.length).toLowerCase(Locale.ROOT) + if (TextUtils.isEmpty(end)) { + return type + } + + //在MIME和文件类型的匹配表中找到对应的MIME类型。 + for (i in MIME_MapTable.indices) { + if (end == MIME_MapTable[i][0]) { + type = MIME_MapTable[i][1] + break + } + } + return type +} + + +/** + * + * 绝对路径转uri + * + * @return content Uri + */ +@SuppressLint("Recycle", "Range") +fun File.getAbsolutePath(): Uri? { + val filePath = this.absolutePath + val cursor = Utils.getApp().contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + arrayOf(MediaStore.Images.Media._ID), MediaStore.Images.Media.DATA + "=? ", + arrayOf(filePath), null + ) + return if (cursor != null && cursor.moveToFirst()) { + val id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)) + val baseUri = Uri.parse("content://media/external/images/media") + cursor.closeQuietly() + Uri.withAppendedPath(baseUri, "" + id) + } else { + cursor?.closeQuietly() + if (this.exists()) { + val values = ContentValues() + values.put(MediaStore.Images.Media.DATA, filePath) + Utils.getApp().contentResolver.insert( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + values + ) + } else { + null + } + } +} + + +private val MIME_MapTable = arrayOf( + // {后缀名,MIME类型} + arrayOf(".3gp", "video/3gpp"), + arrayOf(".apk", "application/vnd.android.package-archive"), + arrayOf(".asf", "video/x-ms-asf"), + arrayOf(".avi", "video/x-msvideo"), + arrayOf(".bin", "application/octet-stream"), + arrayOf(".bmp", "image/bmp"), + arrayOf(".c", "text/plain"), + arrayOf(".class", "application/octet-stream"), + arrayOf(".conf", "text/plain"), + arrayOf(".cpp", "text/plain"), + arrayOf(".doc", "application/msword"), + arrayOf(".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + arrayOf(".xls", "application/vnd.ms-excel"), + arrayOf(".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + arrayOf(".exe", "application/octet-stream"), + arrayOf(".gif", "image/gif"), + arrayOf(".gtar", "application/x-gtar"), + arrayOf(".gz", "application/x-gzip"), + arrayOf(".h", "text/plain"), + arrayOf(".htm", "text/html"), + arrayOf(".html", "text/html"), + arrayOf(".jar", "application/java-archive"), + arrayOf(".java", "text/plain"), + arrayOf(".jpeg", "image/jpeg"), + arrayOf(".jpg", "image/jpeg"), + arrayOf(".js", "application/x-javascript"), + arrayOf(".log", "text/plain"), + arrayOf(".m3u", "audio/x-mpegurl"), + arrayOf(".m4a", "audio/mp4a-latm"), + arrayOf(".m4b", "audio/mp4a-latm"), + arrayOf(".m4p", "audio/mp4a-latm"), + arrayOf(".m4u", "video/vnd.mpegurl"), + arrayOf(".m4v", "video/x-m4v"), + arrayOf(".mov", "video/quicktime"), + arrayOf(".mp2", "audio/x-mpeg"), + arrayOf(".mp3", "audio/x-mpeg"), + arrayOf(".mp4", "video/mp4"), + arrayOf(".mpc", "application/vnd.mpohun.certificate"), + arrayOf(".mpe", "video/mpeg"), + arrayOf(".mpeg", "video/mpeg"), + arrayOf(".mpg", "video/mpeg"), + arrayOf(".mpg4", "video/mp4"), + arrayOf(".mpga", "audio/mpeg"), + arrayOf(".msg", "application/vnd.ms-outlook"), + arrayOf(".ogg", "audio/ogg"), + arrayOf(".pdf", "application/pdf"), + arrayOf(".png", "image/png"), + arrayOf(".pps", "application/vnd.ms-powerpoint"), + arrayOf(".ppt", "application/vnd.ms-powerpoint"), + arrayOf(".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), + arrayOf(".prop", "text/plain"), + arrayOf(".rc", "text/plain"), + arrayOf(".rmvb", "audio/x-pn-realaudio"), + arrayOf(".rtf", "application/rtf"), + arrayOf(".sh", "text/plain"), + arrayOf(".tar", "application/x-tar"), + arrayOf(".tgz", "application/x-compressed"), + arrayOf(".txt", "text/plain"), + arrayOf(".wav", "audio/x-wav"), + arrayOf(".wma", "audio/x-ms-wma"), + arrayOf(".wmv", "audio/x-ms-wmv"), + arrayOf(".wps", "application/vnd.ms-works"), + arrayOf(".xml", "text/plain"), + arrayOf(".z", "application/x-compress"), + arrayOf(".zip", "application/x-zip-compressed"), + arrayOf("", "*/*") +) + + + + diff --git a/base/src/main/java/com/example/base/extensions/FloatExtensions.kt b/base/src/main/java/com/example/base/extensions/FloatExtensions.kt new file mode 100644 index 0000000..6772d27 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/FloatExtensions.kt @@ -0,0 +1,28 @@ +package com.example.base.extensions + +import android.content.Context +import android.util.TypedValue +import com.example.base.utils.Utils + +fun Float.dp2px(): Float { + + return this * Utils.getApp().resources.displayMetrics.density +} + +fun Float.dp2px(context: Context): Float { + + return this * context.resources.displayMetrics.density +} + +fun Float.sp2px(): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Utils.getApp().resources.displayMetrics) +} + +fun Float.sp2px(context: Context): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, context.resources.displayMetrics) +} + +fun Float.getTwoFormat(): String { + return if (this < 10) "0$this" else "$this" +} + diff --git a/base/src/main/java/com/example/base/extensions/FragmentExtension.kt b/base/src/main/java/com/example/base/extensions/FragmentExtension.kt new file mode 100644 index 0000000..584c2d7 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/FragmentExtension.kt @@ -0,0 +1,15 @@ +package com.example.base.extensions + +import android.app.Activity +import android.content.Intent +import androidx.fragment.app.Fragment + + +fun Fragment.startActivity(clazz: Class) { + val intent = Intent(requireContext(), clazz) + this.startActivity(intent) +} + + + + diff --git a/base/src/main/java/com/example/base/extensions/GsonExtensions.kt b/base/src/main/java/com/example/base/extensions/GsonExtensions.kt new file mode 100644 index 0000000..23b381f --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/GsonExtensions.kt @@ -0,0 +1,12 @@ +package com.example.base.extensions + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +inline fun Gson.toList(str: String): List { + return try { + Gson().fromJson(str, object : TypeToken?>() {}.type) + } catch (e: Exception) { + emptyList() + } +} diff --git a/base/src/main/java/com/example/base/extensions/ImageViewExtensions.kt b/base/src/main/java/com/example/base/extensions/ImageViewExtensions.kt new file mode 100644 index 0000000..8f980ce --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ImageViewExtensions.kt @@ -0,0 +1,15 @@ +package com.example.base.extensions + +import android.content.Context +import android.widget.ImageView +import com.example.base.utils.L + +fun ImageView.setImageResourceByName(context: Context, resourceName: String) { + val resourceId = context.resources.getIdentifier(resourceName, "mipmap", context.packageName) + if (resourceId != 0) { + setImageResource(resourceId) + } else { + L.e("setImageResourceByName", "Resource not found: $resourceName") +// setImageResource(R.drawable.default_image) + } +} diff --git a/base/src/main/java/com/example/base/extensions/InputStreamExtensions.kt b/base/src/main/java/com/example/base/extensions/InputStreamExtensions.kt new file mode 100644 index 0000000..b62f98a --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/InputStreamExtensions.kt @@ -0,0 +1,50 @@ +package com.example.base.extensions + +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream + +/** + * /** + * 将inputStream转化为file + * @param is + * @param file 要输出的文件目录 +*/ +public static void inputStream2File(InputStream is, File file) throws IOException { +OutputStream os = null; +try { +os = new FileOutputStream(file); +int len = 0; +byte[] buffer = new byte[8192]; + +while ((len = is.read(buffer)) != -1) { +os.write(buffer, 0, len); +} +} finally { +os.close(); +is.close(); +} +} + */ + +fun InputStream.toFile(file:File) :Boolean { + val dir = if (file.isDirectory) file.absolutePath else file.parent?:"" + File(dir).mkdirs() + var os: OutputStream? = null + try { + os = FileOutputStream(file) + var len = 0 + val buffer = ByteArray(8192) + while (this.read(buffer).also { len = it } != -1) { + os.write(buffer, 0, len) + } + } catch (e:Exception) { + return false + } finally { + os?.safeClose() + this.safeClose() + } + + return true +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/IntExtension.kt b/base/src/main/java/com/example/base/extensions/IntExtension.kt new file mode 100644 index 0000000..ee24332 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/IntExtension.kt @@ -0,0 +1,33 @@ +package com.example.base.extensions + +import android.content.Context +import android.util.TypedValue +import android.view.ViewGroup +import com.example.base.utils.Utils + +fun Int.dp2px(): Int { + if (toInt() in intArrayOf(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)) { + return this + } + return (this * Utils.getApp().resources.displayMetrics.density).toInt() +} + +fun Int.dp2px(context: Context): Int { + if (toInt() in intArrayOf(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)) { + return this + } + return (this * context.resources.displayMetrics.density).toInt() +} + +fun Int.sp2px(): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Utils.getApp().resources.displayMetrics) +} + +fun Int.sp2px(context: Context): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), context.resources.displayMetrics) +} + +fun Int.getTwoFormat(): String { + return if (this < 10) "0$this" else "$this" +} + diff --git a/base/src/main/java/com/example/base/extensions/IntentExtensions.kt b/base/src/main/java/com/example/base/extensions/IntentExtensions.kt new file mode 100644 index 0000000..6eaf0b0 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/IntentExtensions.kt @@ -0,0 +1,39 @@ +package com.example.base.extensions + +import android.content.Intent +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + + +/** + * intent 传object 数据 + */ +fun Intent.putObject(any:Any) { + this.putExtra(any::class.java.simpleName, Gson().toJson(any)) +} + +inline fun Intent.getObject(): T? { + return try { + Gson().fromJson(this.getStringExtra(T::class.java.simpleName), T::class.java) + } catch (e:Exception) { + null + } +} + +/** + * intent 传list 数据 + */ +inline fun Intent.putList(list:List?) { + this.putExtra("list${T::class.java.simpleName}", Gson().toJson(list)) +} + + +inline fun Intent.getList(): List { + return try { + val str = this.getStringExtra("list${T::class.java.simpleName}") + Gson().fromJson(str, object : TypeToken?>() {}.type) + } catch (e:java.lang.Exception) { + emptyList() + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/LongExtensions.kt b/base/src/main/java/com/example/base/extensions/LongExtensions.kt new file mode 100644 index 0000000..cea2114 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/LongExtensions.kt @@ -0,0 +1,214 @@ +package com.example.base.extensions + +import java.math.RoundingMode +import java.text.DecimalFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + + +fun Long.getYYYYMMDDHHMMSSSSS(): String { + val dateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDD4(): String { + val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDDHHMM3(): String { + val dateFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDDHHMMSS(): String { + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDDHHMMSS2(): String { + val dateFormat = SimpleDateFormat("yyyy.MM.dd HH:mm:ss", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDDHHMMSS3(): String { + val dateFormat = SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDDHHMM(): String { + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDDHHMM2(): String { + val dateFormat = SimpleDateFormat("yyyy.MM.dd HH:mm", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDD(): String { + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDD2(): String { + val dateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDD3(): String { + val dateFormat = SimpleDateFormat("yyyy年MM月dd日", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDD5(): String { + val dateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMM(): String { + val dateFormat = SimpleDateFormat("yyyy-MM", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMM2(): String { + val dateFormat = SimpleDateFormat("yyyy.MM", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMM3(): String { + val dateFormat = SimpleDateFormat("yyyy年MM月", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getMMDD(): String { + val dateFormat = SimpleDateFormat("MM-dd", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getMMDD2(): String { + val dateFormat = SimpleDateFormat("MM月dd日", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getMMDD3(): String { + val dateFormat = SimpleDateFormat("MM.dd", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getMMDD4(): String { + val dateFormat = SimpleDateFormat("MM/dd", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYY(): String { + val dateFormat = SimpleDateFormat("yyyy", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getHHMMSS(): String { + val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getHHMM(): String { + val dateFormat = SimpleDateFormat("HH:mm", Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getYYYYMMDDHHMM(format: String): String { + val dateFormat = SimpleDateFormat(format, Locale.CHINA) + return dateFormat.format(this) +} + +fun Long.getDate(): Date { + return Date(this) +} + +fun Long.formatWeek(): String { + val calendar = Calendar.getInstance() + calendar.timeInMillis = this + return when (calendar.get(Calendar.DAY_OF_WEEK)) { + 1 -> "星期日" + 2 -> "星期一" + 3 -> "星期二" + 4 -> "星期三" + 5 -> "星期四" + 6 -> "星期五" + 7 -> "星期六" + else -> "" + } +} + +fun Long.getMessageDate(): String { + //所在时区时8,系统初始时间是1970-01-01 80:00:00,注意是从八点开始,计算的时候要加回去 + val offSet = Calendar.getInstance().timeZone.rawOffset + val today = (System.currentTimeMillis() + offSet) / 86400000 + val start = (this + offSet) / 86400000 + //-2:前天,-1:昨天,0:今天,1:明天,2:后天 + val strDes = when (start - today) { + 0L -> this.getHHMM() //今天 + -1L -> "昨天 ${this.getHHMM()}" + else -> this.getYYYYMMDDHHMM()//直接显示时间 + } + return strDes +} + +fun Long.getCourseMMSS(): String { + val mm = this / 60 + val ss = this % 60 + val ssStr = if (ss <= 9) "0$ss" else "$ss" + return String.format("%2d:%s", mm, ssStr) +} + +fun Long.getCourseHHMMSS(): String { + + val hour = this / 60 / 60 // watchHours秒 + val minute = (this - (hour * 60 * 60)) / 60 + val second = (this - (hour * 60 * 60)) - (minute * 60) + val mm = this / 60 + val ss = this % 60 + val mmStr = if (mm <= 9) "0$mm" else "$mm" + val ssStr = if (ss <= 9) "0$ss" else "$ss" + return "${hour}小时${mmStr}分${ssStr}秒" +} + + +//转换文件大小 +fun Long.formatFileSize(): String { + val df = DecimalFormat("#.00") + return when { + this <= 0 -> "0B" + this < 1024 -> "${this}B" + this < 1048576 -> df.format(this / 1024.0) + "K" + this < 1073741824 -> df.format(this / 1048576.0) + "M" + else -> df.format(this / 1073741824.0) + "G" + } +} + +//转换文件大小 +fun Long.formatFileSizeM(): String { + val df = DecimalFormat("#.00") + return df.format(this / (1024 * 1024.0)) +} + +// 转成 K、M 一位小数 +fun Long.toK_M(): String { + val df = DecimalFormat() + df.applyPattern("#0.0") + df.roundingMode = RoundingMode.UP + + val unit: String + if (this in 0 until 1024 * 1024) { + unit = "K" + return df.format(this / 1024.0) + unit + } + if (this >= 1024 * 1024) { + unit = "M" + return df.format(this / (1024 * 1024.0)) + unit + } + return "" + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/RecyclerViewExtensions.kt b/base/src/main/java/com/example/base/extensions/RecyclerViewExtensions.kt new file mode 100644 index 0000000..4af5198 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/RecyclerViewExtensions.kt @@ -0,0 +1,32 @@ +package com.example.base.extensions + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +/** + * RecyclerView滚动定位到item,并使其置顶 + */ +fun RecyclerView.moveToPosition(position: Int) { + if (layoutManager is LinearLayoutManager) { + this.stopScroll() + (layoutManager as LinearLayoutManager).scrollToPositionWithOffset(position, 0) + (layoutManager as LinearLayoutManager).stackFromEnd = false + } + +} + +/** + * 静止,没有滚动 SCROLL_STATE_IDLE = 0; + * 正在被外部拖拽,一般为用户正在用手指滚动 SCROLL_STATE_DRAGGING = 1; + * 自动滚动开始 SCROLL_STATE_SETTLING = 2; + */ +fun RecyclerView.setScrollCloseKeyboard() { + this.addOnScrollListener(object: RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState ==RecyclerView.SCROLL_STATE_DRAGGING) { + recyclerView.closeKeyboard() + } + } + }) +} diff --git a/base/src/main/java/com/example/base/extensions/RequestBodyExtensions.kt b/base/src/main/java/com/example/base/extensions/RequestBodyExtensions.kt new file mode 100644 index 0000000..a884812 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/RequestBodyExtensions.kt @@ -0,0 +1,25 @@ +package com.example.base.extensions + +import okhttp3.Request +import okio.Buffer +import java.io.IOException +import java.nio.charset.Charset + + +fun Request.bodyStr(): String { + val requestBody = this.body + val buffer = Buffer() + try { + requestBody?.writeTo(buffer) + } catch (e: IOException) { + e.printStackTrace() + return "" + } + //编码设为UTF-8 + var charset = Charset.forName("UTF-8") + val contentType = requestBody?.contentType() + if (contentType != null) { + charset = contentType.charset(Charset.forName("UTF-8")) + } + return buffer.readString(charset) +} diff --git a/base/src/main/java/com/example/base/extensions/StringExtensions.kt b/base/src/main/java/com/example/base/extensions/StringExtensions.kt new file mode 100644 index 0000000..f825561 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/StringExtensions.kt @@ -0,0 +1,69 @@ +package com.example.base.extensions + +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.Locale + + +/** + * 获取字符串的 MD5 + */ + +fun String.getMD5LowerCase(): String { + val hexString = StringBuilder() + try { + //1 创建一个提供信息摘要算法的对象,初始化为md5算法对象 + val md = MessageDigest.getInstance("MD5") + + //2 将消息变成byte数组 + val input = this.toByteArray(Charsets.UTF_8) + + //3 计算后获得字节数组,这就是那128位了 + val buff = md.digest(input) + + //4 把数组每一字节(一个字节占八位)换成16进制连成md5字符串 + for (b in buff) { + hexString.append(String.format("%02X", b)) + } + + } catch (e: Exception) { + e.printStackTrace() + } + return hexString.toString().lowercase(Locale.ROOT) //转小写 +} + + +fun String.toDoubleByTryCatch(): Double { + return try { + this.toDouble() + } catch (e: Exception) { + 0.00 + } +} + +fun String.utcToLocalTime(): Long { + var time = 0L + try { + val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + time = df.parse(this)?.time ?: 0 + } catch (e: java.lang.Exception) { + } + return time +} + +fun String.utcToLocalTime2(): Long { + var time = 0L + try { + val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'+0800'", Locale.CHINA) + time = df.parse(this)?.time ?: 0 + } catch (e: java.lang.Exception) { + } + return time +} + +fun String.maskPhoneNumber(): String { + if (length != 11) { + throw IllegalArgumentException("Invalid phone number length. Expected 11 digits.") + } + return substring(0, 3) + "****" + substring(7) +} diff --git a/base/src/main/java/com/example/base/extensions/TextViewExtensions.kt b/base/src/main/java/com/example/base/extensions/TextViewExtensions.kt new file mode 100644 index 0000000..02a3264 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/TextViewExtensions.kt @@ -0,0 +1,23 @@ +package com.example.base.extensions + +import android.os.Handler +import android.widget.TextView + +fun TextView.showTextOneByOne(text: String, interval: Long = 50, onCompleted: ((Int) -> Unit)? = null) { + var index = 0 + val handler = Handler() + val runnable: Runnable = object : Runnable { + override fun run() { + if (index < text.length) { + val ch = text[index] + append(ch.toString()) + index++ + handler.postDelayed(this, interval) + onCompleted?.invoke(1) + } else { + onCompleted?.invoke(0) + } + } + } + runnable.run() +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/extensions/UriExtension.kt b/base/src/main/java/com/example/base/extensions/UriExtension.kt new file mode 100644 index 0000000..b5c27eb --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/UriExtension.kt @@ -0,0 +1,28 @@ +package com.example.base.extensions + +import android.net.Uri +import com.example.base.utils.Utils +import java.io.File +import java.util.UUID + +/** + * 获取文件路径 + */ +fun Uri.getFilePath(): String? { + + val path: String? = when (this.scheme) { + "file" -> { + this.path + } + "content" -> { + val openIs = Utils.getApp().contentResolver.openInputStream(this) + val file = File("${Utils.getApp().externalCacheDir}/${UUID.randomUUID()}.jpg") + openIs?.toFile(file) + file.absolutePath + } + else -> { // "http", "https" , /storage/emulated/0/... + this.toString() + } + } + return path +} diff --git a/base/src/main/java/com/example/base/extensions/ViewExtensions.kt b/base/src/main/java/com/example/base/extensions/ViewExtensions.kt new file mode 100644 index 0000000..dbf56fa --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ViewExtensions.kt @@ -0,0 +1,147 @@ +package com.example.base.extensions + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.LinearGradient +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.drawable.ShapeDrawable +import android.view.View +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.inputmethod.InputMethodManager + + +/** + * @date 2020-06-12 17:27 + * @desc TODO + */ +fun View.calcViewScreenLocation(): RectF { + + val location = IntArray(2) + // 获取控件在屏幕中的位置,返回的数组分别为控件左顶点的 x、y 的值 + this.getLocationOnScreen(location) + return RectF( + location[0].toFloat(), + location[1].toFloat(), + location[0] + this.width.toFloat(), + location[1] + this.height.toFloat() + ) + +} + +fun View.isInViewRect(x: Float, y: Float): Boolean { + val rect = calcViewScreenLocation() + return rect.contains(x, y) +} + +fun View.closeKeyboard() { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(this.windowToken, InputMethodManager.HIDE_IMPLICIT_ONLY) +} + +/** + * 淡入淡出动画 + */ +fun View.animCrossFade(isShow: Boolean, duration: Long = 600) { + this.apply { + alpha = if (isShow) 0f else 1f + visibility = View.VISIBLE + + animate() + .alpha(if (isShow) 1f else 0f) + .setDuration(duration) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + if (!isShow) { + this@animCrossFade.visibility = View.GONE + } + } + }) + } +} + +fun View.gone() { + if (visibility != View.GONE) { + visibility = View.GONE + } +} + +fun View.gone(gone: Boolean) { + if (gone && visibility != View.GONE) { + visibility = View.GONE + } else if (!gone && visibility == View.GONE) { + visibility = View.VISIBLE + } +} + +fun View.invisible() { + if (visibility != View.INVISIBLE) { + visibility = View.INVISIBLE + } +} + +fun View.visible() { + if (visibility != View.VISIBLE) { + visibility = View.VISIBLE + } +} + +fun View.visible(visible: Boolean) { + if (visible && visibility != View.VISIBLE) { + visibility = View.VISIBLE + } else if (!visible && visibility == View.VISIBLE) { + visibility = View.INVISIBLE + } +} + +fun View.screenshot(): Bitmap? { + return runCatching { + val screenshot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val c = Canvas(screenshot) + c.translate(-scrollX.toFloat(), -scrollY.toFloat()) + draw(c) + screenshot + }.getOrNull() +} + +fun View.startBlinking() { + val fadeIn = AlphaAnimation(0f, 1f).apply { + duration = 500 // 设置每次淡入淡出的时间 + repeatMode = Animation.REVERSE // 反向重复模式 + repeatCount = Animation.INFINITE // 无限次重复 + interpolator = LinearInterpolator() // 使用线性插值器保证匀速变化 + } + startAnimation(fadeIn) +} + +fun View.isAniming() { + + clearAnimation() // 停止并移除当前所有动画 +} + +fun View.stopBlinking() { + clearAnimation() // 停止并移除当前所有动画 +} + +fun View.changeGradientColors(startColor: Int, endColor: Int) { + val currentShapeDrawable = background as? ShapeDrawable ?: return + val paint = currentShapeDrawable.paint + paint.shader = null + val viewWidth = width.coerceAtLeast(1) // 防止宽度为0的情况 + val newShader = LinearGradient( + 0f, // 渐变开始的x坐标 + 0f, // 渐变开始的y坐标 + viewWidth.toFloat(), // 渐变结束的x坐标 + 0f, // 假设为水平渐变,y坐标不变 + intArrayOf(startColor, endColor), // 渐变颜色数组 + null, // 颜色分布位置,默认均匀分布 + Shader.TileMode.CLAMP // 渐变边缘处理方式 + ) + paint.shader = newShader + postInvalidate() +} diff --git a/base/src/main/java/com/example/base/extensions/ViewHolderExtensions.kt b/base/src/main/java/com/example/base/extensions/ViewHolderExtensions.kt new file mode 100644 index 0000000..1d488b7 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ViewHolderExtensions.kt @@ -0,0 +1,14 @@ +package com.example.base.extensions + +import android.widget.ImageView +import androidx.annotation.IdRes +import coil.load +import com.chad.library.adapter.base.viewholder.BaseViewHolder + +/** + * @param + * @desc 加载图片 + */ +fun BaseViewHolder.loadImage(@IdRes id: Int, url: String) { + this.getView(id).load(url) +} diff --git a/base/src/main/java/com/example/base/extensions/ViewModelExtension.kt b/base/src/main/java/com/example/base/extensions/ViewModelExtension.kt new file mode 100644 index 0000000..6fb8736 --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ViewModelExtension.kt @@ -0,0 +1,28 @@ +package com.example.base.extensions + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + + +/** + * @param maxCount 时长 + * @param delay 间隔 + * @param block 回调 + */ +fun ViewModel.countDown( + maxCount: Int, + delay: Long = 1000, + block: (Int) -> Unit +): Job { + return viewModelScope.launch { + repeat(maxCount) { + block.invoke(maxCount - (it + 1)) + delay(delay) + } + } +} + + diff --git a/base/src/main/java/com/example/base/extensions/ViewOnClickExtensions.kt b/base/src/main/java/com/example/base/extensions/ViewOnClickExtensions.kt new file mode 100644 index 0000000..7a4436d --- /dev/null +++ b/base/src/main/java/com/example/base/extensions/ViewOnClickExtensions.kt @@ -0,0 +1,64 @@ +package com.example.base.extensions + +import android.view.View + +/*** + * 设置延迟时间的View扩展 + * @param delay Long 延迟时间,默认500毫秒 + * @return T + */ +fun T.withTrigger(delay: Long = 500): T { + triggerDelay = delay + return this +} + +/*** + * 点击事件的View扩展 + * @param block: (T) -> Unit 函数 + * @return Unit + */ +@Suppress("UNCHECKED_CAST") +fun T.click(time: Long = 500, block: (T) -> Unit) = setOnClickListener { + triggerDelay = time + + if (clickEnable()) { + block(it as T) + } +} + +/*** + * 带延迟过滤的点击事件View扩展 + * @param delay Long 延迟时间,默认500毫秒 + * @param block: (T) -> Unit 函数 + * @return Unit + */ +fun T.clickWithTrigger(delay: Long = 500, block: (T) -> Unit){ + triggerDelay = delay + setOnClickListener { + if (clickEnable()) { + block(it as T) + } + } +} + +private var T.triggerLastTime: Long + get() = if (getTag(1123460103) != null) getTag(1123460103) as Long else 0 + set(value) { + setTag(1123460103, value) + } + +private var T.triggerDelay: Long + get() = if (getTag(1123461123) != null) getTag(1123461123) as Long else -1 + set(value) { + setTag(1123461123, value) + } + +private fun T.clickEnable(): Boolean { + var flag = false + val currentClickTime = System.currentTimeMillis() + if (currentClickTime - triggerLastTime >= triggerDelay) { + flag = true + } + triggerLastTime = currentClickTime + return flag +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/global/ActivityGlobalManager.kt b/base/src/main/java/com/example/base/global/ActivityGlobalManager.kt new file mode 100644 index 0000000..96431b3 --- /dev/null +++ b/base/src/main/java/com/example/base/global/ActivityGlobalManager.kt @@ -0,0 +1,46 @@ +package com.example.base.global + +import android.app.Activity +import java.util.LinkedList + + +/** + * activity 全局管理 + */ +class ActivityGlobalManager { + + companion object { + + private val mActivityList: LinkedList by lazy { LinkedList() } + + fun addActivity(activity: Activity): Boolean { + return mActivityList.add(activity) + } + + fun removeActivity(activity: Activity): Boolean { + return mActivityList.remove(activity) + } + + fun hasActivity(clazz: Class): Boolean { + return mActivityList.find { it.javaClass == clazz } != null + } + + fun lastOrNull(): Activity? { + return mActivityList.lastOrNull() + } + + fun getAllActivity(): LinkedList { + return mActivityList + } + + fun clear() { + return mActivityList.clear() + } + + fun exitApp() { + mActivityList.forEach { + it.finish() + } + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/livedata/DialogLiveEvent.kt b/base/src/main/java/com/example/base/livedata/DialogLiveEvent.kt new file mode 100644 index 0000000..fdb89e3 --- /dev/null +++ b/base/src/main/java/com/example/base/livedata/DialogLiveEvent.kt @@ -0,0 +1,26 @@ +package com.example.base.livedata + +import com.example.base.bean.DialogBean + +/** + * @date 2020-06-05 13:51 + * @desc 对话框显示隐藏liveData + */ + +class DialogLiveEvent : SingleLiveEvent() { + + private val bean = DialogBean() + + fun setValue(isShow: Boolean, cancelable: Boolean = false) { + bean.isShow = isShow + bean.cancelable = cancelable +// bean.msg = "" + value = bean + } + + fun setValue(isShow: Boolean, msg: String) { + bean.isShow = isShow + bean.msg = msg + value = bean + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/livedata/SingleLiveEvent.kt b/base/src/main/java/com/example/base/livedata/SingleLiveEvent.kt new file mode 100644 index 0000000..661ae6e --- /dev/null +++ b/base/src/main/java/com/example/base/livedata/SingleLiveEvent.kt @@ -0,0 +1,57 @@ +package com.example.base.livedata + +import android.util.Log +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + * + * + * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + * + * + * Note that only one observer is going to be notified of changes. + */ +open class SingleLiveEvent : MutableLiveData() { + private val mPending = AtomicBoolean(false) + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + if (hasActiveObservers()) { + Log.w( + TAG, + "Multiple observers registered but only one will be notified of changes." + ) + } + // Observe the internal MutableLiveData + super.observe(owner) { t -> + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + } + } + + @MainThread + override fun setValue(t: T?) { + mPending.set(true) + super.setValue(t) + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + fun call() { + value = null + } + + companion object { + private const val TAG = "SingleLiveEvent" + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/ui/BaseActivity.kt b/base/src/main/java/com/example/base/ui/BaseActivity.kt new file mode 100644 index 0000000..add004f --- /dev/null +++ b/base/src/main/java/com/example/base/ui/BaseActivity.kt @@ -0,0 +1,102 @@ +package com.example.base.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.os.Bundle +import android.os.IBinder +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.example.base.R +import com.example.base.common.ActivityManager +import com.example.base.utils.DisplayUtil +import com.example.base.utils.DisplayUtil.recreate +import com.gyf.immersionbar.ImmersionBar + + +abstract class BaseActivity : AppCompatActivity() { + private var fontScale = 1f + + @SuppressLint("UnsafeOptInUsageError") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ActivityManager.addActivity(this) + setContentView(R.layout.mvvm_activity_base) + var fragment = supportFragmentManager.findFragmentById(R.id.fcvContainer) + if (fragment == null) { + fragment = getFragment() + fragment.arguments = intent.extras + supportFragmentManager.beginTransaction() + .replace(R.id.fcvContainer, getFragment()) + .commitNow() + } + if (immersionBarEnabled()) { + initImmersionBar() + } + } + + abstract fun getFragment(): Fragment + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (ev.action == MotionEvent.ACTION_DOWN) { + if (isShouldHideKeyboard(currentFocus, ev)) { + hideKeyboard(currentFocus!!.windowToken) + } + } + return super.dispatchTouchEvent(ev) + } + + private fun isShouldHideKeyboard(v: View?, event: MotionEvent): Boolean { + if (v != null && v is EditText) { + val l = intArrayOf(0, 0) + v.getLocationInWindow(l) + val left = l[0] + val top = l[1] + val bottom = top + v.getHeight() + val right = left + v.getWidth() + return !(event.x > left && event.x < right && event.y > top && event.y < bottom) + } + return false + } + + private fun hideKeyboard(token: IBinder?) { + if (token != null) { + val im = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + im.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS) + } + } + + private fun initImmersionBar() { + ImmersionBar.with(this) + .keyboardEnable(false) + .statusBarColor(android.R.color.transparent) + .navigationBarColor(android.R.color.white) + .statusBarDarkFont(true) + .navigationBarDarkIcon(true) + .keyboardMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + .init() + } + + private fun immersionBarEnabled(): Boolean { + return true + } + + override fun onDestroy() { + ActivityManager.removeActivity(this) + super.onDestroy() + } + + override fun getResources(): Resources { + val resources = super.getResources() + return DisplayUtil.getResources(this, resources, fontScale) + } + + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(DisplayUtil.attachBaseContext(newBase!!, fontScale)) + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/ui/BaseFragment.kt b/base/src/main/java/com/example/base/ui/BaseFragment.kt new file mode 100644 index 0000000..7fb2d8f --- /dev/null +++ b/base/src/main/java/com/example/base/ui/BaseFragment.kt @@ -0,0 +1,208 @@ +package com.example.base.ui + +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.view.ViewGroup +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.children +import androidx.viewbinding.ViewBinding +import com.example.base.R +import com.example.base.dialog.LoadingDialog +import com.example.base.extensions.longToast +import com.example.base.utils.L +import com.example.base.viewmodel.BaseViewModel +import com.example.base.widget.TitleBar +import com.gyf.immersionbar.ImmersionBar +import com.tendcloud.tenddata.TCAgent +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import kotlinx.coroutines.TimeoutCancellationException +import okio.Timeout +import retrofit2.HttpException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.util.concurrent.CancellationException +import java.util.concurrent.TimeoutException + +abstract class BaseFragment : VBFragment() { + + // 回收维护rx事件 + private var mCompositeDisposable: CompositeDisposable? = null + + // loading 对话框 + private var loadingDialog: LoadingDialog? = null + + // 自定义标题栏 + protected var mTitleBar: TitleBar? = null + + private var statusBarColor: Int = android.R.color.transparent + + private var navigationBarColor: Int = android.R.color.white + + private var isPrepared = false //该页面是否已经准备完毕 + + private var pageStartTime = 0L //事件开始时间 + + var isLazyLoaded = false //是否已经懒加载过 + + override fun initView() { + pageStartTime = System.currentTimeMillis() + // 关联titleBar的返回事件 + mTitleBar = (binding.root as ViewGroup).children.find { it is TitleBar } as TitleBar? + (requireActivity() as AppCompatActivity).setSupportActionBar(mTitleBar) + + setBackColor(android.R.color.black) + + ImmersionBar.setTitleBar(requireActivity(), mTitleBar) + + if (immersionBarEnabled()) { + initImmersionBar() + } + + } + + fun setBackColor(color: Int) { + val upArrow = mTitleBar?.navigationIcon ?: ContextCompat.getDrawable(requireContext(), R.drawable.ic_back) + upArrow?.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(requireContext(), color), PorterDuff.Mode.SRC_ATOP) + (requireActivity() as AppCompatActivity).supportActionBar?.setHomeAsUpIndicator(upArrow) + } + + fun setHomeAsUp(upArrow: Boolean) { + (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(upArrow) + } + + private fun initImmersionBar() { + ImmersionBar.with(this) + .statusBarColor(statusBarColor) + .navigationBarColor(navigationBarColor) + .statusBarDarkFont(true) + .navigationBarDarkIcon(true) + .keyboardEnable(false) + .keyboardMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + .init() + } + + private fun immersionBarEnabled(): Boolean { + return true + } + + override fun initListener() { + // 返回上一页 + mTitleBar?.setNavigationOnClickListener { + requireActivity().onBackPressed() + } + } + + /** + * 显示用户等待框 + * + * @param msg 提示信息 + */ + fun showDialog(msg: String = "请稍后...", cancelable: Boolean = false) { + if (loadingDialog?.isShowing == true) { + loadingDialog!!.setMessage(msg) + } else { + loadingDialog = LoadingDialog(context) + loadingDialog!!.setMessage(msg) + loadingDialog!!.setCancelable(cancelable) + loadingDialog!!.show() + } + } + + /** + * 隐藏等待框 + */ + fun dismissDialog() { + loadingDialog?.dismiss() + loadingDialog = null + } + + /** + * 添加到回收管理 + */ + fun addDisposable(disposable: Disposable) { + if (mCompositeDisposable == null) { + mCompositeDisposable = CompositeDisposable() + } + mCompositeDisposable?.add(disposable) + } + + /** + * 页面停留时间 + */ + fun pageDuration(): String { + val duration = System.currentTimeMillis() - pageStartTime + return if (duration < 1000) { + "停留时间:${duration}ms" + } else { + "停留时间:${duration / 1000}s" + } + } + + override fun onDestroyView() { + super.onDestroyView() + dismissDialog() + //处理当前Fragment 所有的rx事件 + mCompositeDisposable?.dispose() + mCompositeDisposable = null + } + + override fun onResume() { + super.onResume() + TCAgent.onPageStart(context, this.javaClass.simpleName) + isPrepared = true + lazyLoad() + } + + override fun onDestroy() { + TCAgent.onPageEnd(context, this.javaClass.simpleName) + super.onDestroy() + } + + private fun lazyLoad() { + if (isPrepared && !isLazyLoaded) { + onLazyLoad() + isLazyLoaded = true + } + } + + open fun onLazyLoad() {} + + override fun initObserve() { + super.initObserve() + mViewModel.exceptionLiveEvent.observe(this) { + onError(it) + } + mViewModel.showDialogLiveEvent.observe(this) { bean -> + if (bean.isShow) showDialog(bean.msg, bean.cancelable) else dismissDialog() + } + mViewModel.finishActivityLiveEvent.observe(this) { + if (it == mViewModel) activity?.finish() + } + } + + /** + * ViewModel层发生了错误 + */ + open fun onError(e: Throwable) { + dismissDialog() + + val errorMsg = when (e) { + is ConnectException -> "抱歉,网络连接失败" + is HttpException -> "抱歉,网络连接失败" + is UnknownHostException -> "抱歉,网络连接失败" + is TimeoutException -> "抱歉,网络连接失败" + is TimeoutCancellationException -> "抱歉,网络连接失败" + is SocketTimeoutException -> "抱歉,网络连接失败" + is CancellationException -> "抱歉,网络连接失败" + is Timeout -> "抱歉,网络连接失败" + else -> e.message ?: "" + } + + L.d(e.toString()) + longToast(errorMsg) + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/ui/VBFragment.kt b/base/src/main/java/com/example/base/ui/VBFragment.kt new file mode 100644 index 0000000..7a35058 --- /dev/null +++ b/base/src/main/java/com/example/base/ui/VBFragment.kt @@ -0,0 +1,63 @@ +package com.example.base.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import com.example.base.viewmodel.BaseViewModel +import java.lang.reflect.ParameterizedType + +abstract class VBFragment : VMFragment() { + + private var _binding: VB? = null + + val binding get() = _binding!! + + override fun onCreateView(li: LayoutInflater, c: ViewGroup?, sis: Bundle?): View? { + _binding = initVB() + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initView() + + initListener() + + initData() + } + + @Suppress("UNCHECKED_CAST") + private fun initVB(): VB { + // 获取泛型的真实class + val types = javaClass.genericSuperclass as ParameterizedType + + val type = types.actualTypeArguments.find { + val clazz = it as Class<*> + ViewBinding::class.java.isAssignableFrom(clazz) + } ?: RuntimeException("子页面没有传入ViewBinding的泛型") + + val tClass = type as Class + + val method = tClass.getMethod("inflate", LayoutInflater::class.java) + return method.invoke(null, layoutInflater) as VB + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + // init View + open fun initView() {} + + // init view的相关事件 + open fun initListener() {} + + // init Data + open fun initData() {} +} + diff --git a/base/src/main/java/com/example/base/ui/VMFragment.kt b/base/src/main/java/com/example/base/ui/VMFragment.kt new file mode 100644 index 0000000..f00986c --- /dev/null +++ b/base/src/main/java/com/example/base/ui/VMFragment.kt @@ -0,0 +1,66 @@ +package com.example.base.ui + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.base.utils.Utils +import com.example.base.viewmodel.BaseViewModel +import com.example.base.viewmodel.ViewModelScope +import com.example.base.viewmodel.ViewModelScopeType +import java.lang.reflect.ParameterizedType + +abstract class VMFragment : Fragment() { + + private var _viewModel: VM? = null + val mViewModel get() = _viewModel!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + _viewModel = initVM() + + initObserve() + } + + /** + * ViewModel viewModel存储位置 + * @see ViewModelScope + */ + private fun Fragment.initVM(): VM { + // 获取泛型的真实class + val types = javaClass.genericSuperclass as ParameterizedType + + val type = types.actualTypeArguments.find { + val clazz = it as Class<*> + ViewModel::class.java.isAssignableFrom(clazz) + } ?: throw RuntimeException("子页面没有传入ViewModel的泛型") + + @Suppress("UNCHECKED_CAST") + val tClass = type as Class + val viewModelScope = tClass.getAnnotation(ViewModelScope::class.java) +// toast("${viewModelScope?.scope}") + return when (viewModelScope?.scope) { + com.example.base.viewmodel.ViewModelScopeType.ACTIVITY -> { + // 关联到activity(可实现activity下的多个fragment数据共享) + ViewModelProvider(requireActivity()).get(tClass) + } + com.example.base.viewmodel.ViewModelScopeType.GLOBAL -> { + // 关联到 Application (可实现全局数据共享) + ViewModelProvider(Utils.getApp()).get(tClass) + } + else -> { + // 关联到fragment + ViewModelProvider(this).get(tClass) + } + } + } + + override fun onDestroy() { + super.onDestroy() + _viewModel = null + } + + // init livedata监听 + open fun initObserve(){} +} diff --git a/base/src/main/java/com/example/base/ui/list/ListFragment.kt b/base/src/main/java/com/example/base/ui/list/ListFragment.kt new file mode 100644 index 0000000..87ae4e9 --- /dev/null +++ b/base/src/main/java/com/example/base/ui/list/ListFragment.kt @@ -0,0 +1,181 @@ +package com.example.base.ui.list + +import android.annotation.SuppressLint +import android.graphics.drawable.Drawable +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.R +import com.example.base.ui.BaseFragment +import com.example.base.utils.L +import com.example.base.viewmodel.ListViewModel +import com.example.base.widget.EmptyView +import com.example.base.widget.MyLoadMoreView +import com.example.base.widget.PageStatus +import com.scwang.smart.refresh.layout.SmartRefreshLayout + +/** + * @date 2021-08-02 14:50 + * @desc 列表的base类 + */ +abstract class ListFragment, T> : BaseFragment() { + + // 空view (加载中, 无数据, 错误) + lateinit var mEmptyView: EmptyView + + // 下拉刷新view + lateinit var mRefreshLayout: SmartRefreshLayout + + // 列表view + lateinit var mRecyclerView: RecyclerView + + // 子类提供的适配器 + lateinit var mAdapter: BaseQuickAdapter + + override fun initView() { + super.initView() + + mEmptyView = EmptyView(requireContext()) + mRefreshLayout = binding.root.findViewById(R.id.mRefreshLayout) + mRecyclerView = binding.root.findViewById(R.id.mRecyclerView) + mAdapter = bindAdapter() + + mEmptyView.setStatus(PageStatus.LOADING) + mRefreshLayout.isEnabled = false + + mAdapter.setEmptyView(mEmptyView) + mAdapter.loadMoreModule.loadMoreView = MyLoadMoreView() + mAdapter.apply { + // 使用viewModel的实例列表 + setNewInstance(mViewModel.list) + // 设置emptyView + setEmptyView(mEmptyView) + // 设置加载更多view + loadMoreModule.loadMoreView = MyLoadMoreView() + loadMoreModule.setOnLoadMoreListener { + mViewModel.loadMore() + } + } + + + mRecyclerView.layoutManager = LinearLayoutManager(context) + mRecyclerView.adapter = mAdapter + } + + override fun initListener() { + super.initListener() + mRefreshLayout.setOnRefreshListener { refresh() } + + mEmptyView.setOnReloadListener { firstLoad() } + + mEmptyView.noDataBtn { noDataClick() } + } + + abstract fun noDataClick() + + fun setBtnVisible(visible: Boolean) { + mEmptyView.setBtnVisible(visible) + } + + fun setViewLogo(logo: Int) { + mEmptyView.setNoDataLogo(logo) + } + + fun setBtnText(str: String) { + mEmptyView.setBtnText(str) + } + + fun setBtnTextColor(color: Int) { + mEmptyView.setBtnTextColor(color) + } + + fun setBtnTextBackground(drawable: Drawable?) { + mEmptyView.setBtnTextBackground(drawable) + } + + override fun initObserve() { + // 列表数据监听 + mViewModel.pageLiveData.observe(this) { result -> + result.onSuccess { list -> + onSuccess(list) + L.d("onSuccess") + } + result.onFailure { + onFailure(it) + L.d("onFailure: $it") + + } + } + + } + + // 重新加载 比如筛选数据后也调用此方法 + @SuppressLint("NotifyDataSetChanged") + fun firstLoad() { + + mRefreshLayout.isEnabled = false + mEmptyView.setStatus(PageStatus.LOADING) + + mAdapter.data.clear() + mAdapter.notifyDataSetChanged() + + mViewModel.refresh() + } + + private fun refresh() { + mRefreshLayout.autoRefresh() + mViewModel.refresh() + } + + + private fun onSuccess(list: List) { + + // 默认无数据 + mEmptyView.setStatus(PageStatus.NO_DATA) + + // 第一页数据 + if (mRefreshLayout.isRefreshing || mViewModel.pageIndex == 1) { + mAdapter.setList(list) + } else { + mAdapter.addData(list) + } + + // 是否有还有下一页数据 + if (mViewModel.pageCount > list.size) { + mAdapter.loadMoreModule.loadMoreEnd() + } else { + mAdapter.loadMoreModule.loadMoreComplete() + } + + // 重置刷新控件可用 + mRefreshLayout.finishRefresh() + mRefreshLayout.isEnabled = true + onRefreshComplete() + } + + private fun onFailure(e: Throwable) { + mAdapter.run { + if (data.isEmpty()) { + mEmptyView.setStatus(PageStatus.ERROR) + } + if (loadMoreModule.isLoading) { + loadMoreModule.loadMoreFail() + } + } + mRefreshLayout.finishRefresh() + onRefreshComplete() + } + + override fun onDestroy() { + // 清除viewModel中的数据 + viewModelStore.clear() + super.onDestroy() + } + + // 子类提供的适配器 + abstract fun bindAdapter(): BaseQuickAdapter + + open fun onRefreshComplete() {} +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/ui/list/LoadMoreAdapter.kt b/base/src/main/java/com/example/base/ui/list/LoadMoreAdapter.kt new file mode 100644 index 0000000..f8dec02 --- /dev/null +++ b/base/src/main/java/com/example/base/ui/list/LoadMoreAdapter.kt @@ -0,0 +1,11 @@ +package com.example.base.ui.list + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder + +/** + * 实现了 loadMoreModule 有加载更多的实现它 + */ +abstract class LoadMoreAdapter(layoutResId: Int) : + BaseQuickAdapter(layoutResId), LoadMoreModule \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/AppUtils.kt b/base/src/main/java/com/example/base/utils/AppUtils.kt new file mode 100644 index 0000000..b62e798 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/AppUtils.kt @@ -0,0 +1,32 @@ +package com.example.base.utils + +import android.content.pm.PackageManager +import com.example.base.R + +object AppUtils { + + fun getAppVersionName(): String { + return try { + val pm = Utils.getApp().packageManager + val pi = pm.getPackageInfo(Utils.getApp().packageName, 0) + pi.versionName ?: "" + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + "" + } + } + fun getAppName(): String { + return Utils.getApp().resources.getString(R.string.app_name) + } + + fun buildVersionNo(): Int { + return try { + val pm = Utils.getApp().packageManager + val pi = pm.getPackageInfo(Utils.getApp().packageName, 0) + pi.versionCode + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + 0 + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/BarUtils.kt b/base/src/main/java/com/example/base/utils/BarUtils.kt new file mode 100644 index 0000000..118ebfe --- /dev/null +++ b/base/src/main/java/com/example/base/utils/BarUtils.kt @@ -0,0 +1,19 @@ +package com.example.base.utils + +import android.content.res.Resources + + +object BarUtils { + + fun getStatusBarHeight(): Int { + val resources: Resources = Resources.getSystem() + val resourceId: Int = resources.getIdentifier("status_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + + fun getNavBarHeight(): Int { + val resources = Resources.getSystem() + val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/CircleGlideTransform.kt b/base/src/main/java/com/example/base/utils/CircleGlideTransform.kt new file mode 100644 index 0000000..29fd3b1 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/CircleGlideTransform.kt @@ -0,0 +1,65 @@ +package com.example.base.utils + +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Shader +import com.bumptech.glide.load.Key +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.security.MessageDigest +import kotlin.math.min + + +class CircleGlideTransform : BitmapTransformation() { + + private val VERSION = 1 + private val ID = "com.pblk.tiantian.video.utils.CircleGlideTransform.$VERSION" + private val ID_BYTES = ID.toByteArray(Key.CHARSET) + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap? { + return circleCrop(pool, toTransform); + } + + private fun circleCrop(pool: BitmapPool, source: Bitmap?): Bitmap? { + if (source == null) return null + val size = min(source.width, source.height) + val x = (source.width - size) / 2 + val y = (source.height - size) / 2 + val squared = Bitmap.createBitmap(source, x, y, size, size) + var result = pool[size, size, Bitmap.Config.ARGB_8888] + if (result == null) { + result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + } + val canvas = Canvas(result) + val paint = Paint() + paint.shader = BitmapShader( + squared, + Shader.TileMode.CLAMP, + Shader.TileMode.CLAMP + ) + paint.isAntiAlias = true + val r = size / 2f + canvas.drawCircle(r, r, r, paint) + return result + } + + override fun equals(obj: Any?): Boolean { + return obj is CircleGlideTransform + } + + override fun hashCode(): Int { + return ID.hashCode() + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(ID_BYTES); + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/ClipboardUtils.kt b/base/src/main/java/com/example/base/utils/ClipboardUtils.kt new file mode 100644 index 0000000..ec49d95 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/ClipboardUtils.kt @@ -0,0 +1,123 @@ +package com.example.base.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.example.base.extensions.longToast +import com.example.base.extensions.toast + +/** + *
+ * author: Blankj
+ * blog  : http://blankj.com
+ * time  : 2016/09/25
+ * desc  : 剪贴板相关工具类
+
* + */ +class ClipboardUtils private constructor() { + companion object { + /** + * 复制文本到剪贴板 + * + * @param text 文本 + */ + fun copyText(text: CharSequence?) { + val cm = Utils.getApp() + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText("text", text)) + } + + /** + * 获取剪贴板的文本 + * + * @return 剪贴板的文本 + */ + val text: CharSequence? + get() { + val cm = Utils.getApp() + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = cm.primaryClip + return if (clip != null && clip.itemCount > 0) { + clip.getItemAt(0).coerceToText(Utils.getApp()) + } else null + } + + /** + * 复制uri到剪贴板 + * + * @param uri uri + */ + fun copyUri(uri: Uri?) { + val cm = Utils.getApp() + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip( + ClipData.newUri( + Utils.getApp().contentResolver, "uri", uri + ) + ) + } + + /** + * 获取剪贴板的uri + * + * @return 剪贴板的uri + */ + val uri: Uri? + get() { + val cm = Utils.getApp() + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = cm.primaryClip + return if (clip != null && clip.itemCount > 0) { + clip.getItemAt(0).uri + } else null + } + + /** + * 复制意图到剪贴板 + * + * @param intent 意图 + */ + fun copyIntent(intent: Intent?) { + val cm = Utils.getApp() + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newIntent("intent", intent)) + } + + /** + * 获取剪贴板的意图 + * + * @return 剪贴板的意图 + */ + val intent: Intent? + get() { + val cm = Utils.getApp() + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = cm.primaryClip + return if (clip != null && clip.itemCount > 0) { + clip.getItemAt(0).intent + } else null + } + + /** + * 清空剪切板第一条 + * + */ + fun clearFirstClipboard() { + val cm = Utils.getApp() + .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = cm.primaryClip + if (clip != null && clip.itemCount > 0) { + cm.setPrimaryClip(ClipData.newPlainText(null, "")) + if (cm.hasPrimaryClip()) { + cm.primaryClip!!.getItemAt(0).text + } + } + } + } + + init { + throw UnsupportedOperationException("u can't instantiate me...") + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/DensityUtils.kt b/base/src/main/java/com/example/base/utils/DensityUtils.kt new file mode 100644 index 0000000..9375d54 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/DensityUtils.kt @@ -0,0 +1,59 @@ +package com.example.base.utils + +import android.content.Context +import android.content.res.Resources + +/** + * @Des 屏幕相关工具类 + */ +object DensityUtils { + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + fun dp2px(dpValue: Float): Int { + val scale = Resources.getSystem().displayMetrics.density + return (dpValue * scale + 0.5F).toInt() + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + */ + fun px2dp(pxValue: Float): Int { + val scale = Resources.getSystem().displayMetrics.density + return (pxValue / scale + 0.5F).toInt() + } + + /** + * 将sp值转换为px值 + */ + fun sp2px(spValue: Float): Int { + val fontScale = Resources.getSystem().displayMetrics.scaledDensity + return (spValue * fontScale + 0.5F).toInt() + } + + /** + * 将px值转换为sp值 + */ + fun px2sp(pxValue: Float): Int { + val fontScale = Resources.getSystem().displayMetrics.scaledDensity + return (pxValue / fontScale + 0.5F).toInt() + } + + /** + * 获取状态栏高度 + */ + /** + * 获取通知栏(状态栏)的高度 + * + * @return statusBarHeight + */ + fun getStatusBarHeight(context: Context): Int { + var statusHeight = 0 + val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android") + if (resourceId > 0) { + statusHeight = context.resources.getDimensionPixelSize(resourceId) + } + return statusHeight + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/DeviceUtils.kt b/base/src/main/java/com/example/base/utils/DeviceUtils.kt new file mode 100644 index 0000000..5f57aa6 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/DeviceUtils.kt @@ -0,0 +1,383 @@ +package com.example.base.utils + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.res.Configuration +import android.content.res.Resources +import android.net.Uri +import android.net.wifi.WifiManager +import android.os.Build +import android.provider.Settings +import android.telephony.TelephonyManager +import android.text.TextUtils +import androidx.annotation.RequiresPermission +import java.io.File +import java.net.InetAddress +import java.net.NetworkInterface +import java.net.SocketException +import java.util.Enumeration +import java.util.UUID + + +object DeviceUtils { + + /** + * Whether the device is rooted + */ + fun isDeviceRooted(): Boolean { + val su = "su" + val locations = arrayOf( + "/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/", + "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/", + "/system/sbin/", "/usr/bin/", "/vendor/bin/" + ) + for (location in locations) { + if (File(location + su).exists()) { + return true + } + } + return false + } + + fun getAppVersion(): PackageInfo? { + try { + return Utils.getApp().packageManager.getPackageInfo( + Utils.getApp().packageName, + 0 + ) + } catch (e: Exception) { + + } + return null + } + + /** + * AppName + */ + fun getAppName(): String? { + return getAppVersion()?.let { + Utils.getApp().resources.getString(it.applicationInfo.labelRes) + } + } + + /** + * VersionName + */ + fun getSDKVersionName(): String? { + return Build.VERSION.RELEASE + } + + /** + * VersionCode + */ + fun getSDKVersionCode(): Int { + return Build.VERSION.SDK_INT + } + + /** + * AndroidID + */ + @SuppressLint("HardwareIds") + fun getAndroidID(): String? { + val id: String? = Settings.Secure.getString( + Utils.getApp().contentResolver, + Settings.Secure.ANDROID_ID + ) + return if ("9774d56d682e549c" == id) "" else id ?: "" + } + + /** + * Mac Address + */ + @SuppressLint("MissingPermission") + fun getMMacAddress(): String? { + val macAddress = getMacAddress() + if (macAddress != "" || getWifiEnabled()) return macAddress + setWifiEnabled(true) + setWifiEnabled(false) + return getMacAddress() + } + + private fun getWifiEnabled(): Boolean { + @SuppressLint("WifiManagerLeak") val manager = + Utils.getApp().getSystemService(Context.WIFI_SERVICE) as? WifiManager + ?: return false + return manager.isWifiEnabled + } + + /** + * enable or disable wifi + */ + @RequiresPermission(Manifest.permission.CHANGE_WIFI_STATE) + private fun setWifiEnabled(enabled: Boolean) { + @SuppressLint("WifiManagerLeak") val manager = + Utils.getApp().getSystemService(Context.WIFI_SERVICE) as? WifiManager + ?: return + if (enabled == manager.isWifiEnabled) return + manager.isWifiEnabled = enabled + } + + /** + * Mac Address + */ + @RequiresPermission(allOf = [Manifest.permission.ACCESS_WIFI_STATE, Manifest.permission.INTERNET]) + fun getMacAddress(vararg excepts: String?): String? { + var macAddress: String? = getMacAddressByNetworkInterface() + if (isAddressNotInExcepts(macAddress, *excepts)) { + return macAddress + } + macAddress = getMacAddressByInetAddress() + if (isAddressNotInExcepts(macAddress, *excepts)) { + return macAddress + } + macAddress = getMacAddressByWifiInfo() + + return if (isAddressNotInExcepts(macAddress, *excepts)) { + macAddress + } else "" + } + + private fun isAddressNotInExcepts( + address: String?, + vararg excepts: String? + ): Boolean { + if (excepts.isEmpty()) { + return "02:00:00:00:00:00" != address + } + for (filter in excepts) { + if (address == filter) { + return false + } + } + return true + } + + @SuppressLint("MissingPermission", "HardwareIds") + private fun getMacAddressByWifiInfo(): String? { + try { + val wifi = Utils.getApp() + .applicationContext + .getSystemService(Context.WIFI_SERVICE) as? WifiManager + if (wifi != null) { + val info = wifi.connectionInfo + if (info != null) return info.macAddress + } + } catch (e: Exception) { + e.printStackTrace() + } + return "02:00:00:00:00:00" + } + + private fun getMacAddressByNetworkInterface(): String? { + try { + val nis: Enumeration = NetworkInterface.getNetworkInterfaces() + while (nis.hasMoreElements()) { + val ni: NetworkInterface? = nis.nextElement() + if (ni == null || !ni.name.equals("wlan0", true)) continue + val macBytes: ByteArray? = ni.hardwareAddress + if (macBytes != null && macBytes.isNotEmpty()) { + val sb = StringBuilder() + for (b in macBytes) { + sb.append(String.format("%02x:", b)) + } + return sb.substring(0, sb.length - 1) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "02:00:00:00:00:00" + } + + private fun getMacAddressByInetAddress(): String? { + try { + val inetAddress: InetAddress? = getInetAddress() + if (inetAddress != null) { + val ni: NetworkInterface? = NetworkInterface.getByInetAddress(inetAddress) + if (ni != null) { + val macBytes: ByteArray? = ni.hardwareAddress + if (macBytes != null && macBytes.isNotEmpty()) { + val sb = StringBuilder() + for (b in macBytes) { + sb.append(String.format("%02x:", b)) + } + return sb.substring(0, sb.length - 1) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "02:00:00:00:00:00" + } + + private fun getInetAddress(): InetAddress? { + try { + val nis: Enumeration = NetworkInterface.getNetworkInterfaces() + while (nis.hasMoreElements()) { + val ni: NetworkInterface = nis.nextElement() + // To prevent phone of xiaomi return "10.0.2.15" + if (!ni.isUp()) continue + val addresses: Enumeration = ni.inetAddresses + while (addresses.hasMoreElements()) { + val inetAddress: InetAddress = addresses.nextElement() + if (!inetAddress.isLoopbackAddress) { + val hostAddress: String = inetAddress.hostAddress + if (hostAddress.indexOf(':') < 0) return inetAddress + } + } + } + } catch (e: SocketException) { + e.printStackTrace() + } + return null + } + + /** + * obtain the equipment manufacturer + */ + fun getManufacturer(): String? { + return Build.MANUFACTURER + } + + /** + * Device Name + */ + fun getDeviceName(): String? { + return Build.DEVICE + } + + /** + * get device model + */ + fun getModel(): String? { + var model = Build.MODEL + model = model?.trim { it <= ' ' }?.replace("\\s*".toRegex(), "") ?: "" + return model + } + + /** + * Returns an ordered list of ABIs supported by this device. The most preferred ABI is the first element in the list + */ + fun getABIs(): Array? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Build.SUPPORTED_ABIS + } else { + if (!TextUtils.isEmpty(Build.CPU_ABI2)) { + arrayOf(Build.CPU_ABI, Build.CPU_ABI2) + } else arrayOf(Build.CPU_ABI) + } + } + + /** + * isTablet + */ + fun isTablet(): Boolean { + return ((Resources.getSystem().configuration.screenLayout + and Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE) + } + + /** + * isEmulator + */ + fun isEmulator(): Boolean { + val checkProperty = (Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.toLowerCase().contains("vbox") + || Build.FINGERPRINT.toLowerCase().contains("test-keys") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") + || "google_sdk" == Build.PRODUCT) + if (checkProperty) return true + var operatorName = "" + val tm = + Utils.getApp().getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + if (tm != null) { + val name = tm.networkOperatorName + if (name != null) { + operatorName = name + } + } + val checkOperatorName = operatorName.toLowerCase() == "android" + if (checkOperatorName) return true + val url = "tel:" + "123456" + val intent = Intent() + intent.data = Uri.parse(url) + intent.action = Intent.ACTION_DIAL + return intent.resolveActivity(Utils.getApp().packageManager) == null + } + + private const val KEY_UDID = "KEY_UDID" + + @Volatile + private var udid: String? = null + + /** + * get the only device ID + *
{1}{UUID(macAddress)}
+ *
{2}{UUID(androidId )}
+ *
{9}{UUID(random    )}
+ * + * @return the unique device id + */ + @SuppressLint("MissingPermission", "HardwareIds") + fun getUniqueDeviceId(): String? { + return getUniqueDeviceId("", true) + } + + /** + * getUniqueDeviceId + *
android 10 deprecated {prefix}{1}{UUID(macAddress)}
+ *
{prefix}{2}{UUID(androidId )}
+ *
{prefix}{9}{UUID(random    )}
+ */ + @SuppressLint("MissingPermission", "HardwareIds") + fun getUniqueDeviceId(prefix: String, useCache: Boolean): String? { + if (!useCache) { + return getUniqueDeviceIdReal(prefix) + } + if (udid == null) { + synchronized(DeviceUtils::class.java) { + if (udid == null) { + val id = SPUtils.getString(KEY_UDID, "") + if (id.isNotEmpty()) { + udid = id + return udid + } + return getUniqueDeviceIdReal(prefix) + } + } + } + return udid + } + + private fun getUniqueDeviceIdReal(prefix: String): String? { + try { + val androidId = getAndroidID() + if (!TextUtils.isEmpty(androidId)) { + return saveUdid(prefix + 2, androidId) + } + } catch (ignore: Exception) { /**/ + } + return saveUdid(prefix + 9, "") + } + + private fun saveUdid(prefix: String, id: String?): String? { + udid = getUdid(prefix, id) + SPUtils.putString(KEY_UDID, udid!!) + return udid + } + + private fun getUdid(prefix: String, id: String?): String { + return if (id == "") { + prefix + UUID.randomUUID().toString().replace("-", "") + } else prefix + UUID.nameUUIDFromBytes(id!!.toByteArray()).toString().replace("-", "") + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/DisplayUtils.kt b/base/src/main/java/com/example/base/utils/DisplayUtils.kt new file mode 100644 index 0000000..9cbfb7e --- /dev/null +++ b/base/src/main/java/com/example/base/utils/DisplayUtils.kt @@ -0,0 +1,42 @@ +package com.example.base.utils + +import android.app.Activity + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources + + +object DisplayUtil { + /** + * 保持字体大小不随系统设置变化(用在界面加载之前) + * 要重写Activity的attachBaseContext() + */ + fun attachBaseContext(context: Context, fontScale: Float): Context { + val config: Configuration = context.resources.configuration + //正确写法 + config.fontScale = fontScale + return context.createConfigurationContext(config) + } + + /** + * 保持字体大小不随系统设置变化(用在界面加载之前) + * 要重写Activity的getResources() + */ + fun getResources(context: Context, resources: Resources, fontScale: Float): Resources { + val config: Configuration = resources.configuration + return if (config.fontScale != fontScale) { + config.fontScale = fontScale + context.createConfigurationContext(config).resources + } else { + resources + } + } + + /** + * 保存字体大小,后通知界面重建,它会触发attachBaseContext,来改变字号 + */ + fun recreate(activity: Activity) { + activity.recreate() + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/FilePickerProvider.kt b/base/src/main/java/com/example/base/utils/FilePickerProvider.kt new file mode 100644 index 0000000..e757b7e --- /dev/null +++ b/base/src/main/java/com/example/base/utils/FilePickerProvider.kt @@ -0,0 +1,5 @@ +package com.example.base.utils + +import androidx.core.content.FileProvider + +class FilePickerProvider : FileProvider() \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/GlideUtils.kt b/base/src/main/java/com/example/base/utils/GlideUtils.kt new file mode 100644 index 0000000..e0bfda9 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/GlideUtils.kt @@ -0,0 +1,191 @@ +package com.example.base.utils + +import android.content.Context +import android.graphics.drawable.Drawable +import android.widget.ImageView +import androidx.annotation.DrawableRes +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import com.bumptech.glide.request.RequestOptions + + +object GlideUtils { + + /** + * 普通加载 + */ + @JvmOverloads + fun load( + url: Any?, + imageView: ImageView?, + @DrawableRes errorDefaultRes: Int? = null, + ) { + if (imageView == null) return + Glide.with(imageView.context).load(url).apply { + if (errorDefaultRes != null) { + placeholder(errorDefaultRes) + .fallback(errorDefaultRes) + .error(errorDefaultRes) + } + }.into(imageView) + } + + @JvmOverloads + fun loadCrop( + url: Any?, + imageView: ImageView, + overrideSize: Int? = null, + errorDrawable: Drawable? = null, + @DrawableRes errorDefaultRes: Int? = null, + ) { + Glide.with(imageView.context).load(url).apply { + if (overrideSize != null) override(overrideSize) + if (errorDefaultRes != null) { + placeholder(errorDefaultRes) + .fallback(errorDefaultRes) + .error(errorDefaultRes) + } else if (errorDrawable != null) { + placeholder(errorDrawable) + .fallback(errorDrawable) + .error(errorDrawable) + } + }.into(imageView) + } + + @JvmOverloads + fun loadTransform( + url: Any?, + loadTransform: BitmapTransformation, + imageView: ImageView, + errorDrawable: Drawable? = null, + @DrawableRes errorDefaultRes: Int? = null, + ) { + Glide.with(imageView.context).load(url).transform(loadTransform).apply { + if (errorDefaultRes != null) { + placeholder(errorDefaultRes) + .fallback(errorDefaultRes) + .error(errorDefaultRes) + .thumbnail(getTransform(imageView.context, errorDefaultRes, loadTransform)) + } else if (errorDrawable != null) { + placeholder(errorDrawable) + .fallback(errorDrawable) + .error(errorDrawable) + .thumbnail(getTransform(imageView.context, errorDrawable, loadTransform)) + } + } + .into(imageView) + } + + @JvmOverloads + private fun getTransform( + context: Context, + url: Any?, + loadTransform: BitmapTransformation, + ): RequestBuilder { + return Glide.with(context).load(url).transform(loadTransform) + } + + /** + * 加载圆形 + */ + @JvmOverloads + fun loadCircle( + url: Any?, + imageView: ImageView, + errorDrawable: Drawable? = null, + @DrawableRes errorDefaultRes: Int? = null, + ) { + loadTransform(url, CircleGlideTransform(), imageView, errorDrawable, errorDefaultRes) + } + + /** + * 加载圆角 + */ + @JvmOverloads + fun loadRound( + url: Any?, + imageView: ImageView, + errorDrawable: Drawable? = null, + @DrawableRes errorDefaultRes: Int? = null, + ) { + loadTransform( + url, + RoundGlideTransform(isSquare = false), + imageView, + errorDrawable, + errorDefaultRes + ) + } + + /** + * load round + */ + @JvmOverloads + fun loadRound( + url: Any?, + imageView: ImageView, + round: Float, + errorDrawable: Drawable? = null, + @DrawableRes errorDefaultRes: Int? = null, + ) { + loadTransform( + url, + RoundGlideTransform(round, round, round, round, isSquare = false), + imageView, + errorDrawable, + errorDefaultRes + ) + } + + @JvmOverloads + fun loadRound( + url: Any?, + roundLeftTop: Float, + roundRightTop: Float, + roundRightBottom: Float, + roundLeftBottom: Float, + imageView: ImageView, + errorDrawable: Drawable? = null, + @DrawableRes errorDefaultRes: Int? = null, + ) { + loadTransform( + url, + RoundGlideTransform( + roundLeftTop, + roundRightTop, + roundRightBottom, + roundLeftBottom, + isSquare = false + ), + imageView, + errorDrawable, + errorDefaultRes + ) + } + + /** + * 加载第四秒的帧数作为封面 + * url就是视频的地址 + */ + fun loadCover(url: String?, imageView: ImageView, round: Float) { + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + Glide.with(imageView.context) + .setDefaultRequestOptions( + RequestOptions() + .frame(1000000) + .centerCrop() + ) + .load(url).transform( + RoundGlideTransform( + round, + round, + round, + round, + isSquare = false + ) + ) + .into(imageView) + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/IntentUtils.kt b/base/src/main/java/com/example/base/utils/IntentUtils.kt new file mode 100644 index 0000000..bb9c238 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/IntentUtils.kt @@ -0,0 +1,25 @@ +package com.example.base.utils + +import android.content.Intent +import android.net.Uri + + +object IntentUtils { + + fun getShareTextIntent(text: String?): Intent { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, text) + return getIntent(intent, true) + } + + + fun getDialIntent(phoneNumber: String?): Intent { + val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phoneNumber")) + return getIntent(intent, true) + } + + private fun getIntent(intent: Intent, isNewTask: Boolean): Intent { + return if (isNewTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) else intent + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/L.kt b/base/src/main/java/com/example/base/utils/L.kt new file mode 100644 index 0000000..c4dbb16 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/L.kt @@ -0,0 +1,48 @@ +package com.example.base.utils + +import com.example.base.BuildConfig +import com.orhanobut.logger.AndroidLogAdapter +import com.orhanobut.logger.Logger +import com.orhanobut.logger.PrettyFormatStrategy + + +/** + * + * 描述: 日志打印输出 + * + */ +object L { + + private const val mTag = "Material" + + init { + val formatStrategy = PrettyFormatStrategy.newBuilder() + .showThreadInfo(false) // (Optional) Whether to show thread info or not. Default true + .methodCount(0) // (Optional) How many method line to show. Default 2 + .methodOffset(7) // (Optional) Hides internal method calls up to offset. Default 5 + .tag(mTag) // (Optional) Global tag for every log. Default PRETTY_LOGGER + .build() + Logger.addLogAdapter(object : AndroidLogAdapter(formatStrategy) { + override fun isLoggable(priority: Int, tag: String?): Boolean { + return BuildConfig.DEBUG + } + }) + + } + + fun i(message: String?, vararg args: Any?) { + Logger.i(message ?: "", args) + } + + fun d(any: Any?) { + Logger.d(any) + } + + fun e(message: String?, vararg args: Any?) { + Logger.e(message ?: "", args) + } + + fun json(json: String?) { + Logger.json(json) + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/MMKVUtils.kt b/base/src/main/java/com/example/base/utils/MMKVUtils.kt new file mode 100644 index 0000000..a6c52c4 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/MMKVUtils.kt @@ -0,0 +1,99 @@ +package com.example.base.utils + +import android.os.Parcelable +import com.tencent.mmkv.MMKV +import java.util.Collections + +object MMKVUtils { + + var mmkv: MMKV? = null + + init { + mmkv = MMKV.defaultMMKV() + } + + fun put(key: String, value: Any?): Boolean { + return when (value) { + is String -> mmkv?.encode(key, value)!! + is Float -> mmkv?.encode(key, value)!! + is Boolean -> mmkv?.encode(key, value)!! + is Int -> mmkv?.encode(key, value)!! + is Long -> mmkv?.encode(key, value)!! + is Double -> mmkv?.encode(key, value)!! + is ByteArray -> mmkv?.encode(key, value)!! + else -> false + } + } + + /** + * 这里使用安卓自带的Parcelable序列化,它比java支持的Serializer序列化性能好些 + */ + fun put(key: String, t: T?): Boolean { + if (t == null) { + return false + } + return mmkv?.encode(key, t)!! + } + + fun put(key: String, sets: Set?): Boolean { + if (sets == null) { + return false + } + return mmkv?.encode(key, sets)!! + } + + fun getInt(key: String): Int? { + return mmkv?.decodeInt(key, 0) + } + + fun getDouble(key: String): Double? { + return mmkv?.decodeDouble(key, 0.00) + } + + fun getLong(key: String): Long? { + return mmkv?.decodeLong(key, 0L) + } + + fun getBoolean(key: String, defaultValue: Boolean = false): Boolean { + return mmkv?.decodeBool(key, defaultValue) ?: defaultValue + } + + fun getFloat(key: String): Float? { + return mmkv?.decodeFloat(key, 0F) + } + + fun getByteArray(key: String): ByteArray? { + return mmkv?.decodeBytes(key) + } + + fun getString(key: String): String? { + return mmkv?.decodeString(key, "") + } + + /** + * SpUtils.getParcelable("") + */ + inline fun getParcelable(key: String): T? { + return mmkv?.decodeParcelable(key, T::class.java) + } + + fun getStringSet(key: String): Set? { + return mmkv?.decodeStringSet(key, Collections.emptySet()) + } + + /** + * 是否已经存在 + */ + fun contains(key: String): Boolean? { + return mmkv?.containsKey(key) + } + + fun removeKey(key: String) { + mmkv?.removeValueForKey(key) + } + + fun clearAll() { + mmkv?.clearAll() + } +} + diff --git a/base/src/main/java/com/example/base/utils/NetworkUtils.kt b/base/src/main/java/com/example/base/utils/NetworkUtils.kt new file mode 100644 index 0000000..46713fb --- /dev/null +++ b/base/src/main/java/com/example/base/utils/NetworkUtils.kt @@ -0,0 +1,17 @@ +package com.example.base.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo + +object NetworkUtils { + + fun isNetWorkAvailable(context: Context): Boolean { + return try { + val networkInfo: NetworkInfo? = (context.getSystemService("connectivity") as ConnectivityManager).getActiveNetworkInfo() + networkInfo != null && networkInfo.isAvailable + } catch (var1: Throwable) { + true + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/RegexConstants.kt b/base/src/main/java/com/example/base/utils/RegexConstants.kt new file mode 100644 index 0000000..2f1e677 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/RegexConstants.kt @@ -0,0 +1,116 @@ +package com.example.base.utils + +/** + * @date 2020-06-05 13:49 + * @desc 正则相关常量 + */ +object RegexConstants { + + /** + * 正则:手机号(简单) + */ + const val REGEX_MOBILE_SIMPLE = "^[1]\\d{10}$" + /** + * 正则:手机号(精确) + * + * 移动:134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、178、182、183、184、187、188、198 + * + * 联通:130、131、132、145、155、156、166、171、175、176、185、186 + * + * 电信:133、153、173、177、180、181、189、199 + * + * 全球星:1349 + * + * 虚拟运营商:170 + */ + const val REGEX_MOBILE_EXACT = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(16[6])|(17[0,1,3,5-8])|(18[0-9])|(19[8,9]))\\d{8}$" + /** + * 正则:电话号码 + */ + const val REGEX_TEL = "^0\\d{2,3}[- ]?\\d{7,8}" + /** + * 正则:身份证号码 15 位 + */ + const val REGEX_ID_CARD15 = "^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$" + /** + * 正则:身份证号码 18 位 + */ + const val REGEX_ID_CARD18 = "^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9Xx])$" + /** + * 正则:邮箱 + */ + const val REGEX_EMAIL = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$" + /** + * 正则:URL + */ + const val REGEX_URL = "[a-zA-z]+://[^\\s]*" + /** + * 正则:汉字 + */ + const val REGEX_ZH = "^[\\u4e00-\\u9fa5]+$" + /** + * 正则:用户名,取值范围为 a-z,A-Z,0-9,"_",汉字,不能以"_"结尾,用户名必须是 6-20 位 + */ + const val REGEX_USERNAME = "^[\\w\\u4e00-\\u9fa5]{6,20}(?() + + private fun RegexUtils() { + throw UnsupportedOperationException("u can't instantiate me...") + } + + /////////////////////////////////////////////////////////////////////////// + // If u want more please visit http://toutiao.com/i6231678548520731137 + /////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////// + // If u want more please visit http://toutiao.com/i6231678548520731137 + /////////////////////////////////////////////////////////////////////////// + /** + * Return whether input matches regex of simple mobile. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isMobileSimple(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_MOBILE_SIMPLE, input) + } + + /** + * Return whether input matches regex of exact mobile. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isMobileExact(input: CharSequence?): Boolean { + return isMobileExact(input, null) + } + + /** + * Return whether input matches regex of exact mobile. + * + * @param input The input. + * @param newSegments The new segments of mobile number. + * @return `true`: yes

`false`: no + */ + fun isMobileExact( + input: CharSequence?, + newSegments: List? + ): Boolean { + val match = isMatch(RegexConstants.REGEX_MOBILE_EXACT, input) + if (match) return true + if (newSegments == null) return false + if (input == null || input.length != 11) return false + val content = input.toString() + for (c in content.toCharArray()) { + if (!Character.isDigit(c)) { + return false + } + } + for (newSegment in newSegments) { + if (content.startsWith(newSegment!!)) { + return true + } + } + return false + } + + /** + * Return whether input matches regex of telephone number. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isTel(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_TEL, input) + } + + /** + * Return whether input matches regex of id card number which length is 15. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isIDCard15(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_ID_CARD15, input) + } + + /** + * Return whether input matches regex of id card number which length is 18. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isIDCard18(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_ID_CARD18, input) + } + + /** + * Return whether input matches regex of exact id card number which length is 18. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isIDCard18Exact(input: CharSequence): Boolean { + if (isIDCard18(input)) { + val factor = intArrayOf(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2) + val suffix = + charArrayOf('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2') + if (CITY_MAP.isEmpty) { + CITY_MAP.put("11", "北京") + CITY_MAP.put("12", "天津") + CITY_MAP.put("13", "河北") + CITY_MAP.put("14", "山西") + CITY_MAP.put("15", "内蒙古") + CITY_MAP.put("21", "辽宁") + CITY_MAP.put("22", "吉林") + CITY_MAP.put("23", "黑龙江") + CITY_MAP.put("31", "上海") + CITY_MAP.put("32", "江苏") + CITY_MAP.put("33", "浙江") + CITY_MAP.put("34", "安徽") + CITY_MAP.put("35", "福建") + CITY_MAP.put("36", "江西") + CITY_MAP.put("37", "山东") + CITY_MAP.put("41", "河南") + CITY_MAP.put("42", "湖北") + CITY_MAP.put("43", "湖南") + CITY_MAP.put("44", "广东") + CITY_MAP.put("45", "广西") + CITY_MAP.put("46", "海南") + CITY_MAP.put("50", "重庆") + CITY_MAP.put("51", "四川") + CITY_MAP.put("52", "贵州") + CITY_MAP.put("53", "云南") + CITY_MAP.put("54", "西藏") + CITY_MAP.put("61", "陕西") + CITY_MAP.put("62", "甘肃") + CITY_MAP.put("63", "青海") + CITY_MAP.put("64", "宁夏") + CITY_MAP.put("65", "新疆") + CITY_MAP.put("71", "台湾") + CITY_MAP.put("81", "香港") + CITY_MAP.put("82", "澳门") + CITY_MAP.put("91", "国外") + } + if (CITY_MAP[input.subSequence(0, 2).toString()] != null) { + var weightSum = 0 + for (i in 0..16) { + weightSum += (input[i] - '0') * factor[i] + } + val idCardMod = weightSum % 11 + val idCardLast = input[17] + return idCardLast == suffix[idCardMod] + } + } + return false + } + + /** + * Return whether input matches regex of email. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isEmail(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_EMAIL, input) + } + + /** + * Return whether input matches regex of url. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isURL(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_URL, input) + } + + /** + * Return whether input matches regex of Chinese character. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isZh(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_ZH, input) + } + + /** + * Return whether input matches regex of username. + * + * scope for "a-z", "A-Z", "0-9", "_", "Chinese character" + * + * can't end with "_" + * + * length is between 6 to 20. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isUsername(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_USERNAME, input) + } + + /** + * Return whether input matches regex of date which pattern is "yyyy-MM-dd". + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isDate(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_DATE, input) + } + + /** + * Return whether input matches regex of ip address. + * + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isIP(input: CharSequence?): Boolean { + return isMatch(RegexConstants.REGEX_IP, input) + } + + /** + * Return whether input matches the regex. + * + * @param regex The regex. + * @param input The input. + * @return `true`: yes

`false`: no + */ + fun isMatch(regex: String?, input: CharSequence?): Boolean { + return !input.isNullOrEmpty() && Pattern.matches(regex, input) + } + + /** + * Return the list of input matches the regex. + * + * @param regex The regex. + * @param input The input. + * @return the list of input matches the regex + */ + fun getMatches( + regex: String?, + input: CharSequence? + ): List? { + if (input == null) return Collections.emptyList() + val matches: MutableList = ArrayList() + val pattern: Pattern = Pattern.compile(regex) + val matcher: Matcher = pattern.matcher(input) + while (matcher.find()) { + matches.add(matcher.group()) + } + return matches + } + + + + /** + * Replace the first subsequence of the input sequence that matches the + * regex with the given replacement string. + * + * @param input The input. + * @param regex The regex. + * @param replacement The replacement string. + * @return the string constructed by replacing the first matching + * subsequence by the replacement string, substituting captured + * subsequences as needed + */ + fun getReplaceFirst( + input: String?, + regex: String?, + replacement: String? + ): String? { + return if (input == null) "" else Pattern.compile(regex).matcher(input) + .replaceFirst(replacement) + } + + /** + * Replace every subsequence of the input sequence that matches the + * pattern with the given replacement string. + * + * @param input The input. + * @param regex The regex. + * @param replacement The replacement string. + * @return the string constructed by replacing each matching subsequence + * by the replacement string, substituting captured subsequences + * as needed + */ + fun getReplaceAll( + input: String?, + regex: String?, + replacement: String? + ): String? { + return if (input == null) "" else Pattern.compile(regex).matcher(input) + .replaceAll(replacement) + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/RoundGlideTransform.kt b/base/src/main/java/com/example/base/utils/RoundGlideTransform.kt new file mode 100644 index 0000000..755588e --- /dev/null +++ b/base/src/main/java/com/example/base/utils/RoundGlideTransform.kt @@ -0,0 +1,112 @@ +package com.example.base.utils + +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF +import android.graphics.Shader +import com.bumptech.glide.load.Key +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import com.bumptech.glide.load.resource.bitmap.TransformationUtils +import java.security.MessageDigest +import kotlin.math.min + + +/** + * @Des: Glide + */ +class RoundGlideTransform constructor( + private var roundLeftTop: Float = 5F, + private var roundRightTop: Float = 5F, + private var roundRightBottom: Float = 5F, + private var roundLeftBottom: Float = 5F, + private val isSquare: Boolean = true, + private val isCenterCrop: Boolean = true +) : BitmapTransformation() { + + private var roundArray: FloatArray + private val VERSION = 1 + private val ID = "com.pblk.tiantian.video.utils.RoundGlideTransform.$VERSION" + private val ID_BYTES = ID.toByteArray(Key.CHARSET) + + init { + roundLeftTop = DensityUtils.dp2px(roundLeftTop).toFloat() + roundRightTop = DensityUtils.dp2px(roundRightTop).toFloat() + roundRightBottom = DensityUtils.dp2px(roundRightBottom).toFloat() + roundLeftBottom = DensityUtils.dp2px(roundLeftBottom).toFloat() + roundArray = floatArrayOf( + roundLeftTop, roundLeftTop, + roundRightTop, roundRightTop, + roundRightBottom, roundRightBottom, + roundLeftBottom, roundLeftBottom + ) + } + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap? { + val bitmap: Bitmap = if (isCenterCrop) TransformationUtils.centerCrop( + pool, + toTransform, + outWidth, + outHeight + ) + else toTransform + return roundCrop(pool, bitmap); + } + + private fun roundCrop(pool: BitmapPool, source: Bitmap?): Bitmap? { + if (source == null) return null + var width = source.width + var height = source.height + if (isSquare) { + width = min(width, height) + height = width + } + val x = (source.width - width) / 2 + val y = (source.height - height) / 2 + + val squared = Bitmap.createBitmap(source, x, y, width, height) + + var result: Bitmap? = pool[width, height, Bitmap.Config.ARGB_8888] + if (result == null) { + result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + } + val canvas = Canvas(result!!) + val paint = Paint() + paint.shader = BitmapShader( + squared, + Shader.TileMode.CLAMP, + Shader.TileMode.CLAMP + ) + paint.isAntiAlias = true + val rectF = RectF(0f, 0f, width.toFloat(), height.toFloat()) + val path = Path() + path.addRoundRect( + rectF, + roundArray, + Path.Direction.CW + ) + canvas.drawPath(path, paint); + return result + } + + override fun equals(obj: Any?): Boolean { + return obj is RoundGlideTransform + } + + override fun hashCode(): Int { + return ID.hashCode() + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(ID_BYTES); + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/SPUtils.kt b/base/src/main/java/com/example/base/utils/SPUtils.kt new file mode 100644 index 0000000..e282c83 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/SPUtils.kt @@ -0,0 +1,258 @@ +package com.example.base.utils + +/** + * SharedPreferences封装类 + */ + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken + + +object SPUtils { + + private const val PREFERENCE_NAME = "kotlin_preference" + + /** + * put string preferences + + * @param key The name of the preference to modify + * * + * @param value The new value for the preference + * * + * @return True if the new values were successfully written to persistent storage. + */ + fun putString(key: String, value: String): Boolean { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + val editor = settings.edit() + editor.putString(key, value) + return editor.commit() + } + + fun getString(key: String, defaultValue: String = ""): String { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + return settings.getString(key, defaultValue) ?: "" + } + + /** + * put int preferences + + * @param key The name of the preference to modify + * * + * @param value The new value for the preference + * * + * @return True if the new values were successfully written to persistent storage. + */ + fun putInt(key: String, value: Int): Boolean { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + val editor = settings.edit() + editor.putInt(key, value) + return editor.commit() + } + + /** + * get int preferences + + * @param key The name of the preference to retrieve + * * + * @param defaultValue Value to return if this preference does not exist + * * + * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with + * * this name that is not a int + */ + @JvmOverloads + fun getInt(key: String, defaultValue: Int = -1): Int { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + return settings.getInt(key, defaultValue) + } + + /** + * put long preferences + + * @param key The name of the preference to modify + * * + * @param value The new value for the preference + * * + * @return True if the new values were successfully written to persistent storage. + */ + fun putLong(key: String, value: Long): Boolean { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + val editor = settings.edit() + editor.putLong(key, value) + return editor.commit() + } + + /** + * get long preferences + + * @param key The name of the preference to retrieve + * * + * @param defaultValue Value to return if this preference does not exist + * * + * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with + * * this name that is not a long + */ + @JvmOverloads + fun getLong(key: String, defaultValue: Long = -1): Long { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + return settings.getLong(key, defaultValue) + } + + /** + * put float preferences + + * @param context + * * + * @param key The name of the preference to modify + * * + * @param value The new value for the preference + * * + * @return True if the new values were successfully written to persistent storage. + */ + fun putFloat(context: Context, key: String, value: Float): Boolean { + val settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + val editor = settings.edit() + editor.putFloat(key, value) + return editor.commit() + } + + /** + * get float preferences + + * @param key The name of the preference to retrieve + * * + * @param defaultValue Value to return if this preference does not exist + * * + * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with + * * this name that is not a float + */ + @JvmOverloads + fun getFloat(key: String, defaultValue: Float = -1f): Float { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + return settings.getFloat(key, defaultValue) + } + + /** + * put boolean preferences + + * @param key The name of the preference to modify + * * + * @param value The new value for the preference + * * + * @return True if the new values were successfully written to persistent storage. + */ + fun putBoolean(key: String, value: Boolean): Boolean { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + val editor = settings.edit() + editor.putBoolean(key, value) + return editor.commit() + } + + /** + * get boolean preferences + + * @param key The name of the preference to retrieve + * * + * @param defaultValue Value to return if this preference does not exist + * * + * @return The preference value if it exists, or defValue. Throws ClassCastException if there is a preference with + * * this name that is not a boolean + */ + @JvmOverloads + fun getBoolean(key: String, defaultValue: Boolean = false): Boolean { + val settings = + Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + return settings.getBoolean(key, defaultValue) + } + + /** + * 是否已经存在 + */ + fun contains(key: String): Boolean { + val settings = Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + return settings.contains(key) + } + + /** + * 删除 + */ + fun remove(key: String) { + val settings = Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + settings.edit().remove(key).apply() + } + + fun clearAllData(): Boolean { + val settings = Utils.getApp().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + val editor = settings.edit() + editor.clear() + return editor.commit() + } + + fun putObject(key: String, obj: T?) { + if (obj == null) { + putString(key, "") + } else { + val str = Gson().toJson(obj) + putString(key, str) + } + } + + fun putObject(obj: Any?) { + obj ?: return + val str = Gson().toJson(obj) + putString(obj::class.java.simpleName, str) + } + + inline fun putList(list: List?) { + list ?: return + val str = Gson().toJson(list) + putString("list_${T::class.java.simpleName}", str) + } + + + fun getObject(key: String, clazz: Class): T? { + val str = getString(key) + if (str.isEmpty()) { + return null + } + return try { + Gson().fromJson(str, clazz) + } catch (e: JsonSyntaxException) { + null + } + } + + fun getObject(clazz: Class): T? { + val str = getString(clazz.simpleName) + if (str.isEmpty()) { + return null + } + return try { + Gson().fromJson(str, clazz) + } catch (e: JsonSyntaxException) { + null + } + } + + inline fun getList(): List { + val str = getString("list_${T::class.java.simpleName}") + if (str.isEmpty()) { + return emptyList() + } + return try { + Gson().fromJson(str, object : TypeToken?>() {}.type) + } catch (e: java.lang.Exception) { + emptyList() + } + } + +} diff --git a/base/src/main/java/com/example/base/utils/ScreenUtils.kt b/base/src/main/java/com/example/base/utils/ScreenUtils.kt new file mode 100644 index 0000000..390ba5a --- /dev/null +++ b/base/src/main/java/com/example/base/utils/ScreenUtils.kt @@ -0,0 +1,50 @@ +package com.example.base.utils + +import android.content.Context +import android.graphics.Point +import android.provider.Settings +import android.view.WindowManager + + +object ScreenUtils { + fun getScreenHeight(): Int { + val wm = Utils.getApp().getSystemService(Context.WINDOW_SERVICE) as WindowManager? + ?: return -1 + val point = Point() + wm.defaultDisplay.getRealSize(point) + return point.y + } + + + fun getScreenWidth(): Int { + val wm = + Utils.getApp().getSystemService(Context.WINDOW_SERVICE) as WindowManager? ?: return -1 + val point = Point() + wm.defaultDisplay.getRealSize(point) + return point.x + } + + /** + * 获取真实窗口大小 + */ + fun getWindowSize(): Point { + val point = Point() + val displayMetrics = Utils.getApp().resources.displayMetrics + point.x = displayMetrics.widthPixels + point.y = displayMetrics.heightPixels + return point + } + + /** + * 判断是否开启了 “屏幕自动旋转” + */ + fun isScreenAutoRotate(): Boolean { + var gravity = 0 + try { + gravity = Settings.System.getInt(Utils.getApp().contentResolver, Settings.System.ACCELEROMETER_ROTATION) + } catch (e: Settings.SettingNotFoundException) { + e.printStackTrace() + } + return gravity == 1 + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/SpanUtils.kt b/base/src/main/java/com/example/base/utils/SpanUtils.kt new file mode 100644 index 0000000..fe387b0 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/SpanUtils.kt @@ -0,0 +1,1351 @@ +package com.example.base.utils + +import android.annotation.SuppressLint +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.BlurMaskFilter +import android.graphics.BlurMaskFilter.Blur +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Paint.FontMetricsInt +import android.graphics.Path +import android.graphics.Shader +import android.graphics.Typeface +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.Layout +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.AbsoluteSizeSpan +import android.text.style.AlignmentSpan +import android.text.style.BackgroundColorSpan +import android.text.style.CharacterStyle +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.LeadingMarginSpan +import android.text.style.LineHeightSpan +import android.text.style.MaskFilterSpan +import android.text.style.RelativeSizeSpan +import android.text.style.ReplacementSpan +import android.text.style.ScaleXSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan +import android.text.style.SubscriptSpan +import android.text.style.SuperscriptSpan +import android.text.style.TypefaceSpan +import android.text.style.URLSpan +import android.text.style.UnderlineSpan +import android.text.style.UpdateAppearance +import android.util.Log +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.FloatRange +import androidx.annotation.IntDef +import androidx.annotation.IntRange +import androidx.core.content.ContextCompat +import com.example.base.utils.Utils.getApp +import java.io.Serializable +import java.lang.ref.WeakReference + +/** + *
+ * author: Blankj
+ * blog  : http://blankj.com
+ * time  : 16/12/13
+ * desc  : utils about span
+
* + */ +class SpanUtils() { + @IntDef(ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP) + @Retention(AnnotationRetention.SOURCE) + annotation class Align + + private var mTextView: TextView? = null + private var mText: CharSequence + private var flag = 0 + private var foregroundColor = 0 + private var backgroundColor = 0 + private var lineHeight = 0 + private var alignLine = 0 + private var quoteColor = 0 + private var stripeWidth = 0 + private var quoteGapWidth = 0 + private var first = 0 + private var rest = 0 + private var bulletColor = 0 + private var bulletRadius = 0 + private var bulletGapWidth = 0 + private var fontSize = 0 + private var proportion = 0f + private var xProportion = 0f + private var isStrikethrough = false + private var isUnderline = false + private var isSuperscript = false + private var isSubscript = false + private var isBold = false + private var isItalic = false + private var isBoldItalic = false + private var fontFamily: String? = null + private var typeface: Typeface? = null + private var alignment: Layout.Alignment? = null + private var verticalAlign = 0 + private var clickSpan: ClickableSpan? = null + private var url: String? = null + private var blurRadius = 0f + private var style: Blur? = null + private var shader: Shader? = null + private var shadowRadius = 0f + private var shadowDx = 0f + private var shadowDy = 0f + private var shadowColor = 0 + private var spans: Array? = null + private var imageBitmap: Bitmap? = null + private var imageDrawable: Drawable? = null + private var imageUri: Uri? = null + private var imageResourceId = 0 + private var alignImage = 0 + private var spaceSize = 0 + private var spaceColor = 0 + private val mBuilder: SerializableSpannableStringBuilder + private var isCreated = false + private var mType: Int + private val mTypeCharSequence = 0 + private val mTypeImage = 1 + private val mTypeSpace = 2 + + private constructor(textView: TextView) : this() { + mTextView = textView + } + + init { + mBuilder = SerializableSpannableStringBuilder() + mText = "" + mType = -1 + setDefault() + } + + private fun setDefault() { + flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + foregroundColor = COLOR_DEFAULT + backgroundColor = COLOR_DEFAULT + lineHeight = -1 + quoteColor = COLOR_DEFAULT + first = -1 + bulletColor = COLOR_DEFAULT + fontSize = -1 + proportion = -1f + xProportion = -1f + isStrikethrough = false + isUnderline = false + isSuperscript = false + isSubscript = false + isBold = false + isItalic = false + isBoldItalic = false + fontFamily = null + typeface = null + alignment = null + verticalAlign = -1 + clickSpan = null + url = null + blurRadius = -1f + shader = null + shadowRadius = -1f + spans = null + imageBitmap = null + imageDrawable = null + imageUri = null + imageResourceId = -1 + spaceSize = -1 + } + + /** + * Set the span of flag. + * + * @param flag The flag. + * + * * [Spanned.SPAN_INCLUSIVE_EXCLUSIVE] + * * [Spanned.SPAN_INCLUSIVE_INCLUSIVE] + * * [Spanned.SPAN_EXCLUSIVE_EXCLUSIVE] + * * [Spanned.SPAN_EXCLUSIVE_INCLUSIVE] + * + * @return the single [SpanUtils] instance + */ + fun setFlag(flag: Int): SpanUtils { + this.flag = flag + return this + } + + /** + * Set the span of foreground's color. + * + * @param color The color of foreground + * @return the single [SpanUtils] instance + */ + fun setForegroundColor(@ColorInt color: Int): SpanUtils { + foregroundColor = color + return this + } + + /** + * Set the span of background's color. + * + * @param color The color of background + * @return the single [SpanUtils] instance + */ + fun setBackgroundColor(@ColorInt color: Int): SpanUtils { + backgroundColor = color + return this + } + + /** + * Set the span of line height. + * + * @param lineHeight The line height, in pixel. + * @return the single [SpanUtils] instance + */ + fun setLineHeight(@IntRange(from = 0) lineHeight: Int): SpanUtils { + return setLineHeight(lineHeight, ALIGN_CENTER) + } + + /** + * Set the span of line height. + * + * @param lineHeight The line height, in pixel. + * @param align The alignment. + * + * * [Align.ALIGN_TOP] + * * [Align.ALIGN_CENTER] + * * [Align.ALIGN_BOTTOM] + * + * @return the single [SpanUtils] instance + */ + fun setLineHeight( + @IntRange(from = 0) lineHeight: Int, + @Align align: Int + ): SpanUtils { + this.lineHeight = lineHeight + alignLine = align + return this + } + + /** + * Set the span of quote's color. + * + * @param color The color of quote + * @return the single [SpanUtils] instance + */ + fun setQuoteColor(@ColorInt color: Int): SpanUtils { + return setQuoteColor(color, 2, 2) + } + + /** + * Set the span of quote's color. + * + * @param color The color of quote. + * @param stripeWidth The width of stripe, in pixel. + * @param gapWidth The width of gap, in pixel. + * @return the single [SpanUtils] instance + */ + fun setQuoteColor( + @ColorInt color: Int, + @IntRange(from = 1) stripeWidth: Int, + @IntRange(from = 0) gapWidth: Int + ): SpanUtils { + quoteColor = color + this.stripeWidth = stripeWidth + quoteGapWidth = gapWidth + return this + } + + /** + * Set the span of leading margin. + * + * @param first The indent for the first line of the paragraph. + * @param rest The indent for the remaining lines of the paragraph. + * @return the single [SpanUtils] instance + */ + fun setLeadingMargin( + @IntRange(from = 0) first: Int, + @IntRange(from = 0) rest: Int + ): SpanUtils { + this.first = first + this.rest = rest + return this + } + + /** + * Set the span of bullet. + * + * @param gapWidth The width of gap, in pixel. + * @return the single [SpanUtils] instance + */ + fun setBullet(@IntRange(from = 0) gapWidth: Int): SpanUtils { + return setBullet(0, 3, gapWidth) + } + + /** + * Set the span of bullet. + * + * @param color The color of bullet. + * @param radius The radius of bullet, in pixel. + * @param gapWidth The width of gap, in pixel. + * @return the single [SpanUtils] instance + */ + fun setBullet( + @ColorInt color: Int, + @IntRange(from = 0) radius: Int, + @IntRange(from = 0) gapWidth: Int + ): SpanUtils { + bulletColor = color + bulletRadius = radius + bulletGapWidth = gapWidth + return this + } + + /** + * Set the span of font's size. + * + * @param size The size of font. + * @return the single [SpanUtils] instance + */ + fun setFontSize(@IntRange(from = 0) size: Int): SpanUtils { + return setFontSize(size, false) + } + + /** + * Set the span of size of font. + * + * @param size The size of font. + * @param isSp True to use sp, false to use pixel. + * @return the single [SpanUtils] instance + */ + fun setFontSize(@IntRange(from = 0) size: Int, isSp: Boolean): SpanUtils { + if (isSp) { + val fontScale = Resources.getSystem().displayMetrics.scaledDensity + fontSize = (size * fontScale + 0.5f).toInt() + } else { + fontSize = size + } + return this + } + + /** + * Set the span of proportion of font. + * + * @param proportion The proportion of font. + * @return the single [SpanUtils] instance + */ + fun setFontProportion(proportion: Float): SpanUtils { + this.proportion = proportion + return this + } + + /** + * Set the span of transverse proportion of font. + * + * @param proportion The transverse proportion of font. + * @return the single [SpanUtils] instance + */ + fun setFontXProportion(proportion: Float): SpanUtils { + xProportion = proportion + return this + } + + /** + * Set the span of strikethrough. + * + * @return the single [SpanUtils] instance + */ + fun setStrikethrough(): SpanUtils { + isStrikethrough = true + return this + } + + /** + * Set the span of underline. + * + * @return the single [SpanUtils] instance + */ + fun setUnderline(): SpanUtils { + isUnderline = true + return this + } + + /** + * Set the span of superscript. + * + * @return the single [SpanUtils] instance + */ + fun setSuperscript(): SpanUtils { + isSuperscript = true + return this + } + + /** + * Set the span of subscript. + * + * @return the single [SpanUtils] instance + */ + fun setSubscript(): SpanUtils { + isSubscript = true + return this + } + + /** + * Set the span of bold. + * + * @return the single [SpanUtils] instance + */ + fun setBold(): SpanUtils { + isBold = true + return this + } + + /** + * Set the span of italic. + * + * @return the single [SpanUtils] instance + */ + fun setItalic(): SpanUtils { + isItalic = true + return this + } + + /** + * Set the span of bold italic. + * + * @return the single [SpanUtils] instance + */ + fun setBoldItalic(): SpanUtils { + isBoldItalic = true + return this + } + + /** + * Set the span of font family. + * + * @param fontFamily The font family. + * + * * monospace + * * serif + * * sans-serif + * + * @return the single [SpanUtils] instance + */ + fun setFontFamily(fontFamily: String): SpanUtils { + this.fontFamily = fontFamily + return this + } + + /** + * Set the span of typeface. + * + * @param typeface The typeface. + * @return the single [SpanUtils] instance + */ + fun setTypeface(typeface: Typeface): SpanUtils { + this.typeface = typeface + return this + } + + /** + * Set the span of horizontal alignment. + * + * @param alignment The alignment. + * + * * [Alignment.ALIGN_NORMAL] + * * [Alignment.ALIGN_OPPOSITE] + * * [Alignment.ALIGN_CENTER] + * + * @return the single [SpanUtils] instance + */ + fun setHorizontalAlign(alignment: Layout.Alignment): SpanUtils { + this.alignment = alignment + return this + } + + /** + * Set the span of vertical alignment. + * + * @param align The alignment. + * + * * [Align.ALIGN_TOP] + * * [Align.ALIGN_CENTER] + * * [Align.ALIGN_BASELINE] + * * [Align.ALIGN_BOTTOM] + * + * @return the single [SpanUtils] instance + */ + fun setVerticalAlign(@Align align: Int): SpanUtils { + verticalAlign = align + return this + } + + /** + * Set the span of click. + * + * Must set `view.setMovementMethod(LinkMovementMethod.getInstance())` + * + * @param clickSpan The span of click. + * @return the single [SpanUtils] instance + */ + fun setClickSpan(clickSpan: ClickableSpan): SpanUtils { + setMovementMethodIfNeed() + this.clickSpan = clickSpan + return this + } + + /** + * Set the span of click. + * + * Must set `view.setMovementMethod(LinkMovementMethod.getInstance())` + * + * @param color The color of click span. + * @param underlineText True to support underline, false otherwise. + * @param listener The listener of click span. + * @return the single [SpanUtils] instance + */ + fun setClickSpan( + @ColorInt color: Int, + underlineText: Boolean, + listener: View.OnClickListener? + ): SpanUtils { + setMovementMethodIfNeed() + clickSpan = object : ClickableSpan() { + override fun updateDrawState(paint: TextPaint) { + paint.setColor(color) + paint.isUnderlineText = underlineText + } + + override fun onClick(widget: View) { + listener?.onClick(widget) + } + } + return this + } + + /** + * Set the span of url. + * + * Must set `view.setMovementMethod(LinkMovementMethod.getInstance())` + * + * @param url The url. + * @return the single [SpanUtils] instance + */ + fun setUrl(url: String): SpanUtils { + setMovementMethodIfNeed() + this.url = url + return this + } + + private fun setMovementMethodIfNeed() { + if (mTextView != null && mTextView!!.movementMethod == null) { + mTextView!!.movementMethod = LinkMovementMethod.getInstance() + } + } + + /** + * Set the span of blur. + * + * @param radius The radius of blur. + * @param style The style. + * + * * [Blur.NORMAL] + * * [Blur.SOLID] + * * [Blur.OUTER] + * * [Blur.INNER] + * + * @return the single [SpanUtils] instance + */ + fun setBlur( + @FloatRange(from = 0.0, fromInclusive = false) radius: Float, + style: Blur? + ): SpanUtils { + blurRadius = radius + this.style = style + return this + } + + /** + * Set the span of shader. + * + * @param shader The shader. + * @return the single [SpanUtils] instance + */ + fun setShader(shader: Shader): SpanUtils { + this.shader = shader + return this + } + + /** + * Set the span of shadow. + * + * @param radius The radius of shadow. + * @param dx X-axis offset, in pixel. + * @param dy Y-axis offset, in pixel. + * @param shadowColor The color of shadow. + * @return the single [SpanUtils] instance + */ + fun setShadow( + @FloatRange(from = 0.0, fromInclusive = false) radius: Float, + dx: Float, + dy: Float, + shadowColor: Int + ): SpanUtils { + shadowRadius = radius + shadowDx = dx + shadowDy = dy + this.shadowColor = shadowColor + return this + } + + /** + * Set the spans. + * + * @param spans The spans. + * @return the single [SpanUtils] instance + */ + fun setSpans(vararg spans: Any): SpanUtils { + if (spans.isNotEmpty()) { + this.spans = arrayOf(spans) + } + return this + } + + /** + * Append the text text. + * + * @param text The text. + * @return the single [SpanUtils] instance + */ + fun append(text: CharSequence): SpanUtils { + apply(mTypeCharSequence) + mText = text + return this + } + + /** + * Append one line. + * + * @return the single [SpanUtils] instance + */ + fun appendLine(): SpanUtils { + apply(mTypeCharSequence) + mText = LINE_SEPARATOR + return this + } + + /** + * Append text and one line. + * + * @return the single [SpanUtils] instance + */ + fun appendLine(text: CharSequence): SpanUtils { + apply(mTypeCharSequence) + mText = text.toString() + LINE_SEPARATOR + return this + } + /** + * Append one image. + * + * @param bitmap The bitmap. + * @param align The alignment. + * + * * [Align.ALIGN_TOP] + * * [Align.ALIGN_CENTER] + * * [Align.ALIGN_BASELINE] + * * [Align.ALIGN_BOTTOM] + * + * @return the single [SpanUtils] instance + */ + /** + * Append one image. + * + * @param bitmap The bitmap of image. + * @return the single [SpanUtils] instance + */ + @JvmOverloads + fun appendImage(bitmap: Bitmap, @Align align: Int = ALIGN_BOTTOM): SpanUtils { + apply(mTypeImage) + imageBitmap = bitmap + alignImage = align + return this + } + /** + * Append one image. + * + * @param drawable The drawable of image. + * @param align The alignment. + * + * * [Align.ALIGN_TOP] + * * [Align.ALIGN_CENTER] + * * [Align.ALIGN_BASELINE] + * * [Align.ALIGN_BOTTOM] + * + * @return the single [SpanUtils] instance + */ + /** + * Append one image. + * + * @param drawable The drawable of image. + * @return the single [SpanUtils] instance + */ + @JvmOverloads + fun appendImage(drawable: Drawable, @Align align: Int = ALIGN_BOTTOM): SpanUtils { + apply(mTypeImage) + imageDrawable = drawable + alignImage = align + return this + } + /** + * Append one image. + * + * @param uri The uri of image. + * @param align The alignment. + * + * * [Align.ALIGN_TOP] + * * [Align.ALIGN_CENTER] + * * [Align.ALIGN_BASELINE] + * * [Align.ALIGN_BOTTOM] + * + * @return the single [SpanUtils] instance + */ + /** + * Append one image. + * + * @param uri The uri of image. + * @return the single [SpanUtils] instance + */ + @JvmOverloads + fun appendImage(uri: Uri, @Align align: Int = ALIGN_BOTTOM): SpanUtils { + apply(mTypeImage) + imageUri = uri + alignImage = align + return this + } + /** + * Append one image. + * + * @param resourceId The resource id of image. + * @param align The alignment. + * + * * [Align.ALIGN_TOP] + * * [Align.ALIGN_CENTER] + * * [Align.ALIGN_BASELINE] + * * [Align.ALIGN_BOTTOM] + * + * @return the single [SpanUtils] instance + */ + /** + * Append one image. + * + * @param resourceId The resource id of image. + * @return the single [SpanUtils] instance + */ + @JvmOverloads + fun appendImage(@DrawableRes resourceId: Int, @Align align: Int = ALIGN_BOTTOM): SpanUtils { + apply(mTypeImage) + imageResourceId = resourceId + alignImage = align + return this + } + /** + * Append space. + * + * @param size The size of space. + * @param color The color of space. + * @return the single [SpanUtils] instance + */ + /** + * Append space. + * + * @param size The size of space. + * @return the single [SpanUtils] instance + */ + @JvmOverloads + fun appendSpace(@IntRange(from = 0) size: Int, @ColorInt color: Int = Color.TRANSPARENT): SpanUtils { + apply(mTypeSpace) + spaceSize = size + spaceColor = color + return this + } + + private fun apply(type: Int) { + applyLast() + mType = type + } + + fun get(): SpannableStringBuilder { + return mBuilder + } + + /** + * Create the span string. + * + * @return the span string + */ + fun create(): SpannableStringBuilder { + applyLast() + if (mTextView != null) { + mTextView!!.text = mBuilder + } + isCreated = true + return mBuilder + } + + private fun applyLast() { + if (isCreated) { + return + } + if (mType == mTypeCharSequence) { + updateCharCharSequence() + } else if (mType == mTypeImage) { + updateImage() + } else if (mType == mTypeSpace) { + updateSpace() + } + setDefault() + } + + private fun updateCharCharSequence() { + if (mText.length == 0) return + var start = mBuilder.length + if (start == 0 && lineHeight != -1) { // bug of LineHeightSpan when first line + mBuilder.append(Character.toString(2.toChar())) + .append("\n") + .setSpan(AbsoluteSizeSpan(0), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + start = 2 + } + mBuilder.append(mText) + val end = mBuilder.length + if (verticalAlign != -1) { + mBuilder.setSpan(VerticalAlignSpan(verticalAlign), start, end, flag) + } + if (foregroundColor != COLOR_DEFAULT) { + mBuilder.setSpan(ForegroundColorSpan(foregroundColor), start, end, flag) + } + if (backgroundColor != COLOR_DEFAULT) { + mBuilder.setSpan(BackgroundColorSpan(backgroundColor), start, end, flag) + } + if (first != -1) { + mBuilder.setSpan(LeadingMarginSpan.Standard(first, rest), start, end, flag) + } + if (quoteColor != COLOR_DEFAULT) { + mBuilder.setSpan( + CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth), + start, + end, + flag + ) + } + if (bulletColor != COLOR_DEFAULT) { + mBuilder.setSpan( + CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth), + start, + end, + flag + ) + } + if (fontSize != -1) { + mBuilder.setSpan(AbsoluteSizeSpan(fontSize, false), start, end, flag) + } + if (proportion != -1f) { + mBuilder.setSpan(RelativeSizeSpan(proportion), start, end, flag) + } + if (xProportion != -1f) { + mBuilder.setSpan(ScaleXSpan(xProportion), start, end, flag) + } + if (lineHeight != -1) { + mBuilder.setSpan(CustomLineHeightSpan(lineHeight, alignLine), start, end, flag) + } + if (isStrikethrough) { + mBuilder.setSpan(StrikethroughSpan(), start, end, flag) + } + if (isUnderline) { + mBuilder.setSpan(UnderlineSpan(), start, end, flag) + } + if (isSuperscript) { + mBuilder.setSpan(SuperscriptSpan(), start, end, flag) + } + if (isSubscript) { + mBuilder.setSpan(SubscriptSpan(), start, end, flag) + } + if (isBold) { + mBuilder.setSpan(StyleSpan(Typeface.BOLD), start, end, flag) + } + if (isItalic) { + mBuilder.setSpan(StyleSpan(Typeface.ITALIC), start, end, flag) + } + if (isBoldItalic) { + mBuilder.setSpan(StyleSpan(Typeface.BOLD_ITALIC), start, end, flag) + } + if (fontFamily != null) { + mBuilder.setSpan(TypefaceSpan(fontFamily), start, end, flag) + } + if (typeface != null) { + mBuilder.setSpan(CustomTypefaceSpan(typeface!!), start, end, flag) + } + if (alignment != null) { + mBuilder.setSpan(AlignmentSpan.Standard(alignment!!), start, end, flag) + } + if (clickSpan != null) { + mBuilder.setSpan(clickSpan, start, end, flag) + } + if (url != null) { + mBuilder.setSpan(URLSpan(url), start, end, flag) + } + if (blurRadius != -1f) { + mBuilder.setSpan( + MaskFilterSpan(BlurMaskFilter(blurRadius, style)), + start, + end, + flag + ) + } + if (shader != null) { + mBuilder.setSpan(ShaderSpan(shader!!), start, end, flag) + } + if (shadowRadius != -1f) { + mBuilder.setSpan( + ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor), + start, + end, + flag + ) + } + if (spans != null) { + for (span in spans!!) { + mBuilder.setSpan(span, start, end, flag) + } + } + } + + private fun updateImage() { + val start = mBuilder.length + mText = "" + updateCharCharSequence() + val end = mBuilder.length + if (imageBitmap != null) { + mBuilder.setSpan(CustomImageSpan(imageBitmap!!, alignImage), start, end, flag) + } else if (imageDrawable != null) { + mBuilder.setSpan(CustomImageSpan(imageDrawable!!, alignImage), start, end, flag) + } else if (imageUri != null) { + mBuilder.setSpan(CustomImageSpan(imageUri!!, alignImage), start, end, flag) + } else if (imageResourceId != -1) { + mBuilder.setSpan(CustomImageSpan(imageResourceId, alignImage), start, end, flag) + } + } + + private fun updateSpace() { + val start = mBuilder.length + mText = "< >" + updateCharCharSequence() + val end = mBuilder.length + mBuilder.setSpan(SpaceSpan(spaceSize, spaceColor), start, end, flag) + } + + internal class VerticalAlignSpan(val mVerticalAlignment: Int) : ReplacementSpan() { + override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: FontMetricsInt?): Int { + var text = text + text = text.subSequence(start, end) + return paint.measureText(text.toString()).toInt() + } + + override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { + var text = text + text = text.subSequence(start, end) + val fm = paint.getFontMetricsInt() + // int need = height - (v + fm.descent - fm.ascent - spanstartv); +// if (need > 0) { +// if (mVerticalAlignment == ALIGN_TOP) { +// fm.descent += need; +// } else if (mVerticalAlignment == ALIGN_CENTER) { +// fm.descent += need / 2; +// fm.ascent -= need / 2; +// } else { +// fm.ascent -= need; +// } +// } +// need = height - (v + fm.bottom - fm.top - spanstartv); +// if (need > 0) { +// if (mVerticalAlignment == ALIGN_TOP) { +// fm.bottom += need; +// } else if (mVerticalAlignment == ALIGN_CENTER) { +// fm.bottom += need / 2; +// fm.top -= need / 2; +// } else { +// fm.top -= need; +// } +// } + canvas.drawText(text.toString(), x, (y - ((y + fm.descent + y + fm.ascent) / 2 - (bottom + top) / 2)).toFloat(), paint) + } + + companion object { + const val ALIGN_CENTER = 2 + const val ALIGN_TOP = 3 + } + } + + internal class CustomLineHeightSpan(private val height: Int, val mVerticalAlignment: Int) : LineHeightSpan { + override fun chooseHeight( + text: CharSequence, start: Int, end: Int, + spanstartv: Int, v: Int, fm: FontMetricsInt + ) { +// LogUtils.e(fm, sfm); + if (sfm == null) { + sfm = FontMetricsInt() + sfm!!.top = fm.top + sfm!!.ascent = fm.ascent + sfm!!.descent = fm.descent + sfm!!.bottom = fm.bottom + sfm!!.leading = fm.leading + } else { + fm.top = sfm!!.top + fm.ascent = sfm!!.ascent + fm.descent = sfm!!.descent + fm.bottom = sfm!!.bottom + fm.leading = sfm!!.leading + } + var need = height - (v + fm.descent - fm.ascent - spanstartv) + if (need > 0) { + if (mVerticalAlignment == ALIGN_TOP) { + fm.descent += need + } else if (mVerticalAlignment == ALIGN_CENTER) { + fm.descent += need / 2 + fm.ascent -= need / 2 + } else { + fm.ascent -= need + } + } + need = height - (v + fm.bottom - fm.top - spanstartv) + if (need > 0) { + if (mVerticalAlignment == ALIGN_TOP) { + fm.bottom += need + } else if (mVerticalAlignment == ALIGN_CENTER) { + fm.bottom += need / 2 + fm.top -= need / 2 + } else { + fm.top -= need + } + } + if (end == (text as Spanned).getSpanEnd(this)) { + sfm = null + } + // LogUtils.e(fm, sfm); + } + + companion object { + const val ALIGN_CENTER = 2 + const val ALIGN_TOP = 3 + var sfm: FontMetricsInt? = null + } + } + + internal class SpaceSpan(private val width: Int, color: Int = Color.TRANSPARENT) : ReplacementSpan() { + private val paint = Paint() + + init { + paint.setColor(color) + paint.style = Paint.Style.FILL + } + + override fun getSize( + paint: Paint, text: CharSequence, + @IntRange(from = 0) start: Int, + @IntRange(from = 0) end: Int, + fm: FontMetricsInt? + ): Int { + return width + } + + override fun draw( + canvas: Canvas, text: CharSequence, + @IntRange(from = 0) start: Int, + @IntRange(from = 0) end: Int, + x: Float, top: Int, y: Int, bottom: Int, + paint: Paint + ) { + canvas.drawRect(x, top.toFloat(), x + width, bottom.toFloat(), this.paint) + } + } + + internal class CustomQuoteSpan(private val color: Int, private val stripeWidth: Int, private val gapWidth: Int) : LeadingMarginSpan { + override fun getLeadingMargin(first: Boolean): Int { + return stripeWidth + gapWidth + } + + override fun drawLeadingMargin( + c: Canvas, p: Paint, x: Int, dir: Int, + top: Int, baseline: Int, bottom: Int, + text: CharSequence, start: Int, end: Int, + first: Boolean, layout: Layout + ) { + val style = p.style + val color = p.color + p.style = Paint.Style.FILL + p.setColor(this.color) + c.drawRect(x.toFloat(), top.toFloat(), (x + dir * stripeWidth).toFloat(), bottom.toFloat(), p) + p.style = style + p.setColor(color) + } + } + + internal class CustomBulletSpan(private val color: Int, private val radius: Int, private val gapWidth: Int) : LeadingMarginSpan { + private var sBulletPath: Path? = null + override fun getLeadingMargin(first: Boolean): Int { + return 2 * radius + gapWidth + } + + override fun drawLeadingMargin( + c: Canvas, p: Paint, x: Int, dir: Int, + top: Int, baseline: Int, bottom: Int, + text: CharSequence, start: Int, end: Int, + first: Boolean, l: Layout + ) { + if ((text as Spanned).getSpanStart(this) == start) { + val style = p.style + var oldColor = 0 + oldColor = p.color + p.setColor(color) + p.style = Paint.Style.FILL + if (c.isHardwareAccelerated) { + if (sBulletPath == null) { + sBulletPath = Path() + // Bullet is slightly better to avoid aliasing artifacts on mdpi devices. + sBulletPath!!.addCircle(0.0f, 0.0f, radius.toFloat(), Path.Direction.CW) + } + c.save() + c.translate((x + dir * radius).toFloat(), (top + bottom) / 2.0f) + c.drawPath(sBulletPath!!, p) + c.restore() + } else { + c.drawCircle((x + dir * radius).toFloat(), (top + bottom) / 2.0f, radius.toFloat(), p) + } + p.setColor(oldColor) + p.style = style + } + } + } + + @SuppressLint("ParcelCreator") + internal class CustomTypefaceSpan(private val newType: Typeface) : TypefaceSpan("") { + override fun updateDrawState(textPaint: TextPaint) { + apply(textPaint, newType) + } + + override fun updateMeasureState(paint: TextPaint) { + apply(paint, newType) + } + + private fun apply(paint: Paint, tf: Typeface) { + val oldStyle: Int + val old = paint.typeface + oldStyle = old?.style ?: 0 + val fake = oldStyle and tf.style.inv() + if (fake and Typeface.BOLD != 0) { + paint.isFakeBoldText = true + } + if (fake and Typeface.ITALIC != 0) { + paint.textSkewX = -0.25f + } + paint.shader + paint.setTypeface(tf) + } + } + + internal class CustomImageSpan : CustomDynamicDrawableSpan { + private var mDrawable: Drawable? = null + private var mContentUri: Uri? = null + private var mResourceId = 0 + + constructor(b: Bitmap, verticalAlignment: Int) : super(verticalAlignment) { + mDrawable = BitmapDrawable(getApp().resources, b) + (mDrawable!! as BitmapDrawable).apply { + setBounds(0, 0, this.intrinsicWidth, this.intrinsicHeight) + } + } + + constructor(d: Drawable, verticalAlignment: Int) : super(verticalAlignment) { + mDrawable = d + mDrawable!!.setBounds( + 0, 0, mDrawable!!.intrinsicWidth, mDrawable!!.intrinsicHeight + ) + } + + constructor(uri: Uri, verticalAlignment: Int) : super(verticalAlignment) { + mContentUri = uri + } + + constructor(@DrawableRes resourceId: Int, verticalAlignment: Int) : super(verticalAlignment) { + mResourceId = resourceId + } + + override val drawable: Drawable? + get() { + var drawable: Drawable? = null + if (mDrawable != null) { + drawable = mDrawable + } else if (mContentUri != null) { + val bitmap: Bitmap + try { + val `is` = getApp().contentResolver.openInputStream(mContentUri!!) + bitmap = BitmapFactory.decodeStream(`is`) + drawable = BitmapDrawable(getApp().resources, bitmap) + drawable.setBounds( + 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() + ) + `is`?.close() + } catch (e: Exception) { + Log.e("sms", "Failed to loaded content $mContentUri", e) + } + } else { + try { + drawable = ContextCompat.getDrawable(getApp(), mResourceId) + drawable!!.setBounds( + 0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight + ) + } catch (e: Exception) { + Log.e("sms", "Unable to find resource: $mResourceId") + } + } + return drawable + } + } + + internal abstract class CustomDynamicDrawableSpan : ReplacementSpan { + val mVerticalAlignment: Int + + private constructor() { + mVerticalAlignment = ALIGN_BOTTOM + } + + constructor(verticalAlignment: Int) { + mVerticalAlignment = verticalAlignment + } + + abstract val drawable: Drawable? + override fun getSize( + paint: Paint, text: CharSequence, + start: Int, end: Int, fm: FontMetricsInt? + ): Int { + val d = cachedDrawable + val rect = d!!.getBounds() + if (fm != null) { +// LogUtils.d("fm.top: " + fm.top, +// "fm.ascent: " + fm.ascent, +// "fm.descent: " + fm.descent, +// "fm.bottom: " + fm.bottom, +// "lineHeight: " + (fm.bottom - fm.top)); + val lineHeight = fm.bottom - fm.top + if (lineHeight < rect.height()) { + if (mVerticalAlignment == ALIGN_TOP) { + fm.top = fm.top + fm.bottom = rect.height() + fm.top + } else if (mVerticalAlignment == ALIGN_CENTER) { + fm.top = -rect.height() / 2 - lineHeight / 4 + fm.bottom = rect.height() / 2 - lineHeight / 4 + } else { + fm.top = -rect.height() + fm.bottom + fm.bottom = fm.bottom + } + fm.ascent = fm.top + fm.descent = fm.bottom + } + } + return rect.right + } + + override fun draw( + canvas: Canvas, text: CharSequence, + start: Int, end: Int, x: Float, + top: Int, y: Int, bottom: Int, paint: Paint + ) { + val d = cachedDrawable + val rect = d!!.getBounds() + canvas.save() + val transY: Float + val lineHeight = bottom - top + // LogUtils.d("rectHeight: " + rect.height(), +// "lineHeight: " + (bottom - top)); + if (rect.height() < lineHeight) { + transY = if (mVerticalAlignment == ALIGN_TOP) { + top.toFloat() + } else if (mVerticalAlignment == ALIGN_CENTER) { + ((bottom + top - rect.height()) / 2).toFloat() + } else if (mVerticalAlignment == ALIGN_BASELINE) { + (y - rect.height()).toFloat() + } else { + (bottom - rect.height()).toFloat() + } + canvas.translate(x, transY) + } else { + canvas.translate(x, top.toFloat()) + } + d.draw(canvas) + canvas.restore() + } + + private val cachedDrawable: Drawable? + private get() { + val wr = mDrawableRef + var d: Drawable? = null + if (wr != null) { + d = wr.get() + } + if (d == null) { + d = drawable + mDrawableRef = WeakReference(d) + } + return d + } + private var mDrawableRef: WeakReference? = null + + companion object { + const val ALIGN_BOTTOM = 0 + const val ALIGN_BASELINE = 1 + const val ALIGN_CENTER = 2 + const val ALIGN_TOP = 3 + } + } + + internal class ShaderSpan(private val mShader: Shader) : CharacterStyle(), UpdateAppearance { + override fun updateDrawState(tp: TextPaint) { + tp.setShader(mShader) + } + } + + internal class ShadowSpan( + private val radius: Float, + private val dx: Float, + private val dy: Float, + private val shadowColor: Int + ) : CharacterStyle(), UpdateAppearance { + override fun updateDrawState(tp: TextPaint) { + tp.setShadowLayer(radius, dx, dy, shadowColor) + } + } + + private class SerializableSpannableStringBuilder : SpannableStringBuilder(), Serializable { + private val serialVersionUID = 4909567650765875771L + } + + companion object { + private const val COLOR_DEFAULT = -0x1000001 + const val ALIGN_BOTTOM = 0 + const val ALIGN_BASELINE = 1 + const val ALIGN_CENTER = 2 + const val ALIGN_TOP = 3 + private val LINE_SEPARATOR = System.getProperty("line.separator") + fun with(textView: TextView): SpanUtils { + return SpanUtils(textView) + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/TabletUtils.kt b/base/src/main/java/com/example/base/utils/TabletUtils.kt new file mode 100644 index 0000000..6914794 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/TabletUtils.kt @@ -0,0 +1,99 @@ +package com.example.base.utils + +import android.app.Activity +import android.content.res.Configuration +import com.example.base.utils.ScreenUtils +import com.example.base.utils.Utils +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +object TabletUtils { + /** + * 判断是否平板设备,此值不会改变 + */ + val isTabletDevice: Boolean by lazy { + SystemPropertiesProxy["ro.build.characteristics"].contains("tablet") + } + + /** + * 动态判断是否平板窗口 + * 在平板设备上,也可能返回false。如分屏模式下 + * 如想判断物理设备是不是平板,请使用 isTabletDevice + * @return true:平板,false:手机 + * @see isTabletDevice + */ + fun isTabletWindow(): Boolean { + return Utils.getApp().resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= + Configuration.SCREENLAYOUT_SIZE_LARGE + } + + /** + * 判断是否平行窗口模式(华为、小米) + */ + fun inMagicWindow(): Boolean { + val config: String = Utils.getApp().resources.configuration.toString() + return config.contains("hwMultiwindow-magic") || config.contains("miui-magic-windows") || config.contains("hw-magic-windows") + } + + /** + * 判断是否窗口是横屏 + */ + fun isWindowLandscape(): Boolean { + val orientation: Int = Utils.getApp().resources.configuration.orientation + return orientation == Configuration.ORIENTATION_LANDSCAPE + } + + /** + * 判断是否设备是横屏 + */ + fun isDeviceLandscape(): Boolean { + return ScreenUtils.getScreenWidth() > ScreenUtils.getScreenHeight() + } + + /** + * 判断是否在分屏模式 + */ + fun isInMultiWindowMode(activity: Activity): Boolean { + return activity.isInMultiWindowMode + } +} + +object SystemPropertiesProxy { + operator fun get(key: String?): String { + var result = "" + try { + val c = Class.forName("android.os.SystemProperties") + val get: Method = c.getMethod("get", String::class.java) + result = get.invoke(c, key)?.toString() ?: "" + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } catch (e: NoSuchMethodException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } catch (e: InvocationTargetException) { + e.printStackTrace() + } + return result + } + + operator fun set(key: String?, value: String?) { + try { + val c = Class.forName("android.os.SystemProperties") + val set: Method = c.getMethod("set", String::class.java, String::class.java) + set.invoke(c, key, value) + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } catch (e: NoSuchMethodException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } catch (e: InvocationTargetException) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/TimeUtils.kt b/base/src/main/java/com/example/base/utils/TimeUtils.kt new file mode 100644 index 0000000..17c1b19 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/TimeUtils.kt @@ -0,0 +1,36 @@ +package com.example.base.utils + +import android.annotation.SuppressLint +import java.text.SimpleDateFormat +import java.util.Date + +object TimeUtils { + + /** + * 时间戳转换时间 + */ + fun getChangeTime(time: String, sdf: SimpleDateFormat): String? { + return try { + if (time.isNotEmpty()) { + val date = if (time.length == 10) Date(time.toLong() * 1000) else Date(time.toLong()) + sdf.format(date) + } else "" + } catch (e: Exception) { + "" + } + } + + /** + * 将毫秒转化成固定格式的时间 + * 时间格式: yyyy-MM-dd-HHmmssSSS + * + * @param millisecond 时间毫秒 + * @return 时间格式: yyyy-MM-dd-HHmmssSSS + */ + fun getDateTimeFromMillisecond(millisecond: Long?): String? { + @SuppressLint("SimpleDateFormat") val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HHmmssSSS") + val date = Date(millisecond!!) + return simpleDateFormat.format(date) + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/utils/Utils.kt b/base/src/main/java/com/example/base/utils/Utils.kt new file mode 100644 index 0000000..c3f1898 --- /dev/null +++ b/base/src/main/java/com/example/base/utils/Utils.kt @@ -0,0 +1,31 @@ +package com.example.base.utils + +import com.example.base.MvvmApplication + + +/** + * @date 2021-08-15 09:10 + * @desc 在application 初始化 + */ + +object Utils { + + private var instance: MvvmApplication? = null + + + fun init(application: MvvmApplication) { + + instance = application + } + + @JvmStatic + fun getApp(): MvvmApplication { + if (instance == null) { + throw RuntimeException("Utils::Init::Invoke init(context) first!") + } + return instance!! + } + + + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/viewmodel/BaseViewModel.kt b/base/src/main/java/com/example/base/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..3001002 --- /dev/null +++ b/base/src/main/java/com/example/base/viewmodel/BaseViewModel.kt @@ -0,0 +1,123 @@ +package com.example.base.viewmodel + +import android.app.Activity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.base.common.ActivityManager +import com.example.base.livedata.DialogLiveEvent +import com.example.base.livedata.SingleLiveEvent +import com.example.base.utils.L +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +/** + * 文件名: BaseViewModel + * 创建时间: 2021-08-06 on 10:44 + * 描述: ViewModel基类 + */ + +abstract class BaseViewModel : ViewModel() { + + /** + * 用来通知 Activity/Fragment 是否显示等待Dialog + */ + val showDialogLiveEvent: DialogLiveEvent = DialogLiveEvent() + + /** + * 出现异常 用来通知 Activity/Fragment + */ + val exceptionLiveEvent: SingleLiveEvent = SingleLiveEvent() + + /** + * 用来通知 Activity/Fragment finish + */ + val finishActivityLiveEvent: SingleLiveEvent = SingleLiveEvent() + + fun setError(e: Throwable?) { + exceptionLiveEvent.value = e!! + } + + fun setError(e: String?) { + exceptionLiveEvent.value = Exception(e) + } + + fun showDialog(cancelable: Boolean = false) { + showDialogLiveEvent.setValue(true, cancelable) + } + + fun setMessage(msg: String) { + showDialogLiveEvent.setValue(true, msg) + } + + fun dismissDialog() { + showDialogLiveEvent.setValue(false) + } + + fun finishActivity() { + finishActivityLiveEvent.value = this + } + + fun getActivity(): Activity? { + return ActivityManager.lastOrNull() + } + + /** + * @desc 启动协程处理网络请求事件 + * 此方法会自动捕捉异不处理 + */ + fun launchOnUi(tryBlock: suspend CoroutineScope.() -> Unit): Job { + return viewModelScope.launch { + tryCatch(tryBlock, {}, {}) + } + } + + /** + * @desc 启动协程处理网络请求事件 + * 用户自己处理错误 错误回调 + * @see catchBlock + */ + fun launchOnUiTryCatch( + tryBlock: suspend CoroutineScope.() -> Unit, + catchBlock: suspend CoroutineScope.(Throwable) -> Unit + ): Job { + + return viewModelScope.launch { + try { + tryBlock() + } catch (e: Throwable) { + catchBlock(e) + } + } + } + + /** + * 异常处理 + */ + private suspend fun tryCatch( + tryBlock: suspend CoroutineScope.() -> Unit, + catchBlock: suspend CoroutineScope.(Throwable) -> Unit, + finallyBlock: suspend CoroutineScope.() -> Unit + ) { + coroutineScope { + try { + tryBlock() + } catch (e: Throwable) { + catchBlock(e) + } finally { + finallyBlock() + } + } + } + + /** + * ViewModel销毁同时也取消请求 + */ + override fun onCleared() { + super.onCleared() + + L.d("onDestroy: ${this}onCleared") + } + +} diff --git a/base/src/main/java/com/example/base/viewmodel/ListViewModel.kt b/base/src/main/java/com/example/base/viewmodel/ListViewModel.kt new file mode 100644 index 0000000..94f7591 --- /dev/null +++ b/base/src/main/java/com/example/base/viewmodel/ListViewModel.kt @@ -0,0 +1,77 @@ +package com.example.base.viewmodel + +import androidx.collection.ArrayMap +import androidx.collection.arrayMapOf +import com.example.base.livedata.SingleLiveEvent +import kotlinx.coroutines.Job +import retrofit2.http.Body + + +abstract class ListViewModel : BaseViewModel() { + + // 全部数据(recyclerView使用的是当前list) + val list = mutableListOf() + + // 取消上次请示 防止多次请求 + private var job: Job? = null + + // 请求参数 + val params = arrayMapOf() + + // 当前页码 + private var _pageIndex = 1 + val pageIndex get() = _pageIndex + + // 每页条数 + val pageCount = 20 + + + // 每页的数据 + val pageLiveData = SingleLiveEvent>>() + + + /** + * 获取第一页数据 + */ + fun refresh() { + _pageIndex = 1 + request() + } + + /** + * 获取下一页数据 + */ + fun loadMore() { + request() + } + + /** + * @param + * 请求数据 + */ + private fun request() { + + params["page"] = "$pageIndex" + params["size"] = "$pageCount" + + job?.cancel() // 取消上次请求 + job = launchOnUiTryCatch({ + + val result = requestApi(params) + if (result.isSuccess) { + _pageIndex++ + } + + pageLiveData.value = result + + }, { error -> + pageLiveData.value = Result.failure(error) + }) + + } + + /** + * 请求接口返回数据 + */ + abstract suspend fun requestApi(@Body params: ArrayMap): Result> +} diff --git a/base/src/main/java/com/example/base/viewmodel/ViewModelScope.kt b/base/src/main/java/com/example/base/viewmodel/ViewModelScope.kt new file mode 100644 index 0000000..b8c77d8 --- /dev/null +++ b/base/src/main/java/com/example/base/viewmodel/ViewModelScope.kt @@ -0,0 +1,11 @@ +package com.example.base.viewmodel + +/** + * 注解指定viewModel生命周期范围 实现fragment、activity数据共享 + * @see com.example.base.ui.VMFragment + */ +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class ViewModelScope( + val scope: com.example.base.viewmodel.ViewModelScopeType = com.example.base.viewmodel.ViewModelScopeType.DEFAULT +) \ No newline at end of file diff --git a/base/src/main/java/com/example/base/viewmodel/ViewModelScopeType.java b/base/src/main/java/com/example/base/viewmodel/ViewModelScopeType.java new file mode 100644 index 0000000..173dcc4 --- /dev/null +++ b/base/src/main/java/com/example/base/viewmodel/ViewModelScopeType.java @@ -0,0 +1,14 @@ +package com.example.base.viewmodel; + +public enum ViewModelScopeType { + + // 默认生命周期范围(fragment) + DEFAULT, + + // activity数据共享 + ACTIVITY, + + // 全局数据共享 + GLOBAL, + +} diff --git a/base/src/main/java/com/example/base/widget/EmptyView.kt b/base/src/main/java/com/example/base/widget/EmptyView.kt new file mode 100644 index 0000000..7818a4e --- /dev/null +++ b/base/src/main/java/com/example/base/widget/EmptyView.kt @@ -0,0 +1,179 @@ +package com.example.base.widget + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.load +import com.example.base.R +import com.example.base.extensions.gone +import com.example.base.extensions.onClick +import com.example.base.extensions.visible +import org.jetbrains.anko.textColor + +/** + * 加载view 错误view 无数据view + */ + +enum class PageStatus { + ERROR, // 加载错误 + NO_DATA, // 没有数据 + LOADING, // 加载中 + GONG // 不显示(默认) +} + + +class EmptyView : FrameLayout { + + private var status: PageStatus = PageStatus.LOADING + + constructor(context: Context, status: PageStatus) : super(context, null) { + this.status = status + initLayout(context, null) + } + + constructor(context: Context) : super(context, null) { + initLayout(context, null) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initLayout(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + initLayout(context, attrs) + } + + private var onReloadListener: (() -> Unit)? = null + + private val mLayoutParams by lazy { + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + } + + private val mLoadView: View by lazy { + View.inflate( + context, + R.layout.mvvm_emptyview_loading, + null + ) + } + private val mNotDataView: View by lazy { + View.inflate( + context, + R.layout.mvvm_emptyview_nodata, + null + ) + } + private val mErrorView: View by lazy { + View.inflate( + context, + R.layout.mvvm_emptyview_error, + null + ) + } + + + private fun initLayout(context: Context, attrs: AttributeSet?) { + + isFocusable = true + isClickable = true + + val gifImageLoader = ImageLoader.Builder(context) + gifImageLoader.components { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + + mErrorView.findViewById(R.id.ivLogo) + .load(R.drawable.brvah_sample_footer_loading, gifImageLoader.build()) + + addView(mLoadView, mLayoutParams) + addView(mNotDataView, mLayoutParams) + addView(mErrorView, mLayoutParams) + + // 错误点击 重新加载 + mErrorView.setOnClickListener { + onReloadListener?.invoke() + setStatus(PageStatus.LOADING) + } + + // 默认loading 状态 + setStatus(status) + } + + fun setStatus(status: PageStatus) { + this.status = status + this.removeAllViews() + this.visibility = View.VISIBLE + when (status) { + PageStatus.LOADING -> addView(mLoadView, mLayoutParams) + PageStatus.NO_DATA -> addView(mNotDataView, mLayoutParams) + PageStatus.ERROR -> addView(mErrorView, mLayoutParams) + else -> this.visibility = View.GONE + } + + } + + fun getStatus(): PageStatus { + return status + } + + fun setOnReloadListener(listener: (() -> Unit)?) { + onReloadListener = listener + } + + fun setNoDataText(text: String) { + mNotDataView.findViewById(R.id.tvNoData).text = text + } + + fun setNoDataLogo(logo: Int) { + mNotDataView.findViewById(R.id.ivLogo).setBackgroundResource(logo) + } + + fun setBtnVisible(visible: Boolean) { + if (visible) { + mNotDataView.findViewById(R.id.reLoadBtn).visible() + } else { + mNotDataView.findViewById(R.id.reLoadBtn).gone() + } + } + + fun setBtnText(str:String){ + mNotDataView.findViewById(R.id.reLoadBtn).text = str + } + + fun setBtnTextColor(color: Int) { + mNotDataView.findViewById(R.id.reLoadBtn).textColor = color + } + + fun setBtnTextBackground(drawable: Drawable?) { + mNotDataView.findViewById(R.id.reLoadBtn).background = drawable + } + + fun setBtnTextIcon(resId: Int){ + mNotDataView.findViewById(R.id.reLoadBtn).setCompoundDrawablesRelativeWithIntrinsicBounds(resId, 0 ,0,0) + } + + fun noDataBtn(clickFun: () -> Unit) { + mNotDataView.findViewById(R.id.reLoadBtn).onClick { clickFun.invoke() } + } + +} diff --git a/base/src/main/java/com/example/base/widget/LoadingView.kt b/base/src/main/java/com/example/base/widget/LoadingView.kt new file mode 100644 index 0000000..32af54c --- /dev/null +++ b/base/src/main/java/com/example/base/widget/LoadingView.kt @@ -0,0 +1,89 @@ +package com.example.base.widget + + +import android.animation.ArgbEvaluator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import com.example.base.extensions.dp2px +import kotlin.math.abs + +/** + * @date 2021-08-12 13:57 + * @desc 加载中View + */ +class LoadingView : View { + + constructor(context: Context?) : super(context) + + constructor(context: Context?, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context?, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + private var radius: Float = 0.toFloat() + private var radiusOffset: Float = 0.toFloat() + // 不是固定不变的,当width为30dp时,它为2dp,当宽度变大,这个也会相应的变大 + private var stokeWidth = 2f + private val argbEvaluator = ArgbEvaluator() + private val startColor = Color.parseColor("#CCCCCC") + private val endColor = Color.parseColor("#333333") + private var lineCount = 12 // 共12条线 + private var avgAngle = 360f / lineCount + private var time = 0 // 重复次数 + private var centerX: Float = 0.toFloat() + private var centerY: Float = 0.toFloat() // 中心x,y + + private val increaseTask = Runnable { + time++ + invalidate() + } + + init { + stokeWidth = stokeWidth.dp2px() + paint.strokeWidth = stokeWidth + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + radius = (measuredWidth / 2).toFloat() + radiusOffset = radius / 2.5f + + centerX = (measuredWidth / 2).toFloat() + centerY = (measuredHeight / 2).toFloat() + + stokeWidth *= measuredWidth * 1f / 30f.dp2px() + paint.strokeWidth = stokeWidth + + } + + override fun onDraw(canvas: Canvas) { + // 1 2 3 4 5 + // 2 3 4 5 1 + // 3 4 5 1 2 + // ... + for (i in lineCount - 1 downTo 0) { + val temp = abs(i + time) % lineCount + val fraction = (temp + 1) * 1f / lineCount + val color = argbEvaluator.evaluate(fraction, startColor, endColor) as Int + paint.color = color + + val startX = centerX + radiusOffset + val endX = startX + radius / 3f + canvas.drawLine(startX, centerY, endX, centerY, paint) + // 线的两端画个点,看着圆滑 + canvas.drawCircle(startX, centerY, stokeWidth / 2, paint) + canvas.drawCircle(endX, centerY, stokeWidth / 2, paint) + canvas.rotate(avgAngle, centerX, centerY) + } + postDelayed(increaseTask, 80) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + removeCallbacks(increaseTask) + } +} diff --git a/base/src/main/java/com/example/base/widget/MonthView.kt b/base/src/main/java/com/example/base/widget/MonthView.kt new file mode 100644 index 0000000..1b420ab --- /dev/null +++ b/base/src/main/java/com/example/base/widget/MonthView.kt @@ -0,0 +1,124 @@ +package com.example.base.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.R +import org.jetbrains.anko.find +import org.jetbrains.anko.textColor +import java.util.Calendar + +class MonthView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + private var ivPreYear: ImageView? = null + private var ivNextYear: ImageView? = null + + private val monthList by lazy { initMonthList() } + private val adapter by lazy { MonthAdapter() } + + private val calendar by lazy { Calendar.getInstance() } + private val startCalendar by lazy { Calendar.getInstance() } + private val endCalendar by lazy { Calendar.getInstance() } + + private var onMonthSelectedListener: ((year: Int, month: Int) -> Unit)? = null + + init { + initView() + } + + @SuppressLint("NotifyDataSetChanged") + private fun initView() { + val view = View.inflate(context, R.layout.layout_month_view, null) + ivPreYear = view.find(R.id.iv_pre_year) + ivNextYear = view.find(R.id.iv_next_year) + val rvMonth = view.find(R.id.rv_month) + + rvMonth.adapter = adapter + adapter.setOnItemClickListener { _, _, index -> + adapter.data.forEach { + it.isSelected = false + } + adapter.data[index].isSelected = true + calendar.set(Calendar.MONTH, adapter.data[index].index) + onMonthSelectedListener?.invoke(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH)) + adapter.notifyDataSetChanged() + } + + ivPreYear?.setOnClickListener { + if (calendar.get(Calendar.YEAR) > startCalendar.get(Calendar.YEAR)) { + calendar.add(Calendar.YEAR, -1) + onMonthSelectedListener?.invoke(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH)) + } + setNaviBtn() + } + ivNextYear?.setOnClickListener { + if (calendar.get(Calendar.YEAR) < endCalendar.get(Calendar.YEAR)) { + calendar.add(Calendar.YEAR, 1) + onMonthSelectedListener?.invoke(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH)) + } + setNaviBtn() + } + addView(view) + } + + private fun initMonthList(): ArrayList { + val list: ArrayList = ArrayList() + for (i in 0..11) { + list.add(Month(i, calendar.get(Calendar.MONTH) == i)) + } + return list + } + + class MonthAdapter : BaseQuickAdapter(R.layout.item_month_view) { + @SuppressLint("SetTextI18n") + override fun convert(holder: BaseViewHolder, item: Month) { + val tvMonth = holder.getView(R.id.text) + tvMonth.text = "${item.index + 1}月" + tvMonth.textColor = if (item.isSelected) Color.WHITE else Color.BLACK + tvMonth.background = if (item.isSelected) ContextCompat.getDrawable(context, R.drawable.shape_month_view_text_bg) else null + } + + } + + class Month(val index: Int, var isSelected: Boolean = false) + + @SuppressLint("NotifyDataSetChanged") + fun setDate(currentDate: Long = System.currentTimeMillis()) { + calendar.timeInMillis = currentDate + calendar.set(Calendar.DAY_OF_MONTH, 1) + adapter.setList(monthList) + adapter.notifyDataSetChanged() + setNaviBtn() + } + + fun setRange(startDate: Long, endDate: Long) { + startCalendar.timeInMillis = startDate + startCalendar.set(Calendar.DAY_OF_MONTH, 1) + + endCalendar.timeInMillis = endDate + endCalendar.set(Calendar.DAY_OF_MONTH, 1) + setNaviBtn() + } + + private fun setNaviBtn() { + ivNextYear?.visibility = if (calendar.get(Calendar.YEAR) >= endCalendar.get(Calendar.YEAR)) View.INVISIBLE else View.VISIBLE + ivPreYear?.visibility = if (calendar.get(Calendar.YEAR) <= startCalendar.get(Calendar.YEAR)) View.INVISIBLE else View.VISIBLE + } + + fun setOnMonthSelectedListener(listener: ((year: Int, month: Int) -> Unit)?) { + onMonthSelectedListener = listener + } +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/widget/MyLoadMoreView.kt b/base/src/main/java/com/example/base/widget/MyLoadMoreView.kt new file mode 100644 index 0000000..2efc34a --- /dev/null +++ b/base/src/main/java/com/example/base/widget/MyLoadMoreView.kt @@ -0,0 +1,36 @@ +package com.example.base.widget + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.chad.library.adapter.base.loadmore.BaseLoadMoreView +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.example.base.R + +/** + * 加载更多view + */ + +class MyLoadMoreView : BaseLoadMoreView() { + + override fun getLoadComplete(holder: BaseViewHolder): View { + return holder.getView(R.id.tvLoadEnd) + } + + override fun getLoadEndView(holder: BaseViewHolder): View { + return holder.getView(R.id.tvLoadEnd) + } + + override fun getLoadFailView(holder: BaseViewHolder): View { + return holder.getView(R.id.tvLoadFail) + } + + override fun getLoadingView(holder: BaseViewHolder): View { + return holder.getView(R.id.llLoading) + } + + override fun getRootView(parent: ViewGroup): View { + return LayoutInflater.from(parent.context).inflate(R.layout.mvvm_list_loadmore, parent, false) + } + +} \ No newline at end of file diff --git a/base/src/main/java/com/example/base/widget/TitleBar.kt b/base/src/main/java/com/example/base/widget/TitleBar.kt new file mode 100644 index 0000000..e9d8ae4 --- /dev/null +++ b/base/src/main/java/com/example/base/widget/TitleBar.kt @@ -0,0 +1,45 @@ +package com.example.base.widget + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.Gravity +import android.widget.ImageButton +import android.widget.TextView +import androidx.appcompat.widget.Toolbar +import androidx.core.view.children + + +/** + * 自定义标题栏 实现标题居中 + */ +class TitleBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : Toolbar(context, attrs, defStyleAttr) { + + var titleView: TextView + + init { + + // 居中导航键 + val navImageButton = children.find { it is ImageButton } as ImageButton + (navImageButton.layoutParams as LayoutParams).gravity = Gravity.CENTER_VERTICAL + + // 标题居中 + titleView = children.find { it is TextView } as TextView + val layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + layoutParams.gravity = Gravity.CENTER + titleView.layoutParams = layoutParams + + setBackgroundColor(Color.TRANSPARENT) + setTitleTextColor(Color.parseColor("#212226")) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + titleView.x = ((width - titleView.width) / 2).toFloat() + } + +} \ No newline at end of file diff --git a/base/src/main/res/drawable/brvah_sample_footer_loading.png b/base/src/main/res/drawable/brvah_sample_footer_loading.png new file mode 100644 index 0000000..4e7dbf3 Binary files /dev/null and b/base/src/main/res/drawable/brvah_sample_footer_loading.png differ diff --git a/base/src/main/res/drawable/ic_back.xml b/base/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..bab545a --- /dev/null +++ b/base/src/main/res/drawable/ic_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/base/src/main/res/drawable/ic_back_black.xml b/base/src/main/res/drawable/ic_back_black.xml new file mode 100644 index 0000000..b5487b3 --- /dev/null +++ b/base/src/main/res/drawable/ic_back_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/base/src/main/res/drawable/mvvm_ic_close_black.xml b/base/src/main/res/drawable/mvvm_ic_close_black.xml new file mode 100644 index 0000000..6443a3c --- /dev/null +++ b/base/src/main/res/drawable/mvvm_ic_close_black.xml @@ -0,0 +1,5 @@ + + + diff --git a/base/src/main/res/drawable/mvvm_ic_close_white.xml b/base/src/main/res/drawable/mvvm_ic_close_white.xml new file mode 100644 index 0000000..d5ee5f2 --- /dev/null +++ b/base/src/main/res/drawable/mvvm_ic_close_white.xml @@ -0,0 +1,5 @@ + + + diff --git a/base/src/main/res/drawable/mvvm_ic_menu_browser.xml b/base/src/main/res/drawable/mvvm_ic_menu_browser.xml new file mode 100644 index 0000000..1a1fed1 --- /dev/null +++ b/base/src/main/res/drawable/mvvm_ic_menu_browser.xml @@ -0,0 +1,5 @@ + + + diff --git a/base/src/main/res/drawable/mvvm_ic_menu_link.xml b/base/src/main/res/drawable/mvvm_ic_menu_link.xml new file mode 100644 index 0000000..acfd382 --- /dev/null +++ b/base/src/main/res/drawable/mvvm_ic_menu_link.xml @@ -0,0 +1,5 @@ + + + diff --git a/base/src/main/res/drawable/mvvm_ic_menu_refresh.xml b/base/src/main/res/drawable/mvvm_ic_menu_refresh.xml new file mode 100644 index 0000000..c1d5363 --- /dev/null +++ b/base/src/main/res/drawable/mvvm_ic_menu_refresh.xml @@ -0,0 +1,5 @@ + + + diff --git a/base/src/main/res/drawable/mvvm_ic_menu_share.xml b/base/src/main/res/drawable/mvvm_ic_menu_share.xml new file mode 100644 index 0000000..5023d2d --- /dev/null +++ b/base/src/main/res/drawable/mvvm_ic_menu_share.xml @@ -0,0 +1,5 @@ + + + diff --git a/base/src/main/res/drawable/mvvm_shape_white6.xml b/base/src/main/res/drawable/mvvm_shape_white6.xml new file mode 100644 index 0000000..2c8bf6b --- /dev/null +++ b/base/src/main/res/drawable/mvvm_shape_white6.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/base/src/main/res/drawable/mvvm_toolbar_divide.xml b/base/src/main/res/drawable/mvvm_toolbar_divide.xml new file mode 100644 index 0000000..5f3b7f1 --- /dev/null +++ b/base/src/main/res/drawable/mvvm_toolbar_divide.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/base/src/main/res/drawable/progressbar.xml b/base/src/main/res/drawable/progressbar.xml new file mode 100644 index 0000000..4aeb65f --- /dev/null +++ b/base/src/main/res/drawable/progressbar.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/base/src/main/res/drawable/ripple_month_view_navi_bg.xml b/base/src/main/res/drawable/ripple_month_view_navi_bg.xml new file mode 100644 index 0000000..348e088 --- /dev/null +++ b/base/src/main/res/drawable/ripple_month_view_navi_bg.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/drawable/shape_2090fe_cor25.xml b/base/src/main/res/drawable/shape_2090fe_cor25.xml new file mode 100644 index 0000000..4c1db59 --- /dev/null +++ b/base/src/main/res/drawable/shape_2090fe_cor25.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/base/src/main/res/drawable/shape_466afd_line_cor25.xml b/base/src/main/res/drawable/shape_466afd_line_cor25.xml new file mode 100644 index 0000000..406c677 --- /dev/null +++ b/base/src/main/res/drawable/shape_466afd_line_cor25.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/base/src/main/res/drawable/shape_month_view_text_bg.xml b/base/src/main/res/drawable/shape_month_view_text_bg.xml new file mode 100644 index 0000000..85b4f09 --- /dev/null +++ b/base/src/main/res/drawable/shape_month_view_text_bg.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/base/src/main/res/drawable/shape_white_cor8.xml b/base/src/main/res/drawable/shape_white_cor8.xml new file mode 100644 index 0000000..88dc3e2 --- /dev/null +++ b/base/src/main/res/drawable/shape_white_cor8.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/base/src/main/res/layout/item_month_view.xml b/base/src/main/res/layout/item_month_view.xml new file mode 100644 index 0000000..e4d4230 --- /dev/null +++ b/base/src/main/res/layout/item_month_view.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/base/src/main/res/layout/layout_month_view.xml b/base/src/main/res/layout/layout_month_view.xml new file mode 100644 index 0000000..b445b0b --- /dev/null +++ b/base/src/main/res/layout/layout_month_view.xml @@ -0,0 +1,44 @@ + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/layout/mvvm_activity_base.xml b/base/src/main/res/layout/mvvm_activity_base.xml new file mode 100644 index 0000000..ec75df4 --- /dev/null +++ b/base/src/main/res/layout/mvvm_activity_base.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/base/src/main/res/layout/mvvm_activity_browser.xml b/base/src/main/res/layout/mvvm_activity_browser.xml new file mode 100644 index 0000000..6cb5359 --- /dev/null +++ b/base/src/main/res/layout/mvvm_activity_browser.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_dialog_date.xml b/base/src/main/res/layout/mvvm_dialog_date.xml new file mode 100644 index 0000000..0a63688 --- /dev/null +++ b/base/src/main/res/layout/mvvm_dialog_date.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/layout/mvvm_dialog_date_time.xml b/base/src/main/res/layout/mvvm_dialog_date_time.xml new file mode 100644 index 0000000..842362e --- /dev/null +++ b/base/src/main/res/layout/mvvm_dialog_date_time.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/layout/mvvm_dialog_loading.xml b/base/src/main/res/layout/mvvm_dialog_loading.xml new file mode 100644 index 0000000..1245557 --- /dev/null +++ b/base/src/main/res/layout/mvvm_dialog_loading.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_dialog_select_photo.xml b/base/src/main/res/layout/mvvm_dialog_select_photo.xml new file mode 100644 index 0000000..6f83745 --- /dev/null +++ b/base/src/main/res/layout/mvvm_dialog_select_photo.xml @@ -0,0 +1,36 @@ + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/layout/mvvm_dialog_time.xml b/base/src/main/res/layout/mvvm_dialog_time.xml new file mode 100644 index 0000000..0d87a08 --- /dev/null +++ b/base/src/main/res/layout/mvvm_dialog_time.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/layout/mvvm_emptyview_error.xml b/base/src/main/res/layout/mvvm_emptyview_error.xml new file mode 100644 index 0000000..beaffce --- /dev/null +++ b/base/src/main/res/layout/mvvm_emptyview_error.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_emptyview_loading.xml b/base/src/main/res/layout/mvvm_emptyview_loading.xml new file mode 100644 index 0000000..13dd44e --- /dev/null +++ b/base/src/main/res/layout/mvvm_emptyview_loading.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_emptyview_nodata.xml b/base/src/main/res/layout/mvvm_emptyview_nodata.xml new file mode 100644 index 0000000..4fc0d59 --- /dev/null +++ b/base/src/main/res/layout/mvvm_emptyview_nodata.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_layout_loading.xml b/base/src/main/res/layout/mvvm_layout_loading.xml new file mode 100644 index 0000000..39d3210 --- /dev/null +++ b/base/src/main/res/layout/mvvm_layout_loading.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_list.xml b/base/src/main/res/layout/mvvm_list.xml new file mode 100644 index 0000000..a8c26b2 --- /dev/null +++ b/base/src/main/res/layout/mvvm_list.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_list_loadmore.xml b/base/src/main/res/layout/mvvm_list_loadmore.xml new file mode 100644 index 0000000..c7a4753 --- /dev/null +++ b/base/src/main/res/layout/mvvm_list_loadmore.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/base/src/main/res/layout/mvvm_list_title_bar.xml b/base/src/main/res/layout/mvvm_list_title_bar.xml new file mode 100644 index 0000000..67fcbdd --- /dev/null +++ b/base/src/main/res/layout/mvvm_list_title_bar.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/menu/mvvm_menu_browser.xml b/base/src/main/res/menu/mvvm_menu_browser.xml new file mode 100644 index 0000000..c177730 --- /dev/null +++ b/base/src/main/res/menu/mvvm_menu_browser.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/base/src/main/res/mipmap-xxhdpi/ic_add.png b/base/src/main/res/mipmap-xxhdpi/ic_add.png new file mode 100644 index 0000000..f1d5a7f Binary files /dev/null and b/base/src/main/res/mipmap-xxhdpi/ic_add.png differ diff --git a/base/src/main/res/mipmap-xxhdpi/ic_arrow_next.png b/base/src/main/res/mipmap-xxhdpi/ic_arrow_next.png new file mode 100644 index 0000000..78821eb Binary files /dev/null and b/base/src/main/res/mipmap-xxhdpi/ic_arrow_next.png differ diff --git a/base/src/main/res/mipmap-xxhdpi/ic_arrow_pre.png b/base/src/main/res/mipmap-xxhdpi/ic_arrow_pre.png new file mode 100644 index 0000000..843986b Binary files /dev/null and b/base/src/main/res/mipmap-xxhdpi/ic_arrow_pre.png differ diff --git a/base/src/main/res/mipmap-xxhdpi/ic_loading.png b/base/src/main/res/mipmap-xxhdpi/ic_loading.png new file mode 100644 index 0000000..564112c Binary files /dev/null and b/base/src/main/res/mipmap-xxhdpi/ic_loading.png differ diff --git a/base/src/main/res/mipmap-xxhdpi/no_empty.png b/base/src/main/res/mipmap-xxhdpi/no_empty.png new file mode 100644 index 0000000..e4a8a0f Binary files /dev/null and b/base/src/main/res/mipmap-xxhdpi/no_empty.png differ diff --git a/base/src/main/res/mipmap-xxhdpi/no_wifi.png b/base/src/main/res/mipmap-xxhdpi/no_wifi.png new file mode 100644 index 0000000..fed39e9 Binary files /dev/null and b/base/src/main/res/mipmap-xxhdpi/no_wifi.png differ diff --git a/base/src/main/res/values-v28/styles.xml b/base/src/main/res/values-v28/styles.xml new file mode 100644 index 0000000..a0512bd --- /dev/null +++ b/base/src/main/res/values-v28/styles.xml @@ -0,0 +1,15 @@ + + + + + + \ 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..a3b9a90 --- /dev/null +++ b/base/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #F4F6FA + \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml new file mode 100644 index 0000000..8466fd1 --- /dev/null +++ b/base/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + + Basic + + 加载失败,点击屏幕重试~ + 正在加载… + 暂无数据 + 拍照 + 相册 + 到底了 ~ + \ 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..2b863b9 --- /dev/null +++ b/base/src/main/res/values/styles.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + diff --git a/base/src/main/res/xml/network_security_config.xml b/base/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..97f86ac --- /dev/null +++ b/base/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + 127.0.0.1 + + + lkme.cc + + \ No newline at end of file diff --git a/base/src/main/res/xml/provider_paths.xml b/base/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..c725e03 --- /dev/null +++ b/base/src/main/res/xml/provider_paths.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4ce5a86 --- /dev/null +++ b/build.gradle @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.jetbrainsKotlinAndroid) apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8454ff0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,32 @@ +# 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 -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-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 +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.enableJetifier=true +#android.defaults.buildfeatures.buildconfig = true +org.gradle.caching = true +android.injected.testOnly=false + +RELEASE_KEY_PASSWORD=sQYG1Jee +RELEASE_KEY_ALIAS=__uni__7e100bb +RELEASE_STORE_PASSWORD=sQYG1Jee +RELEASE_STORE_FILE=bidinfo.keystore \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..4944527 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,26 @@ +[versions] +agp = "8.5.0" +androidxConstraintlayout = "2.1.4" +appcompat = "1.6.1" +googleMaterial = "1.10.0" +kotlin = "1.9.0" +coreKtx = "1.10.1" +junit = "4.13.2" +junitVersion = "1.1.5" +espressoCore = "3.5.1" +lifecycleRuntimeKtx = "2.6.1" + +[libraries] +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-constraintlayout-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidxConstraintlayout" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +com-google-android-material-material = { module = "com.google.android.material:material", version.ref = "googleMaterial" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c 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..e902fa8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Dec 11 10:45:37 CST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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..434d9e9 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,36 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + maven { url 'https://developer.huawei.com/repo/' } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + maven { url 'https://mvn.getui.com/nexus/content/repositories/releases' } + maven { url 'https://developer.huawei.com/repo/' }//添加华为仓库 获取oaid + maven { url 'https://developer.hihonor.com/repo/'} + maven { url 'https://artifact.bytedance.com/repository/Volcengine/' } //巨量融合 + maven { url "https://jitpack.io" } + mavenCentral() + google() + } +} + +rootProject.name = "blzb" +include ':app' +include ':base' \ No newline at end of file