|
|
@ -0,0 +1,15 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
rabbit
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
|
||||||
|
<option name="CKGUserStarteByUser" value="true" />
|
||||||
|
<option name="chatAppRouterInfo" value="builder/698af709d073d0c19b43196d" />
|
||||||
|
<option name="ckgOperationStatus" value="CONFIRM" />
|
||||||
|
<option name="lastCKGNotifyTime" value="1770886315206" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
|
kotlin("plugin.serialization") version "2.3.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.img.rabbit"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.4.8"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.img.rabbit"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 2
|
||||||
|
versionName = "1.1"
|
||||||
|
|
||||||
|
|
||||||
|
setManifestPlaceholders(mapOf(
|
||||||
|
"GETUI_APPID" to (project.findProperty("GETUI_APPID") as? String ?: ""),
|
||||||
|
"GT_INSTALL_CHANNEL" to (project.findProperty("GT_INSTALL_CHANNEL") as? String ?: "GT_INSTALL_CHANNEL")
|
||||||
|
))
|
||||||
|
ndk {
|
||||||
|
abiFilters.addAll(listOf("arm64-v8a", "x86_64"))
|
||||||
|
}
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
vectorDrawables {
|
||||||
|
useSupportLibrary = true
|
||||||
|
}
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
resConfigs("en", "zh-rCN")
|
||||||
|
|
||||||
|
flavorDimensions.addAll(listOf("channel"))
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
create("general") {
|
||||||
|
dimension = "channel"
|
||||||
|
manifestPlaceholders["UMENG_CHANNEL"] = "general"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productFlavors.configureEach {
|
||||||
|
dimension = "channel"
|
||||||
|
manifestPlaceholders.putAll(mapOf("UMENG_CHANNEL" to name))
|
||||||
|
}
|
||||||
|
manifestPlaceholders.putAll(mapOf(
|
||||||
|
"GETUI_APPID" to "40qbPjPkYs7TnVAYCX0Ig6",
|
||||||
|
"GT_INSTALL_CHANNEL" to "general",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 配置签名信息
|
||||||
|
signingConfigs {
|
||||||
|
create("config") {
|
||||||
|
storeFile = file(project.findProperty("RELEASE_STORE_FILE") ?: "bidinfo.keystore")
|
||||||
|
storePassword = project.findProperty("RELEASE_STORE_PASSWORD") as? String ?: ""
|
||||||
|
keyAlias = project.findProperty("RELEASE_KEY_ALIAS") as? String ?: ""
|
||||||
|
keyPassword = project.findProperty("RELEASE_KEY_PASSWORD") as? String ?: ""
|
||||||
|
enableV1Signing = true
|
||||||
|
enableV2Signing = true
|
||||||
|
enableV3Signing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
signingConfig = signingConfigs.getByName("config")
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
signingConfig = signingConfigs.getByName("config")
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// 基础依赖
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.androidx.material)
|
||||||
|
implementation(libs.androidx.activity)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.core.splashscreen)
|
||||||
|
|
||||||
|
// Compose 依赖
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
implementation(libs.androidx.compose.ui)
|
||||||
|
implementation(libs.androidx.compose.ui.tooling)
|
||||||
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.compose.activity.compose)
|
||||||
|
implementation(libs.androidx.compose.foundation)
|
||||||
|
implementation(libs.androidx.navigation.runtime.ktx)
|
||||||
|
|
||||||
|
// ViewModel 和 Lifecycle 依赖
|
||||||
|
implementation(libs.androidx.compose.runtime.livedata)
|
||||||
|
implementation(libs.androidx.compose.lifecycle.viewmodel)
|
||||||
|
implementation(libs.androidx.compose.lifecycle.runtime.compose)
|
||||||
|
implementation(libs.androidx.compose.runtime.livedata)
|
||||||
|
|
||||||
|
// Kotlinx Serialization 依赖
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.androidx.datastore.core)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.androidx.foundation)
|
||||||
|
|
||||||
|
// 测试依赖
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||||
|
androidTestImplementation(libs.androidx.test.espresso.core)
|
||||||
|
|
||||||
|
//个推一键认证
|
||||||
|
implementation(libs.gysdk)
|
||||||
|
implementation(libs.gson)
|
||||||
|
implementation(libs.mmkv)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.network.okhttp)
|
||||||
|
implementation(libs.segmentation.selfie)
|
||||||
|
implementation(libs.face.detection)
|
||||||
|
implementation(libs.android.gif.drawable)
|
||||||
|
implementation(libs.gif.encoder)
|
||||||
|
implementation("com.caverock:androidsvg:1.4")
|
||||||
|
implementation("io.github.lucksiege:pictureselector:v3.11.2")
|
||||||
|
// 压缩库 (可选,建议长图拼接前先压缩防止OOM)
|
||||||
|
implementation("io.github.lucksiege:compress:v3.11.2")
|
||||||
|
implementation("io.github.leavesczy:matisse:2.3.0")
|
||||||
|
implementation("com.github.moyuruaizawa:cropify:0.5.2")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# 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.umeng.**{*;}
|
||||||
|
|
||||||
|
-keepclassmembers class*{
|
||||||
|
public <init> (org.json.JSONObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers enum*{
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
# 友盟号码认证 结束
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "APK",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "com.img.rabbit",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"attributes": [],
|
||||||
|
"versionCode": 2,
|
||||||
|
"versionName": "1.1",
|
||||||
|
"outputFile": "app-release.apk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementType": "File",
|
||||||
|
"baselineProfiles": [
|
||||||
|
{
|
||||||
|
"minApi": 28,
|
||||||
|
"maxApi": 30,
|
||||||
|
"baselineProfiles": [
|
||||||
|
"baselineProfiles/1/app-release.dm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minApi": 31,
|
||||||
|
"maxApi": 2147483647,
|
||||||
|
"baselineProfiles": [
|
||||||
|
"baselineProfiles/0/app-release.dm"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minSdkVersionForDexing": 24
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.img.rabbit
|
||||||
|
|
||||||
|
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.img.rabbit", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- 读取相册权限-Android 12及以下版本的权限 -->
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
<!--相机权限,用于打开相机和拍照:如用户反馈等-->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32"
|
||||||
|
tools:ignore="SelectedPhotoAccess" />
|
||||||
|
<uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32"
|
||||||
|
tools:ignore="SelectedPhotoAccess" />
|
||||||
|
<!-- 读取相册图片权限-Android 13及以上版本的权限 -->
|
||||||
|
<uses-permission android:name = "android.permission.READ_MEDIA_IMAGES"
|
||||||
|
tools:ignore="PhotoAndVideoPolicy,SelectedPhotoAccess" />
|
||||||
|
<!--检测联网方式,在网络异常状态避免数据发送,节省流量和电量-->
|
||||||
|
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<!--查看WIFI网络状态-->
|
||||||
|
<uses-permission android:name = "android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<!--网络访问,允许SDK联网和发送数据的最基础权限-->
|
||||||
|
<uses-permission android:name = "android.permission.INTERNET" />
|
||||||
|
<!--切换网络通道-->
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||||
|
<!--开关wifi状态,解决国内机型移动网络权限问题需要-->
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".BaseApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher_logo"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_logo"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.rabbit"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
tools:replace="android:allowBackup,android:supportsRtl"
|
||||||
|
tools:targetApi="33">
|
||||||
|
<activity
|
||||||
|
android:name="com.img.rabbit.MainActivity"
|
||||||
|
android:theme="@style/SplashTheme"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/filepath_data" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.img.rabbit
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import com.img.rabbit.utils.NetworkMonitor
|
||||||
|
import com.g.gysdk.GYManager
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
|
||||||
|
|
||||||
|
class BaseApplication : Application() {
|
||||||
|
private val TAG = "BaseApplication"
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
// 初始化网络状态监控
|
||||||
|
NetworkMonitor.initialize(this)
|
||||||
|
// 初始化MMKV
|
||||||
|
initMMKV()
|
||||||
|
// 初始化个推SDK
|
||||||
|
initGeTuiOneKeyLogin()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initGeTuiOneKeyLogin() {
|
||||||
|
GYManager.getInstance().preInit(this.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initMMKV() {
|
||||||
|
val rootDir = MMKV.initialize(this)
|
||||||
|
Log.i(TAG, "MMKV root dir: $rootDir")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package com.img.rabbit
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.img.rabbit.pages.LoginScreen
|
||||||
|
import com.img.rabbit.pages.MainScreen
|
||||||
|
import com.img.rabbit.viewmodel.GeneralViewModel
|
||||||
|
import com.img.rabbit.viewmodel.LoginViewModel
|
||||||
|
import com.img.rabbit.viewmodel.SplashViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// 必须在 super.onCreate 之前调用
|
||||||
|
val splashScreen = installSplashScreen()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// 启用Edge-to-Edge模式(沉浸模式)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val splashViewModel: SplashViewModel = viewModel()
|
||||||
|
val generalViewModel: GeneralViewModel = viewModel()
|
||||||
|
val loginViewModel: LoginViewModel = viewModel()
|
||||||
|
|
||||||
|
// 设置启动页显示条件
|
||||||
|
splashScreen.setKeepOnScreenCondition {
|
||||||
|
splashViewModel.isLoading.value // 当为 true 时,启动页不消失
|
||||||
|
}
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
SplashScreenContent {
|
||||||
|
val token = generalViewModel.kv.decodeString("token")
|
||||||
|
// 未登录,显示登录页
|
||||||
|
if (token?.isNotEmpty() == false && !loginViewModel.isLogin.value) {
|
||||||
|
// 显示登录页
|
||||||
|
LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel)
|
||||||
|
} else {
|
||||||
|
//已登录,显示主页面
|
||||||
|
MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟加载过程,2秒后关闭启动页
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(100L)
|
||||||
|
splashViewModel.setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppTheme(content: @Composable () -> Unit) {
|
||||||
|
// 使用Material3主题
|
||||||
|
androidx.compose.material3.MaterialTheme {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplashScreenContent(
|
||||||
|
onAnimationFinished: @Composable () -> Unit // 改为Composable函数类型
|
||||||
|
) {
|
||||||
|
val scale = remember { Animatable(0f) }
|
||||||
|
var animationFinished by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
scale.animateTo(
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = 800)
|
||||||
|
)
|
||||||
|
animationFinished = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (!animationFinished) {
|
||||||
|
// 显示启动页动画
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_splash_mask),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.scale(scale.value)
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_splash_logo),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.wrapContentSize()
|
||||||
|
.scale(scale.value)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 动画完成后显示主界面
|
||||||
|
onAnimationFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun MainScreenPreview() {
|
||||||
|
AppTheme {
|
||||||
|
MainScreen(generalViewModel = viewModel(), loginViewModel = viewModel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.img.rabbit.bean
|
||||||
|
|
||||||
|
data class ClothingBean(
|
||||||
|
//衣服索引(区分男女)
|
||||||
|
val index: Int,
|
||||||
|
//衣服id(不区分男女类别)
|
||||||
|
val id: Int,
|
||||||
|
val icon: Int,
|
||||||
|
val clothing: Int? =null,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.img.rabbit.bean
|
||||||
|
|
||||||
|
data class FormatBean(
|
||||||
|
//格式id
|
||||||
|
val id: Int,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.img.rabbit.bean
|
||||||
|
|
||||||
|
data class HairstyleBean(
|
||||||
|
//发型索引(区分男女)
|
||||||
|
val index: Int,
|
||||||
|
//发型id(不区分男女类别)
|
||||||
|
val id: Int,
|
||||||
|
val icon: Int,
|
||||||
|
val hairstyle: Int? = null,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.img.rabbit.bean
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class LongImageBean(
|
||||||
|
val uri: Uri,
|
||||||
|
val bitmap: Bitmap,
|
||||||
|
var cropTop: Float = 0f, // 裁剪起始点(像素)
|
||||||
|
var displayHeight: Float = bitmap.height.toFloat() // 显示高度(像素)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.img.rabbit.bean
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预登录数据模型
|
||||||
|
* {"process_id":"982ec252c9bd82d06886fafc8a01b068","operatorType":"2","clienttype":"1","accessCode":"2ed00faebc5ceb5c55d5623e52672ae6e806d2ba3ef0a2018960165154037812","number":"186****0253","expiredTime":1770196189245,"errorCode":0,"errorDesc":"gysdk success!","costTime":3}
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class OnekeyPreLogin(
|
||||||
|
val process_id: String,
|
||||||
|
val operatorType: String,
|
||||||
|
val clienttype: String,
|
||||||
|
val accessCode: String,
|
||||||
|
val number: String,
|
||||||
|
val expiredTime: Long,
|
||||||
|
val errorCode: Int,
|
||||||
|
val errorDesc: String,
|
||||||
|
val costTime: Int
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.img.rabbit.bean
|
||||||
|
|
||||||
|
data class ResizeBean(
|
||||||
|
//尺寸id
|
||||||
|
val id: Int,
|
||||||
|
val size: String,
|
||||||
|
val title: String,
|
||||||
|
val width: Float,
|
||||||
|
val height: Float
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.img.rabbit.bean
|
||||||
|
|
||||||
|
data class UserInfo(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val login: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,443 @@
|
||||||
|
package com.img.rabbit.components
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
// 首先添加ImageUtils的导入
|
||||||
|
import android.graphics.Bitmap.CompressFormat
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.utils.ImageUtils
|
||||||
|
import kotlin.apply
|
||||||
|
import kotlin.collections.all
|
||||||
|
import kotlin.collections.minus
|
||||||
|
import kotlin.collections.plus
|
||||||
|
import kotlin.collections.toMutableList
|
||||||
|
import kotlin.let
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ImagePicker(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
imageHeight: Dp = 100.dp,
|
||||||
|
aspectRatio: Float = 100f / 100f,
|
||||||
|
maxCount: Int = 3,
|
||||||
|
addButtonName: String = "添加图片",
|
||||||
|
currentImageUris: List<Uri> = emptyList(),
|
||||||
|
currentImagePaths: List<String> = emptyList(), // 新增参数
|
||||||
|
onImagesUpdated: (uris: List<Uri>, paths: List<String>) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
// 添加临时文件管理
|
||||||
|
val tempImageUri = remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
// 记录当前操作场景
|
||||||
|
var currentScene by remember { mutableStateOf<PickerScene?>(null) }
|
||||||
|
// 新增选择对话框
|
||||||
|
var showChoiceDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 新增大图预览状态
|
||||||
|
var previewImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
// 图片路径转Uri的辅助函数
|
||||||
|
fun getDisplayImages(): List<Uri> {
|
||||||
|
return currentImageUris.ifEmpty {
|
||||||
|
currentImagePaths.mapNotNull { path ->
|
||||||
|
try {
|
||||||
|
Uri.fromFile(File(path))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
if (currentImages.size < maxCount) {
|
||||||
|
onImagesUpdated(currentImages + uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 相册选择逻辑
|
||||||
|
val galleryLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
if (currentImageUris.size < maxCount) {
|
||||||
|
// 保存图片到存储并获取路径
|
||||||
|
val savedPath = ImageUtils.saveUriToStorage(
|
||||||
|
context = context,
|
||||||
|
uri = it,
|
||||||
|
format = CompressFormat.JPEG,
|
||||||
|
quality = 90
|
||||||
|
)
|
||||||
|
// 更新图片列表和路径列表
|
||||||
|
onImagesUpdated(
|
||||||
|
currentImageUris + it,
|
||||||
|
if (savedPath != null) currentImagePaths + savedPath else currentImagePaths
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拍照逻辑
|
||||||
|
val cameraLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.TakePicture()
|
||||||
|
) { success ->
|
||||||
|
if (success) {
|
||||||
|
tempImageUri.value?.let { uri ->
|
||||||
|
if (currentImageUris.size < maxCount) {
|
||||||
|
// 保存图片到存储并获取路径
|
||||||
|
val savedPath = ImageUtils.saveUriToStorage(
|
||||||
|
context = context,
|
||||||
|
uri = uri,
|
||||||
|
format = CompressFormat.JPEG,
|
||||||
|
quality = 90
|
||||||
|
)
|
||||||
|
// 更新图片列表和路径列表
|
||||||
|
onImagesUpdated(
|
||||||
|
currentImageUris + uri,
|
||||||
|
if (savedPath != null) currentImagePaths + savedPath else currentImagePaths
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
// 修改权限回调逻辑
|
||||||
|
if (permissions.all { it.value }) {
|
||||||
|
when (currentScene) {
|
||||||
|
PickerScene.GALLERY -> galleryLauncher.launch("image/*")
|
||||||
|
PickerScene.CAMERA -> {
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
File.createTempFile("IMG_", ".jpg", context.externalCacheDir)
|
||||||
|
)
|
||||||
|
tempImageUri.value = uri
|
||||||
|
cameraLauncher.launch(uri)
|
||||||
|
}
|
||||||
|
null -> { /* 无操作 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (showChoiceDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showChoiceDialog = false },
|
||||||
|
title = { Text(text = "选择图片来源", fontSize = 14.sp) },
|
||||||
|
text = { Text(text = "请选择拍照或从相册选取, 若无权限点击相册或者拍照后会弹出权限请求", fontSize = 12.sp) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showChoiceDialog = false
|
||||||
|
|
||||||
|
currentScene = PickerScene.CAMERA
|
||||||
|
permissionLauncher.launch(
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { Text(text = "拍照", fontSize = 14.sp) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showChoiceDialog = false
|
||||||
|
|
||||||
|
currentScene = PickerScene.GALLERY
|
||||||
|
permissionLauncher.launch(
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { Text(text = "相册", fontSize = 14.sp) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏大图对话框
|
||||||
|
previewImageUri?.let { uri ->
|
||||||
|
FullScreenDialog(
|
||||||
|
onDismiss = { previewImageUri = null }
|
||||||
|
) {
|
||||||
|
ZoomableImage(
|
||||||
|
uri = uri,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(modifier = modifier.defaultMinSize(minHeight = (imageHeight + 8.dp))) {
|
||||||
|
items(getDisplayImages()) { uri ->
|
||||||
|
Box(
|
||||||
|
Modifier.padding(end = 6.dp, top = 6.dp).background(color = Color(0x80E5E5E7), shape = RoundedCornerShape(8.dp))
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = uri,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(imageHeight)
|
||||||
|
.aspectRatio(aspectRatio)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
previewImageUri = uri
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val size = if(imageHeight < 150.dp){
|
||||||
|
24.dp
|
||||||
|
}else{
|
||||||
|
32.dp
|
||||||
|
}
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_picture_del),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size)
|
||||||
|
.aspectRatio(1f) // 设置宽高比为1:1
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(top = 4.dp, end = 4.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
){
|
||||||
|
// 找到当前uri在列表中的索引,同时从paths列表中移除对应的路径
|
||||||
|
val uriIndex = currentImageUris.indexOf(uri)
|
||||||
|
val newPaths = if (uriIndex >= 0 && uriIndex < currentImagePaths.size) {
|
||||||
|
currentImagePaths.toMutableList().apply { removeAt(uriIndex) }
|
||||||
|
} else {
|
||||||
|
// 如果uri不在currentImageUris中,可能是从currentImagePaths转换过来的
|
||||||
|
// 尝试通过路径匹配找到对应的索引
|
||||||
|
val pathIndex = currentImagePaths.indexOf(uri.path)
|
||||||
|
if (pathIndex >= 0) {
|
||||||
|
currentImagePaths.toMutableList().apply { removeAt(pathIndex) }
|
||||||
|
} else {
|
||||||
|
currentImagePaths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onImagesUpdated(currentImageUris - uri, newPaths)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
/*
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
// 找到当前uri在列表中的索引,同时从paths列表中移除对应的路径
|
||||||
|
val uriIndex = currentImageUris.indexOf(uri)
|
||||||
|
val newPaths = if (uriIndex >= 0 && uriIndex < currentImagePaths.size) {
|
||||||
|
currentImagePaths.toMutableList().apply { removeAt(uriIndex) }
|
||||||
|
} else {
|
||||||
|
// 如果uri不在currentImageUris中,可能是从currentImagePaths转换过来的
|
||||||
|
// 尝试通过路径匹配找到对应的索引
|
||||||
|
val pathIndex = currentImagePaths.indexOf(uri.path)
|
||||||
|
if (pathIndex >= 0) {
|
||||||
|
currentImagePaths.toMutableList().apply { removeAt(pathIndex) }
|
||||||
|
} else {
|
||||||
|
currentImagePaths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onImagesUpdated(currentImageUris - uri, newPaths)
|
||||||
|
},
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Close, "删除")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
if (currentImageUris.size < maxCount && currentImagePaths.size < maxCount) {
|
||||||
|
// 在触发按钮中添加
|
||||||
|
if(maxCount==1){
|
||||||
|
AddSingleButton(
|
||||||
|
imageHeight = imageHeight,
|
||||||
|
aspectRatio = aspectRatio,
|
||||||
|
onClick = { showChoiceDialog = true },
|
||||||
|
label = addButtonName,
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
Box(
|
||||||
|
Modifier.defaultMinSize(minHeight = (imageHeight + 8.dp)),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
AddButton(
|
||||||
|
onClick = { showChoiceDialog = true },
|
||||||
|
label = addButtonName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
label: String = "添加图片",
|
||||||
|
imageHeight: Dp = 100.dp,
|
||||||
|
aspectRatio: Float = 100f / 100f
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(imageHeight)
|
||||||
|
.aspectRatio(aspectRatio)
|
||||||
|
.border(1.dp, Color(0xFFA5A5A5), RoundedCornerShape(8.dp))
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = label)
|
||||||
|
Text(text = label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddSingleButton(
|
||||||
|
imageHeight: Dp = 100.dp,
|
||||||
|
aspectRatio: Float = 100f / 100f,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
label: String = "添加图片"
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(imageHeight + 8.dp)
|
||||||
|
.aspectRatio(aspectRatio)
|
||||||
|
.border(1.dp, Color(0xFFA5A5A5), RoundedCornerShape(8.dp))
|
||||||
|
) {
|
||||||
|
if(label == "上传头像"){
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||||
|
Text(text = label)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = label)
|
||||||
|
Text(text = label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FullScreenDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
decorFitsSystemWindows = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.95f))
|
||||||
|
.clickable(onClick = onDismiss)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope", "AutoboxingStateValueProperty")
|
||||||
|
@Composable
|
||||||
|
private fun ZoomableImage(uri: Uri, modifier: Modifier = Modifier) {
|
||||||
|
val scale = remember { mutableFloatStateOf(1f) }
|
||||||
|
val offset = remember { mutableStateOf(Offset.Zero) }
|
||||||
|
|
||||||
|
BoxWithConstraints(modifier = modifier.pointerInput(Unit) {
|
||||||
|
detectTransformGestures { _, pan, zoom, _ ->
|
||||||
|
scale.floatValue *= zoom
|
||||||
|
offset.value += pan
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
AsyncImage(
|
||||||
|
model = uri,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale.value
|
||||||
|
scaleY = scale.value
|
||||||
|
translationX = offset.value.x
|
||||||
|
translationY = offset.value.y
|
||||||
|
}
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加场景枚举
|
||||||
|
enum class PickerScene { GALLERY, CAMERA }
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.img.rabbit.config
|
||||||
|
|
||||||
|
object Common {
|
||||||
|
const val privacyUrl = "https://www.baidu.com"
|
||||||
|
const val agreementUrl = "https://www.baidu.com"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package com.img.rabbit.config
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.bean.ClothingBean
|
||||||
|
import com.img.rabbit.bean.FormatBean
|
||||||
|
import com.img.rabbit.bean.HairstyleBean
|
||||||
|
import com.img.rabbit.bean.ResizeBean
|
||||||
|
|
||||||
|
object CommonData {
|
||||||
|
//背景颜色
|
||||||
|
val colors = listOf(
|
||||||
|
Color.Transparent, // 第一个:带边框的透明
|
||||||
|
Color.Red, // 第二个:红色
|
||||||
|
Color(0xFFC62828), // 第三个:深红色
|
||||||
|
Color(0xFFB71C1C), // 第四个:暗红色
|
||||||
|
Color(0xFF2196F3), // 第五个:蓝色
|
||||||
|
Color(0xFF1565C0), // 第六个:深蓝色
|
||||||
|
Color(0xFF0277BD), // 第七个:亮蓝色
|
||||||
|
Color.Transparent // 最后一个:带边框的透明
|
||||||
|
)
|
||||||
|
// 女装
|
||||||
|
val clothingForFemales = mutableListOf<ClothingBean>().apply {
|
||||||
|
add(ClothingBean(index = 0, id = 0, icon = R.mipmap.ic_cutout_color_less, clothing = null,title = "无"))
|
||||||
|
add(ClothingBean(index = 1, id = 1, icon = R.mipmap.ic_clothing_female_1, clothing = R.mipmap.ic_clothing_female_1, title = "青春"))
|
||||||
|
add(ClothingBean(index = 2, id = 2, icon = R.mipmap.ic_clothing_female_2, clothing = R.mipmap.ic_clothing_female_2, title = "学生"))
|
||||||
|
add(ClothingBean(index = 3, id = 3, icon = R.mipmap.ic_clothing_female_3, clothing = R.mipmap.ic_clothing_female_3, title = "职场"))
|
||||||
|
add(ClothingBean(index = 4, id = 4, icon = R.mipmap.ic_clothing_female_4, clothing = R.mipmap.ic_clothing_female_4, title = "休闲"))
|
||||||
|
add(ClothingBean(index = 5, id = 5, icon = R.mipmap.ic_clothing_female_5, clothing = R.mipmap.ic_clothing_female_5, title = "日韩"))
|
||||||
|
add(ClothingBean(index = 6, id = 6, icon = R.mipmap.ic_clothing_female_6, clothing = R.mipmap.ic_clothing_female_6, title = "艺术"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 男装
|
||||||
|
val clothingForMans = mutableListOf<ClothingBean>().apply {
|
||||||
|
add(ClothingBean(index = 0, id = 7, icon = R.mipmap.ic_cutout_color_less, clothing = null, title = "无"))
|
||||||
|
add(ClothingBean(index = 1, id = 8, icon = R.mipmap.ic_clothing_man_2, clothing = R.mipmap.ic_clothing_man_2, title = "青春"))
|
||||||
|
add(ClothingBean(index = 2, id = 9, icon = R.mipmap.ic_clothing_man_3, clothing = R.mipmap.ic_clothing_man_3, title = "学生"))
|
||||||
|
add(ClothingBean(index = 3, id = 10, icon = R.mipmap.ic_clothing_man_4, clothing = R.mipmap.ic_clothing_man_4, title = "职场"))
|
||||||
|
add(ClothingBean(index = 4, id = 11, icon = R.mipmap.ic_clothing_man_5, clothing = R.mipmap.ic_clothing_man_5, title = "休闲"))
|
||||||
|
add(ClothingBean(index = 5, id = 12, icon = R.mipmap.ic_clothing_man_6, clothing = R.mipmap.ic_clothing_man_6, title = "日韩"))
|
||||||
|
add(ClothingBean(index = 6, id = 13, icon = R.mipmap.ic_clothing_man_7, clothing = R.mipmap.ic_clothing_man_7, title = "艺术"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 定义发型列表-女生
|
||||||
|
val hairstyleForFemales = mutableListOf<HairstyleBean>().apply {
|
||||||
|
add(HairstyleBean(index = 0, id = 0, icon = R.mipmap.ic_cutout_color_less, hairstyle = null, title = "无"))
|
||||||
|
add(HairstyleBean(index = 1, id = 1, icon = R.mipmap.ic_hairstyle_female_1, hairstyle = R.mipmap.ic_hairstyle_female_1, title = "青春"))
|
||||||
|
add(HairstyleBean(index = 2, id = 2, icon = R.mipmap.ic_hairstyle_female_2, hairstyle = R.mipmap.ic_hairstyle_female_2, title = "学生"))
|
||||||
|
add(HairstyleBean(index = 3, id = 3, icon = R.mipmap.ic_hairstyle_female_3, hairstyle = R.mipmap.ic_hairstyle_female_3, title = "职场"))
|
||||||
|
add(HairstyleBean(index = 4, id = 4, icon = R.mipmap.ic_hairstyle_female_4, hairstyle = R.mipmap.ic_hairstyle_female_4, title = "休闲"))
|
||||||
|
add(HairstyleBean(index = 5, id = 5, icon = R.mipmap.ic_hairstyle_female_5, hairstyle = R.mipmap.ic_hairstyle_female_5, title = "日韩"))
|
||||||
|
add(HairstyleBean(index = 6, id = 6, icon = R.mipmap.ic_hairstyle_female_6, hairstyle = R.mipmap.ic_hairstyle_female_6, title = "艺术"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发型(男)
|
||||||
|
// 定义发型列表-男生
|
||||||
|
val hairstyleForMans = mutableListOf<HairstyleBean>().apply {
|
||||||
|
add(HairstyleBean(index = 0, id = 7, icon = R.mipmap.ic_cutout_color_less, hairstyle = null, title = "无"))
|
||||||
|
add(HairstyleBean(index = 1, id = 8, icon = R.mipmap.ic_hairstyle_man_1, hairstyle = R.mipmap.ic_hairstyle_man_1, title = "青春"))
|
||||||
|
add(HairstyleBean(index = 2, id = 9, icon = R.mipmap.ic_hairstyle_man_2, hairstyle = R.mipmap.ic_hairstyle_man_2, title = "学生"))
|
||||||
|
add(HairstyleBean(index = 3, id = 10, icon = R.mipmap.ic_hairstyle_man_3, hairstyle = R.mipmap.ic_hairstyle_man_3, title = "职场"))
|
||||||
|
add(HairstyleBean(index = 4, id = 11, icon = R.mipmap.ic_hairstyle_man_4, hairstyle = R.mipmap.ic_hairstyle_man_4, title = "休闲"))
|
||||||
|
add(HairstyleBean(index = 5, id = 12, icon = R.mipmap.ic_hairstyle_man_5, hairstyle = R.mipmap.ic_hairstyle_man_5, title = "日韩"))
|
||||||
|
add(HairstyleBean(index = 6, id = 13, icon = R.mipmap.ic_hairstyle_man_6, hairstyle = R.mipmap.ic_hairstyle_man_6, title = "艺术"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义尺寸列表
|
||||||
|
val resizes = mutableListOf<ResizeBean>().apply {
|
||||||
|
add(ResizeBean(id = 0, size = "23x35mm", title = "标准一寸", width = 25f, height = 35f))
|
||||||
|
add(ResizeBean(id = 1, size = "22x23mm", title = "小一寸", width = 22f, height = 32f))
|
||||||
|
add(ResizeBean(id = 2, size = "35x53mm", title = "标准二寸", width = 35f, height = 53f))
|
||||||
|
add(ResizeBean(id = 3, size = "33x48mm", title = "小二寸", width = 33f, height = 48f))
|
||||||
|
add(ResizeBean(id = 4, size = "26x32mm", title = "身份证", width = 26f, height = 32f))
|
||||||
|
add(ResizeBean(id = 5, size = "33x48mm", title = "护照", width = 33f, height = 48f))
|
||||||
|
add(ResizeBean(id = 6, size = "22x32mm", title = "驾驶证", width = 22f, height = 32f))
|
||||||
|
add(ResizeBean(id = 7, size = "26x32mm", title = "社保卡", width = 26f, height = 32f))
|
||||||
|
add(ResizeBean(id = 8, size = "26x32mm", title = "四六级", width = 26f, height = 32f))
|
||||||
|
add(ResizeBean(id = 9, size = "33x48mm", title = "司法考试", width = 33f, height = 48f))
|
||||||
|
add(ResizeBean(id = 10, size = "40x60mm", title = "结婚证", width = 40f, height = 60f))
|
||||||
|
add(ResizeBean(id = 11, size = "35x45mm", title = "中国签证", width = 35f, height = 45f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义尺寸列表(与您提供的示例完全匹配)
|
||||||
|
val formats = mutableListOf<FormatBean>().apply {
|
||||||
|
add(FormatBean(id = 0, title = "JPG"))
|
||||||
|
add(FormatBean(id = 1, title = "PNG"))
|
||||||
|
add(FormatBean(id = 2, title = "GIF"))
|
||||||
|
add(FormatBean(id = 3, title = "SVG"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
package com.img.rabbit.pages
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.pages.screen.mine.FeedbackScreen
|
||||||
|
import com.img.rabbit.pages.screen.mine.OnlineServiceScreen
|
||||||
|
import com.img.rabbit.pages.screen.mine.SettingScreen
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.pages.screen.HomeScreen
|
||||||
|
import com.img.rabbit.pages.screen.MineScreen
|
||||||
|
import com.img.rabbit.pages.screen.make.CutoutScreen
|
||||||
|
import com.img.rabbit.pages.screen.make.FormatScreen
|
||||||
|
import com.img.rabbit.pages.screen.make.LongImageScreen
|
||||||
|
import com.img.rabbit.pages.screen.make.ResizeScreen
|
||||||
|
import com.img.rabbit.pages.screen.mine.setting.AboutScreen
|
||||||
|
import com.img.rabbit.pages.screen.mine.setting.AccountBindScreen
|
||||||
|
import com.img.rabbit.pages.screen.mine.setting.AccountManagerScreen
|
||||||
|
import com.img.rabbit.pages.screen.other.CameraGuideScreen
|
||||||
|
import com.img.rabbit.route.ScreenRoute
|
||||||
|
import com.img.rabbit.viewmodel.GeneralViewModel
|
||||||
|
import com.img.rabbit.viewmodel.LoginViewModel
|
||||||
|
|
||||||
|
// 定义底部导航的标签页
|
||||||
|
sealed class TabItem(val title: String, val normalIconRes: Int, val selectedIconRes: Int, val normalColor: Color, val selectedColor: Color) {
|
||||||
|
object Home : TabItem("首页", R.mipmap.ic_home_normal, R.mipmap.ic_home_selected, Color(0xFFAAAAAA), Color(0xFF1A1A1A))
|
||||||
|
object Mine : TabItem("我的", R.mipmap.ic_mine_normal, R.mipmap.ic_mine_selected, Color(0xFFAAAAAA), Color(0xFF1A1A1A))
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true)
|
||||||
|
val isNavigationBarVisible by generalViewModel.isNavigationBarVisible.observeAsState(initial = true)
|
||||||
|
val tabItems = listOf(
|
||||||
|
TabItem.Home,
|
||||||
|
TabItem.Mine
|
||||||
|
)
|
||||||
|
var selectedTab: TabItem by remember { mutableStateOf(TabItem.Home) }
|
||||||
|
|
||||||
|
// 监听返回事件
|
||||||
|
val currentBackStackEntry = navController.currentBackStackEntry
|
||||||
|
|
||||||
|
LaunchedEffect(currentBackStackEntry) {
|
||||||
|
// 当返回到MineScreen页面时执行的操作
|
||||||
|
if (currentBackStackEntry?.destination?.route == "home") {
|
||||||
|
// 显示TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络状态监听
|
||||||
|
LaunchedEffect(networkStatus) {
|
||||||
|
if (!networkStatus) {
|
||||||
|
// 网络断开时的处理
|
||||||
|
Log.w("NetworkStatus","网络断开")
|
||||||
|
}else{
|
||||||
|
Log.w("NetworkStatus","网络已连接")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = {
|
||||||
|
if (isNavigationBarVisible) {
|
||||||
|
Box {
|
||||||
|
NavigationBar(
|
||||||
|
containerColor = Color.White,
|
||||||
|
contentColor = Color.Transparent
|
||||||
|
) {
|
||||||
|
tabItems.forEachIndexed { index, item ->
|
||||||
|
val iconRes = if (selectedTab == tabItems[index]) {
|
||||||
|
item.selectedIconRes
|
||||||
|
} else {
|
||||||
|
item.normalIconRes
|
||||||
|
}
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = iconRes),
|
||||||
|
contentDescription = item.title,
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(item.title, color = if (selectedTab == tabItems[index]) item.selectedColor else item.normalColor) },
|
||||||
|
selected = selectedTab == tabItems[index],
|
||||||
|
onClick = { selectedTab = tabItems[index] },
|
||||||
|
colors = NavigationBarItemDefaults.colors(
|
||||||
|
indicatorColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 顶部横线
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(Color.Gray.copy(alpha = 0.3f))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 导航主机
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = ScreenRoute.Home.route
|
||||||
|
) {
|
||||||
|
// Tab页面
|
||||||
|
composable(ScreenRoute.Home.route) {
|
||||||
|
HomeScreen(
|
||||||
|
navController = navController,
|
||||||
|
generalViewModel = generalViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(ScreenRoute.Mine.route) {
|
||||||
|
MineScreen(
|
||||||
|
navController = navController,
|
||||||
|
generalViewModel = generalViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 抠图页面(Cutout)
|
||||||
|
composable(ScreenRoute.Cutout.route) {
|
||||||
|
CutoutScreen(navController = navController)
|
||||||
|
}
|
||||||
|
// 证件页面(Certificate)
|
||||||
|
composable(ScreenRoute.Resize.route) {
|
||||||
|
ResizeScreen(navController = navController)
|
||||||
|
}
|
||||||
|
// 格式页面(Format)
|
||||||
|
composable(ScreenRoute.Format.route) {
|
||||||
|
FormatScreen(navController = navController)
|
||||||
|
}
|
||||||
|
// 拍照指南页面(CameraGuide)
|
||||||
|
composable(ScreenRoute.CameraGuide.route) {
|
||||||
|
CameraGuideScreen(navController = navController)
|
||||||
|
}
|
||||||
|
// 长图页面(LongImage)
|
||||||
|
composable(ScreenRoute.LongImage.route) {
|
||||||
|
LongImageScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 我的页面(Mine)
|
||||||
|
composable(ScreenRoute.Feedback.route) {
|
||||||
|
FeedbackScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable(ScreenRoute.OnlineService.route) {
|
||||||
|
OnlineServiceScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable(ScreenRoute.Setting.route) {
|
||||||
|
SettingScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置页面(Setting)
|
||||||
|
composable(ScreenRoute.BindAccount.route) {
|
||||||
|
AccountBindScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable(ScreenRoute.ManagerAccount.route) {
|
||||||
|
AccountManagerScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable(ScreenRoute.AboutMine.route) {
|
||||||
|
AboutScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录页面(Login)
|
||||||
|
composable(ScreenRoute.Login.route) {
|
||||||
|
LoginScreen(
|
||||||
|
navController = navController,
|
||||||
|
generalViewModel = generalViewModel,
|
||||||
|
loginViewModel = loginViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据选中的Tab切换导航路由
|
||||||
|
LaunchedEffect(selectedTab) {
|
||||||
|
when (selectedTab) {
|
||||||
|
TabItem.Home -> navController.navigate(ScreenRoute.Home.route) {
|
||||||
|
popUpTo(ScreenRoute.Home.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
TabItem.Mine -> navController.navigate(ScreenRoute.Mine.route) {
|
||||||
|
popUpTo(ScreenRoute.Mine.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
package com.img.rabbit.pages
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.utils.NetworkStatus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NetworkDisconnectedPage(onNetworkStatus: (Boolean) -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scale = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().background(color = Color.White)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_normal_top_mask),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 返回按钮
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_back),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 56.dp, start = 16.dp)
|
||||||
|
.wrapContentSize()
|
||||||
|
.clickable {
|
||||||
|
// 点击返回按钮,返回上一页
|
||||||
|
(context as MainActivity).onBackPressed()
|
||||||
|
//navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_network_disconnected),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(top = 253.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "网络连接断开,请检查网络设置",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(top = 23.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "刷新页面",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(top = 57.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 点击刷新按钮,刷新页面
|
||||||
|
onNetworkStatus(NetworkStatus.isNetworkAvailable(context))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(horizontal = 71.dp)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(top = 11.dp)
|
||||||
|
.background(color = Color(0xFF252525), shape = RoundedCornerShape(359.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 点击开启网络权限按钮,跳转至系统设置页面
|
||||||
|
NetworkStatus.openNetworkSettings(context)
|
||||||
|
}
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "开启网络权限",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFFC2FF43),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(top = 12.dp, bottom = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewNetworkDisconnectedPage() {
|
||||||
|
NetworkDisconnectedPage(onNetworkStatus = {})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,883 @@
|
||||||
|
package com.img.rabbit.pages.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.route.ScreenRoute
|
||||||
|
import com.img.rabbit.viewmodel.GeneralViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(navController: NavHostController,generalViewModel: GeneralViewModel) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
// 监听返回事件
|
||||||
|
val currentBackStackEntry = navController.currentBackStackEntry
|
||||||
|
|
||||||
|
LaunchedEffect(currentBackStackEntry) {
|
||||||
|
// 当返回到MineScreen页面时执行的操作
|
||||||
|
if (currentBackStackEntry?.destination?.route == "home") {
|
||||||
|
// 显示TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_top_mask),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Column (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.offset(y = (-17).dp)
|
||||||
|
.padding(bottom = 56.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.White,
|
||||||
|
shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||||
|
)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
){
|
||||||
|
//选尺寸制作
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_title_1_size),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 25f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 35f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_size_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "标准一寸",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "25x35mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(10.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 22f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 32f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_size_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "小一寸",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "22x32mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 35f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 53f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_size_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "标准二寸",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "35x53mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(10.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 33f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 48f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_size_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "小二寸",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "33x48mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//证据制作
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_title_2_certificate),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 18.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 26f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 32f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "身份证",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "26x32mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 33f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 48f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "护照",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "33x48mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 22f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 32f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "驾驶证",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "22x32mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 26f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 32f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "社保卡",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "26x32mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 26f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 32f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "四六级",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "26x32mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 33f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 48f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "司法考试",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "33x48mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 40f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 60f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "结婚证",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "40x60mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转抠图页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 35f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 45f)
|
||||||
|
navigate(ScreenRoute.Cutout.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_certificate_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "中国签证",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "35x45mm",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//其他
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_title_3_other),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转格式页面
|
||||||
|
navController.navigate(ScreenRoute.LongImage.route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_other_1_puzzle),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "拼长图",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize().padding(top = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转格式页面
|
||||||
|
navController.navigate(ScreenRoute.Format.route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_other_2_format),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "格式转换",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize().padding(top = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转证件页面
|
||||||
|
navController.apply {
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("width", 25f)
|
||||||
|
currentBackStackEntry?.savedStateHandle?.set("height", 35f)
|
||||||
|
navigate(ScreenRoute.Resize.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_other_3_size),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "改尺寸",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize().padding(top = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转拍照指南页面
|
||||||
|
navController.navigate(ScreenRoute.CameraGuide.route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_home_other_4_camera),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "拍照指南",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize().padding(top = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewHomeScreen() {
|
||||||
|
HomeScreen(navController = rememberNavController(),generalViewModel = viewModel())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,437 @@
|
||||||
|
package com.img.rabbit.pages.screen
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.viewmodel.GeneralViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MineScreen(
|
||||||
|
navController: NavHostController,
|
||||||
|
generalViewModel: GeneralViewModel,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val vipMember by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 监听返回事件
|
||||||
|
val currentBackStackEntry = navController.currentBackStackEntry
|
||||||
|
|
||||||
|
LaunchedEffect(currentBackStackEntry) {
|
||||||
|
// 当返回到MineScreen页面时执行的操作
|
||||||
|
if (currentBackStackEntry?.destination?.route == "mine") {
|
||||||
|
// 显示TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().background(Color(0xFFF9F9F9))
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_top_mask),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 112.dp)
|
||||||
|
) {
|
||||||
|
// 头像和登录/注册
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_user_avatar_default),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(RoundedCornerShape(90.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 处理点击事件
|
||||||
|
Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转登录页面
|
||||||
|
navController.navigate("login")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "登录/注册",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "登录体验更多功能哦~",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIP bar
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(top = 33.dp)
|
||||||
|
){
|
||||||
|
|
||||||
|
if(vipMember){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_vip_bar_bg1),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_vip_bar_bg),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (vipMember) "月度会员" else "开通会员",
|
||||||
|
style = TextStyle(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(Color(0xFFC8FF54), Color(0xFFFFFFFF))
|
||||||
|
),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 14.dp, top = 10.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (vipMember) "2025.12.22前会员到期" else "解锁微圈相册全部特权",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFFFFFFF),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 14.dp, top = 10.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vipMember){
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(end = 17.dp)
|
||||||
|
.offset(y = (-40).dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_open),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.End)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 处理点击事件
|
||||||
|
Toast.makeText(context, "开通会员", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//功能项
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
RoundedCornerShape(18.dp)
|
||||||
|
)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
ambientColor = Color(0x4DE5E5E5),
|
||||||
|
spotColor = Color(0x4DE5E5E5)
|
||||||
|
)
|
||||||
|
){
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转反馈页面
|
||||||
|
navController.navigate("feedback")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_feedback),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 12.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "意见反馈",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp) // 改为end padding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转在线客服页面
|
||||||
|
navController.navigate("onlineService")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_service),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 12.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "在线客服",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp) // 改为end padding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 处理点击事件
|
||||||
|
Toast.makeText(context, "版本更新", Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_update),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 12.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "版本更新",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp) // 改为end padding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 隐藏TabBar
|
||||||
|
generalViewModel.setNavigationBarVisible(false)
|
||||||
|
// 跳转设置页面
|
||||||
|
navController.navigate("setting")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_mine_setting),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 12.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "设置",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp) // 改为end padding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMineScreen(){
|
||||||
|
MineScreen(navController = rememberNavController(), generalViewModel = viewModel())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,621 @@
|
||||||
|
package com.img.rabbit.pages.screen.make
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.gestures.forEachGesture
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.center
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.bean.ClothingBean
|
||||||
|
import com.img.rabbit.bean.HairstyleBean
|
||||||
|
import com.img.rabbit.components.AppearanceType
|
||||||
|
import com.img.rabbit.components.DrawingBoardPicker
|
||||||
|
import com.img.rabbit.config.CommonData.clothingForFemales
|
||||||
|
import com.img.rabbit.config.CommonData.clothingForMans
|
||||||
|
import com.img.rabbit.config.CommonData.colors
|
||||||
|
import com.img.rabbit.config.CommonData.hairstyleForFemales
|
||||||
|
import com.img.rabbit.config.CommonData.hairstyleForMans
|
||||||
|
import com.img.rabbit.config.CommonData.resizes
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
import com.img.rabbit.utils.ExportFormat
|
||||||
|
import com.img.rabbit.utils.ImageUtils
|
||||||
|
import com.img.rabbit.utils.ImageUtils.saveCanvasToGallery
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import com.img.rabbit.utils.PhotoCutter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
// 建议的状态数据结构
|
||||||
|
data class TransformState(
|
||||||
|
val offset: Offset = Offset.Zero,
|
||||||
|
val scale: Float = 1f,
|
||||||
|
val rotation: Float = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
// 服装和发型资源管理类
|
||||||
|
object ResourceManager {
|
||||||
|
// 加载服装资源
|
||||||
|
fun loadClothingResource(context: Context, resId: Int): Bitmap? {
|
||||||
|
return try {
|
||||||
|
BitmapFactory.decodeResource(context.resources, resId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ResourceManager", "加载服装资源失败: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载发型资源
|
||||||
|
fun loadHairstyleResource(context: Context, resId: Int): Bitmap? {
|
||||||
|
return try {
|
||||||
|
BitmapFactory.decodeResource(context.resources, resId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ResourceManager", "加载发型资源失败: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CutoutScreen(navController: NavController) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
// 图片显示区域 - 支持头像编辑(用于最终数据的导出保存Bitmap)
|
||||||
|
val graphicsLayer = rememberGraphicsLayer()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
// 抠图加载中状态
|
||||||
|
val isLoading = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val widthParam = navController.previousBackStackEntry?.savedStateHandle?.get<Float>("width") ?: 23f
|
||||||
|
val heightParam = navController.previousBackStackEntry?.savedStateHandle?.get<Float>("height") ?: 35f
|
||||||
|
|
||||||
|
//显示服装和发型
|
||||||
|
val viewClothing = remember { mutableStateOf(ViewType.VISIBLE) }
|
||||||
|
val viewHairstyle = remember { mutableStateOf(ViewType.VISIBLE) }
|
||||||
|
|
||||||
|
// 人物图片选择和抠图相关状态
|
||||||
|
val selectedImageUri = remember { mutableStateOf<Uri?>(null) }
|
||||||
|
val cutoutResultBitmap = remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
|
// 头像编辑状态
|
||||||
|
val headTransform = remember { mutableStateOf(TransformState()) }
|
||||||
|
val clothesTransform = remember { mutableStateOf(TransformState()) }
|
||||||
|
val hairTransform = remember { mutableStateOf(TransformState()) }
|
||||||
|
|
||||||
|
var selectedTarget by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
// 辅助函数:判断点击位置
|
||||||
|
fun hitTest(tap: Offset, state: TransformState, bmp: Bitmap?, canvasCenter: Offset): Boolean {
|
||||||
|
if (bmp == null) return false
|
||||||
|
val w = bmp.width * state.scale
|
||||||
|
val h = bmp.height * state.scale
|
||||||
|
val left = canvasCenter.x + state.offset.x - w / 2
|
||||||
|
val top = canvasCenter.y + state.offset.y - h / 2
|
||||||
|
return tap.x in left..(left + w) && tap.y in top..(top + h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服装和发型资源
|
||||||
|
val selectedClothingBitmap = remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
val selectedHairstyleBitmap = remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
|
// 当前选中的外观
|
||||||
|
val selectedAppearance = remember { mutableStateOf(AppearanceType.BACKGROUND) }
|
||||||
|
// 当前选中的颜色索引
|
||||||
|
val selectedColorIndex = remember { mutableIntStateOf(0) }
|
||||||
|
// 当前选中的衣服
|
||||||
|
val selectedClothing = remember { mutableStateOf(clothingForFemales.getOrNull(0)) }
|
||||||
|
// 当前选中的发型
|
||||||
|
val selectedHairstyle = remember { mutableStateOf(hairstyleForFemales.getOrNull(0)) }
|
||||||
|
// 当前选中的尺寸
|
||||||
|
val selectedSize = remember { mutableStateOf(resizes.first { it.width == widthParam && it.height == heightParam }) }
|
||||||
|
|
||||||
|
|
||||||
|
// 图片选择启动器
|
||||||
|
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
selectedImageUri.value = it
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
// 执行抠图操作
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val originalBitmap = ImageUtils.getBitmapFromUri(context, it)
|
||||||
|
originalBitmap?.let { bitmap ->
|
||||||
|
PhotoCutter.cutPureHead(bitmap) { croppedBitmap ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
cutoutResultBitmap.value = croppedBitmap
|
||||||
|
isLoading.value = false
|
||||||
|
|
||||||
|
// 重置头像变换
|
||||||
|
headTransform.value = TransformState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("CutoutScreen", "抠图失败: ${e.message}")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
isLoading.value = false
|
||||||
|
Toast.makeText(context, "抠图失败,请重试", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载服装资源
|
||||||
|
fun loadClothingResource(clothing: ClothingBean?) {
|
||||||
|
if(clothing?.clothing == null){
|
||||||
|
selectedClothingBitmap.value = null
|
||||||
|
}else{
|
||||||
|
Thread {
|
||||||
|
val bitmap = ResourceManager.loadClothingResource(context, clothing.clothing)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
selectedClothingBitmap.value = bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载发型资源
|
||||||
|
fun loadHairstyleResource(hairstyle: HairstyleBean?) {
|
||||||
|
if(hairstyle?.hairstyle == null){
|
||||||
|
selectedHairstyleBitmap.value = null
|
||||||
|
}else{
|
||||||
|
Thread {
|
||||||
|
val bitmap = ResourceManager.loadHairstyleResource(context, hairstyle.hairstyle)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
selectedHairstyleBitmap.value = bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
imagePickerLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4))
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "", showSave = true){
|
||||||
|
// 保存图片
|
||||||
|
coroutineScope.launch {
|
||||||
|
// 从 Layer 捕获 Bitmap
|
||||||
|
val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap()
|
||||||
|
// 保存图片到系统相册(图片已按比例裁剪)
|
||||||
|
saveCanvasToGallery(context, bitmap, ExportFormat.JPG){fileName, isSuccess ->
|
||||||
|
if(isSuccess){
|
||||||
|
Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show()
|
||||||
|
}else{
|
||||||
|
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 保存图片到系统相册(指定尺寸,如果targetWidth与targetHeight比原始值小太多会导致图片模糊)
|
||||||
|
saveCanvasToGallery(context, bitmap, ExportFormat.JPG, selectedSize.value.width.toInt(), selectedSize.value.height.toInt()){fileName, isSuccess ->
|
||||||
|
if(isSuccess){
|
||||||
|
Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show()
|
||||||
|
}else{
|
||||||
|
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 图片显示区域 - 支持头像编辑
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 38.dp, end = 38.dp
|
||||||
|
)
|
||||||
|
.aspectRatio(selectedSize.value.width/selectedSize.value.height)
|
||||||
|
.background(Color(0xFFFFFFFF))
|
||||||
|
.border(1.dp, Color(0xFFD8D8D8))
|
||||||
|
) {
|
||||||
|
if (isLoading.value) {
|
||||||
|
// 加载中状态
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_loading),
|
||||||
|
contentDescription = "加载效果图片",
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "正在抠图中...",
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (cutoutResultBitmap.value != null) {
|
||||||
|
// 显示合成结果
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.drawWithContent {
|
||||||
|
// 将内容绘制到 graphicsLayer 中
|
||||||
|
graphicsLayer.record {
|
||||||
|
this@drawWithContent.drawContent()
|
||||||
|
}
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
.clipToBounds()
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
forEachGesture {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
val down = awaitFirstDown()
|
||||||
|
val center = size.toSize().center
|
||||||
|
|
||||||
|
// 1. 修改判定逻辑:只有当服装可见时,才进行它的 hitTest
|
||||||
|
selectedTarget = when {
|
||||||
|
// 处理发型
|
||||||
|
viewHairstyle.value == ViewType.VISIBLE && hitTest(down.position, hairTransform.value, selectedHairstyleBitmap.value, center) -> "HAIR"
|
||||||
|
|
||||||
|
// 处理服装
|
||||||
|
viewClothing.value == ViewType.VISIBLE && hitTest(down.position, clothesTransform.value, selectedClothingBitmap.value, center) -> "CLOTHES"
|
||||||
|
|
||||||
|
hitTest(down.position, headTransform.value, cutoutResultBitmap.value, center) -> "HEAD"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTransformGestures { _, pan, zoom, rotation ->
|
||||||
|
selectedTarget?.let { target ->
|
||||||
|
// 更新逻辑保持不变...
|
||||||
|
val stateRef = when(target) {
|
||||||
|
"HEAD" -> headTransform
|
||||||
|
"CLOTHES" -> clothesTransform
|
||||||
|
else -> hairTransform
|
||||||
|
}
|
||||||
|
val bmp = when(target) {
|
||||||
|
"HEAD" -> cutoutResultBitmap.value
|
||||||
|
"CLOTHES" -> selectedClothingBitmap.value
|
||||||
|
else -> selectedHairstyleBitmap.value
|
||||||
|
}
|
||||||
|
|
||||||
|
bmp?.let { b ->
|
||||||
|
val current = stateRef.value
|
||||||
|
val newScale = (current.scale * zoom).coerceIn(0.1f, 5f)
|
||||||
|
stateRef.value = current.copy(
|
||||||
|
offset = current.offset + pan,
|
||||||
|
scale = newScale,
|
||||||
|
rotation = if (target == "HEAD") current.rotation + rotation else current.rotation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val canvasCenter = center
|
||||||
|
|
||||||
|
// 1. 绘制背景
|
||||||
|
drawRect(colors[selectedColorIndex.intValue])
|
||||||
|
|
||||||
|
// 2. 绘制头像
|
||||||
|
cutoutResultBitmap.value?.let { bmp ->
|
||||||
|
withTransform({
|
||||||
|
translate(headTransform.value.offset.x, headTransform.value.offset.y)
|
||||||
|
rotate(headTransform.value.rotation, canvasCenter)
|
||||||
|
scale(headTransform.value.scale, headTransform.value.scale, canvasCenter)
|
||||||
|
}) {
|
||||||
|
drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 绘制服装 (根据状态[viewClothing]显示)
|
||||||
|
if (viewClothing.value == ViewType.VISIBLE) {
|
||||||
|
selectedClothingBitmap.value?.let { bmp ->
|
||||||
|
withTransform({
|
||||||
|
translate(clothesTransform.value.offset.x, clothesTransform.value.offset.y)
|
||||||
|
scale(clothesTransform.value.scale, clothesTransform.value.scale, canvasCenter)
|
||||||
|
}) {
|
||||||
|
drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 绘制发型 (根据状态[viewHairstyle]显示)
|
||||||
|
if (viewHairstyle.value == ViewType.VISIBLE) {
|
||||||
|
selectedHairstyleBitmap.value?.let { bmp ->
|
||||||
|
withTransform({
|
||||||
|
translate(hairTransform.value.offset.x, hairTransform.value.offset.y)
|
||||||
|
scale(hairTransform.value.scale, hairTransform.value.scale, canvasCenter)
|
||||||
|
}) {
|
||||||
|
drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (selectedImageUri.value != null) {
|
||||||
|
// 显示选中的原始图片
|
||||||
|
AsyncImage(
|
||||||
|
model = selectedImageUri.value,
|
||||||
|
contentDescription = "选中的图片",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Fit
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 空状态
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_image_empty_pld),
|
||||||
|
contentDescription = "截图",
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "点击选择图片",
|
||||||
|
color = Color(0xFFD8D8D8),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部控制区域
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (selectedAppearance.value == AppearanceType.CLOTHING || selectedAppearance.value == AppearanceType.HAIRSTYLE) {
|
||||||
|
// 服装和发型查看
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 16.dp)
|
||||||
|
) {
|
||||||
|
ViewOption(
|
||||||
|
viewType = viewClothing.value,
|
||||||
|
title = "服装",
|
||||||
|
onClick = {
|
||||||
|
viewClothing.value = if (viewClothing.value == ViewType.HIDE) ViewType.VISIBLE else ViewType.HIDE
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Box(modifier = Modifier.wrapContentSize().padding(end = 8.dp))
|
||||||
|
ViewOption(
|
||||||
|
viewType = viewHairstyle.value,
|
||||||
|
title = "发型",
|
||||||
|
onClick = {
|
||||||
|
viewHairstyle.value = if (viewHairstyle.value == ViewType.HIDE) ViewType.VISIBLE else ViewType.HIDE
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 16.dp)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 说明
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.wrapContentSize().padding(end = 16.dp).align(Alignment.Bottom)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_info),
|
||||||
|
contentDescription = "说明",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(14.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xFFF4F4F4))
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "使用说明",
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.align(Alignment.Bottom).padding(start = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部画板
|
||||||
|
DrawingBoardPicker(
|
||||||
|
// 颜色
|
||||||
|
colors = colors,
|
||||||
|
// 服装
|
||||||
|
clothingForMans = clothingForMans,
|
||||||
|
clothingForFemales = clothingForFemales,
|
||||||
|
// 发型
|
||||||
|
hairstyleForMans = hairstyleForMans,
|
||||||
|
hairstyleForFemales = hairstyleForFemales,
|
||||||
|
// 尺寸
|
||||||
|
sizes = resizes,
|
||||||
|
// 选中
|
||||||
|
selectedAppearance = selectedAppearance,
|
||||||
|
selectedColorIndex = selectedColorIndex,
|
||||||
|
selectedClothing = selectedClothing,
|
||||||
|
selectedHairstyle = selectedHairstyle,
|
||||||
|
selectedSize = selectedSize,
|
||||||
|
// 新增回调函数
|
||||||
|
onClothingSelected = { clothing ->
|
||||||
|
loadClothingResource(clothing)
|
||||||
|
},
|
||||||
|
onHairstyleSelected = { hairstyle ->
|
||||||
|
loadHairstyleResource(hairstyle)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存合成结果
|
||||||
|
private fun saveCompositeResult(context: Context, compositeBitmap: Bitmap): Uri? {
|
||||||
|
try {
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val imageFileName = "COMPOSITE_${timeStamp}.png"
|
||||||
|
|
||||||
|
val storageDir = context.getExternalFilesDir(android.os.Environment.DIRECTORY_PICTURES)
|
||||||
|
val imageFile = File(storageDir, imageFileName)
|
||||||
|
|
||||||
|
val outputStream = FileOutputStream(imageFile)
|
||||||
|
compositeBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
|
||||||
|
// 更新媒体库
|
||||||
|
val mediaScanIntent = android.content.Intent(android.content.Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||||
|
val uri = Uri.fromFile(imageFile)
|
||||||
|
mediaScanIntent.data = uri
|
||||||
|
context.sendBroadcast(mediaScanIntent)
|
||||||
|
|
||||||
|
return uri
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("CutoutScreen", "保存合成结果失败: ${e.message}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ViewOption(
|
||||||
|
viewType: ViewType,
|
||||||
|
title: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.background(
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(359.dp),
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = if(viewType == ViewType.VISIBLE) Color(0xFF000000) else Color(0xFFEDEDED),
|
||||||
|
shape = RoundedCornerShape(359.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(start = 5.dp, end = 7.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = if(viewType == ViewType.VISIBLE) R.mipmap.ic_view else R.mipmap.ic_hide),
|
||||||
|
contentDescription = "图标",
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
color = if(viewType == ViewType.VISIBLE) Color(0xFF1A1A1A) else Color(0xFF767676),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class ViewType {
|
||||||
|
HIDE,
|
||||||
|
VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewCutoutScreen() {
|
||||||
|
CutoutScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package com.img.rabbit.pages.screen.make
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.components.DrawingBoardFormatPicker
|
||||||
|
import com.img.rabbit.config.CommonData.formats
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
import com.img.rabbit.utils.ExportFormat
|
||||||
|
import com.img.rabbit.utils.ImageUtils.convertToGallery
|
||||||
|
import com.img.rabbit.utils.ImageUtils.getBitmapFromUri
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FormatScreen(navController: NavController) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 当前选中的格式
|
||||||
|
val selectedFormat = remember { mutableStateOf(formats.getOrNull(0)) }
|
||||||
|
|
||||||
|
// 人物图片选择相关状态
|
||||||
|
val selectedImageUri = remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
// 图片选择启动器
|
||||||
|
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
selectedImageUri.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold{
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
imagePickerLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4))
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "", showSave = true) {
|
||||||
|
val bitmap = selectedImageUri.value?.let {uri->
|
||||||
|
getBitmapFromUri(context, uri)
|
||||||
|
}
|
||||||
|
if(bitmap != null){
|
||||||
|
// 保存图片
|
||||||
|
coroutineScope.launch {
|
||||||
|
// 转换图片格式
|
||||||
|
val format = when(selectedFormat.value?.title){
|
||||||
|
ExportFormat.PNG.name -> ExportFormat.PNG
|
||||||
|
ExportFormat.GIF.name -> ExportFormat.GIF
|
||||||
|
ExportFormat.SVG.name -> ExportFormat.SVG
|
||||||
|
else -> ExportFormat.JPG
|
||||||
|
}
|
||||||
|
convertToGallery(
|
||||||
|
context = context,
|
||||||
|
sourceBitmap = bitmap,
|
||||||
|
format = format,
|
||||||
|
onSubmitResult = { fileName, isSuccess ->
|
||||||
|
if (isSuccess) {
|
||||||
|
Toast.makeText(context, "已保存至 $fileName", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 38.dp, end = 38.dp, top = 14.dp)
|
||||||
|
.aspectRatio(300f / 420f)
|
||||||
|
.background(Color(0xFFFFFFFF))
|
||||||
|
.border(1.dp, Color(0xFFD8D8D8))
|
||||||
|
){
|
||||||
|
if (selectedImageUri.value != null) {
|
||||||
|
// 显示选中的原始图片
|
||||||
|
AsyncImage(
|
||||||
|
model = selectedImageUri.value,
|
||||||
|
contentDescription = "选中的图片",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Fit
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 空状态
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_image_empty_pld),
|
||||||
|
contentDescription = "空图",
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "去相册选一张美照吧~",
|
||||||
|
color = Color(0xFFD8D8D8),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 14.dp, vertical = 9.dp)
|
||||||
|
.background(Color(0xFFAAAAAA), shape = RoundedCornerShape(7.dp))
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
){
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "请选择图片导出格式,用于适配各种平台",
|
||||||
|
color = Color(0xFFFFFFFF),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(Modifier.height(14.dp))
|
||||||
|
|
||||||
|
//底部画板
|
||||||
|
DrawingBoardFormatPicker(
|
||||||
|
//格式列表数据
|
||||||
|
formats = formats,
|
||||||
|
//选中
|
||||||
|
selectedFormat = selectedFormat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
package com.img.rabbit.pages.screen.make
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.img.rabbit.bean.LongImageBean
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
import com.img.rabbit.utils.ExportFormat
|
||||||
|
import com.img.rabbit.utils.ImageUtils.getBitmapFromUri
|
||||||
|
import com.img.rabbit.utils.ImageUtils.saveLongToGallery
|
||||||
|
import github.leavesczy.matisse.CoilImageEngine
|
||||||
|
import github.leavesczy.matisse.Matisse
|
||||||
|
import github.leavesczy.matisse.MatisseContract
|
||||||
|
import github.leavesczy.matisse.MediaResource
|
||||||
|
import github.leavesczy.matisse.MediaType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||||
|
import io.moyuru.cropify.Cropify
|
||||||
|
import io.moyuru.cropify.CropifyOption
|
||||||
|
import io.moyuru.cropify.rememberCropifyState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LongImageScreen(navController: NavController) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var imageItems by remember { mutableStateOf<List<LongImageBean>>(emptyList()) }
|
||||||
|
var editingIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val mediaPickerLauncher =
|
||||||
|
rememberLauncherForActivityResult(contract = MatisseContract()) { result: List<MediaResource>? ->
|
||||||
|
if (!result.isNullOrEmpty()) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val newItems = result.mapNotNull { mediaResource ->
|
||||||
|
val bitmap = getBitmapFromUri(context, mediaResource.uri)
|
||||||
|
bitmap?.let {
|
||||||
|
LongImageBean(uri = mediaResource.uri, bitmap = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
imageItems = imageItems + newItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val matisse = Matisse(
|
||||||
|
maxSelectable = 10,
|
||||||
|
imageEngine = CoilImageEngine(),
|
||||||
|
mediaType = MediaType.ImageOnly
|
||||||
|
)
|
||||||
|
mediaPickerLauncher.launch(matisse)
|
||||||
|
}
|
||||||
|
Column (
|
||||||
|
modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4))
|
||||||
|
) {
|
||||||
|
TitleBar(
|
||||||
|
navController = navController,
|
||||||
|
paddingValues = it,
|
||||||
|
title = "拼长图",
|
||||||
|
showSave = true
|
||||||
|
){
|
||||||
|
coroutineScope.launch {
|
||||||
|
saveLongToGallery(context = context, items = imageItems, format = ExportFormat.JPG){ fileName, isSuccess ->
|
||||||
|
if (isSuccess) {
|
||||||
|
Toast.makeText(context, "已保存至 $fileName", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 预览列表(支持每张图独立缩放)
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f).padding(start = 30.dp, end = 30.dp, bottom = 30.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = imageItems,
|
||||||
|
// 使用 URI 和 Bitmap 的 hashCode 组合作为 Key
|
||||||
|
key = { index, item -> "${item.uri}_${item.bitmap.hashCode()}" }
|
||||||
|
) { index, item ->
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clipToBounds()
|
||||||
|
.clickable { editingIndex = index }
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = item.bitmap.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editingIndex?.let { index ->
|
||||||
|
FullScreenCropDialog(
|
||||||
|
item = imageItems[index],
|
||||||
|
onDismiss = { editingIndex = null },
|
||||||
|
onConfirmed = { croppedBitmap ->
|
||||||
|
val newList = imageItems.toMutableList()
|
||||||
|
newList[index] = imageItems[index].copy(bitmap = croppedBitmap)
|
||||||
|
imageItems = newList
|
||||||
|
editingIndex = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FullScreenCropDialog(
|
||||||
|
item: LongImageBean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirmed: (Bitmap) -> Unit
|
||||||
|
) {
|
||||||
|
val cropifyState = rememberCropifyState()
|
||||||
|
val cropifyOption = remember {
|
||||||
|
CropifyOption(
|
||||||
|
frameColor = Color.Cyan,
|
||||||
|
gridColor = Color.White.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Cropify(
|
||||||
|
bitmap = item.bitmap.asImageBitmap(),
|
||||||
|
state = cropifyState,
|
||||||
|
option = cropifyOption, // 传入配置项
|
||||||
|
modifier = Modifier.fillMaxSize().padding(bottom = 100.dp),
|
||||||
|
onImageCropped = { croppedImageBitmap ->
|
||||||
|
onConfirmed(croppedImageBitmap.asAndroidBitmap())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 40.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("取消", color = Color.White)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
// 4. 触发裁剪动作
|
||||||
|
cropifyState.crop()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("确定裁剪")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewLongImageScreen(){
|
||||||
|
LongImageScreen(navController = NavController(LocalContext.current))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
package com.img.rabbit.pages.screen.make
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.gestures.forEachGesture
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.center
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.components.DrawingBoardCertificatePicker
|
||||||
|
import com.img.rabbit.config.CommonData.resizes
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
import com.img.rabbit.utils.ExportFormat
|
||||||
|
import com.img.rabbit.utils.ImageUtils
|
||||||
|
import com.img.rabbit.utils.ImageUtils.saveCanvasToGallery
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ResizeScreen(navController: NavController) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val dealTarget = "RESIZE"
|
||||||
|
// 图片显示区域 - 支持头像编辑(用于最终数据的导出保存Bitmap)
|
||||||
|
val graphicsLayer = rememberGraphicsLayer()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
//加载图片参数
|
||||||
|
val widthParam = navController.previousBackStackEntry?.savedStateHandle?.get<Float>("width") ?: 23f
|
||||||
|
val heightParam = navController.previousBackStackEntry?.savedStateHandle?.get<Float>("height") ?: 35f
|
||||||
|
|
||||||
|
|
||||||
|
// 当前选中的尺寸
|
||||||
|
val selectedSize = remember { mutableStateOf(resizes.first { it.width == widthParam && it.height == heightParam }) }
|
||||||
|
|
||||||
|
// 加载中状态
|
||||||
|
val isLoading = remember { mutableStateOf(false) }
|
||||||
|
// 人物图片选择相关状态
|
||||||
|
val selectedImageUri = remember { mutableStateOf<Uri?>(null) }
|
||||||
|
val resizeBitmap = remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
|
|
||||||
|
var selectedTarget by remember { mutableStateOf<String?>(null) }
|
||||||
|
// 编辑状态
|
||||||
|
val transform = remember { mutableStateOf(TransformState()) }
|
||||||
|
|
||||||
|
// 图片选择启动器
|
||||||
|
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
selectedImageUri.value = it
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
// 执行抠图操作
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val originalBitmap = ImageUtils.getBitmapFromUri(context, it)
|
||||||
|
originalBitmap?.let { bitmap ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
resizeBitmap.value = bitmap
|
||||||
|
isLoading.value = false
|
||||||
|
|
||||||
|
// 重置头像变换
|
||||||
|
transform.value = TransformState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ResizeScreen", "加载图片失败: ${e.message}")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
isLoading.value = false
|
||||||
|
Toast.makeText(context, "加载失败,请重试", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:判断点击位置
|
||||||
|
fun hitTest(tap: Offset, state: TransformState, bmp: Bitmap?, canvasCenter: Offset): Boolean {
|
||||||
|
if (bmp == null) return false
|
||||||
|
val w = bmp.width * state.scale
|
||||||
|
val h = bmp.height * state.scale
|
||||||
|
val left = canvasCenter.x + state.offset.x - w / 2
|
||||||
|
val top = canvasCenter.y + state.offset.y - h / 2
|
||||||
|
return tap.x in left..(left + w) && tap.y in top..(top + h)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold{
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
imagePickerLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4))
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "", showSave = true){
|
||||||
|
// 保存图片
|
||||||
|
coroutineScope.launch {
|
||||||
|
// 从 Layer 捕获 Bitmap
|
||||||
|
val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap()
|
||||||
|
// 保存图片到系统相册(图片已按比例裁剪)
|
||||||
|
saveCanvasToGallery(context, bitmap, ExportFormat.JPG){fileName, isSuccess ->
|
||||||
|
if(isSuccess){
|
||||||
|
Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show()
|
||||||
|
}else{
|
||||||
|
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 保存图片到系统相册(指定尺寸,如果targetWidth与targetHeight比原始值小太多会导致图片模糊)
|
||||||
|
saveCanvasToGallery(context, bitmap, ExportFormat.JPG, selectedSize.value.width.toInt(), selectedSize.value.height.toInt()){fileName, isSuccess ->
|
||||||
|
if(isSuccess){
|
||||||
|
Toast.makeText(context, "已保存为: $fileName", Toast.LENGTH_SHORT).show()
|
||||||
|
}else{
|
||||||
|
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 38.dp, end = 38.dp, top = 14.dp)
|
||||||
|
.aspectRatio(selectedSize.value.width/selectedSize.value.height)
|
||||||
|
.background(Color(0xFFFFFFFF))
|
||||||
|
.border(1.dp, Color(0xFFD8D8D8))
|
||||||
|
){
|
||||||
|
|
||||||
|
if (isLoading.value) {
|
||||||
|
// 加载中状态
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_loading),
|
||||||
|
contentDescription = "加载效果图片",
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "加载中...",
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (resizeBitmap.value != null) {
|
||||||
|
// 显示合成结果(原始图片)
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.drawWithContent {
|
||||||
|
// 将内容绘制到 graphicsLayer 中
|
||||||
|
graphicsLayer.record {
|
||||||
|
this@drawWithContent.drawContent()
|
||||||
|
}
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
.clipToBounds()
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
forEachGesture {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
val down = awaitFirstDown()
|
||||||
|
val center = size.toSize().center
|
||||||
|
|
||||||
|
selectedTarget = when {
|
||||||
|
hitTest(down.position, transform.value, resizeBitmap.value, center) -> dealTarget
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTransformGestures { _, pan, zoom, rotation ->
|
||||||
|
selectedTarget?.let { target ->
|
||||||
|
// 更新逻辑保持不变...
|
||||||
|
val stateRef = transform
|
||||||
|
|
||||||
|
val bmp = resizeBitmap.value
|
||||||
|
|
||||||
|
bmp?.let { b ->
|
||||||
|
val current = stateRef.value
|
||||||
|
val newScale = (current.scale * zoom).coerceIn(0.1f, 5f)
|
||||||
|
stateRef.value = current.copy(
|
||||||
|
offset = current.offset + pan,
|
||||||
|
scale = newScale,
|
||||||
|
rotation = if (target == dealTarget) current.rotation + rotation else current.rotation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val canvasCenter = center
|
||||||
|
resizeBitmap.value?.let { bmp ->
|
||||||
|
withTransform({
|
||||||
|
translate(transform.value.offset.x, transform.value.offset.y)
|
||||||
|
rotate(transform.value.rotation, canvasCenter)
|
||||||
|
scale(transform.value.scale, transform.value.scale, canvasCenter)
|
||||||
|
}) {
|
||||||
|
drawImage(bmp.asImageBitmap(), Offset(canvasCenter.x - bmp.width/2, canvasCenter.y - bmp.height/2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 空状态
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_image_empty_pld),
|
||||||
|
contentDescription = "截图",
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "去相册选一张美照吧~",
|
||||||
|
color = Color(0xFFD8D8D8),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 14.dp, vertical = 9.dp)
|
||||||
|
.background(Color(0xFFAAAAAA), shape = RoundedCornerShape(7.dp))
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
){
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "将已有证件照尺寸进行修改,用于适配各种场景",
|
||||||
|
color = Color(0xFFFFFFFF),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(Modifier.height(14.dp))
|
||||||
|
|
||||||
|
//底部画板
|
||||||
|
DrawingBoardCertificatePicker(
|
||||||
|
//尺寸列表数据
|
||||||
|
sizes = resizes,
|
||||||
|
//选中
|
||||||
|
selectedSize = selectedSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,419 @@
|
||||||
|
package com.img.rabbit.pages.screen.mine
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.components.ImagePicker
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
import com.img.rabbit.viewmodel.FeedbackViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewModel = viewModel()) {
|
||||||
|
Scaffold{
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "意见反馈")
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(start = 16.dp, end = 16.dp)
|
||||||
|
){
|
||||||
|
// 意见反馈内容
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 意见反馈类型
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "*",
|
||||||
|
color = Color(0xFFEA0000),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "您想反馈的功能类型",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 12.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(34.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FUNCTION) 0xFF252525 else 0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
Color(0xFFD8D8D8),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { viewModel.setFeedbackType(FeedbackViewModel.FeedbackType.FUNCTION) }
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "功能问题",
|
||||||
|
color = Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FUNCTION) 0xFFC2FF43 else 0xFF1A1A1A),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.width(21.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(34.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FEATURE) 0xFF252525 else 0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
Color(0xFFD8D8D8),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { viewModel.setFeedbackType(FeedbackViewModel.FeedbackType.FEATURE) }
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "优化建议",
|
||||||
|
color = Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.FEATURE) 0xFFC2FF43 else 0xFF1A1A1A),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.width(21.dp)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(34.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.OTHER) 0xFF252525 else 0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
Color(0xFFD8D8D8),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { viewModel.setFeedbackType(FeedbackViewModel.FeedbackType.OTHER) }
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "其他",
|
||||||
|
color = Color(if (viewModel.feedbackType == FeedbackViewModel.FeedbackType.OTHER) 0xFFC2FF43 else 0xFF1A1A1A),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//补充反馈内容
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "*",
|
||||||
|
color = Color(0xFFEA0000),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "请补充详细问题或意见",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(150.dp)
|
||||||
|
.padding(top = 12.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFFEEEEEE),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
Color(0xFFEEEEEE),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
){
|
||||||
|
BasicTextField(
|
||||||
|
value = viewModel.feedbackMore,
|
||||||
|
onValueChange = {content->
|
||||||
|
if (content.length <= 200) {
|
||||||
|
viewModel.setFeedbackMore(content)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.padding(start = 18.dp, top = 7.dp, end = 18.dp, bottom = 7.dp),
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = Color.Black,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
singleLine = false,
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Text,
|
||||||
|
imeAction = androidx.compose.ui.text.input.ImeAction.Done
|
||||||
|
),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
) {
|
||||||
|
if (viewModel.feedbackMore.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
"请在这里输入内容",
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${viewModel.feedbackMore.length}/200",
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 18.dp, bottom = 7.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//提供相关图片
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "请提供相关问题的图片",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "(选填,最多可上传三张)",
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight().padding(top = 12.dp)
|
||||||
|
) {
|
||||||
|
ImagePicker(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||||
|
imageHeight = 100.dp,
|
||||||
|
aspectRatio = 100f / 100f,
|
||||||
|
maxCount = 3,
|
||||||
|
addButtonName = "添加图片",
|
||||||
|
currentImageUris = viewModel.currentImageUris,
|
||||||
|
currentImagePaths = emptyList(), // 新增参数
|
||||||
|
onImagesUpdated = { uris, _ ->
|
||||||
|
viewModel.setCurrentImageUris(uris)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//联系方式
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "联系方式",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "(选填,可以留下您的手机号、微信、邮箱)",
|
||||||
|
color = Color(0xFFAAAAAA),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(top = 12.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFFEEEEEE),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
Color(0xFFEEEEEE),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
){
|
||||||
|
BasicTextField(
|
||||||
|
value = viewModel.feedbackContact,
|
||||||
|
onValueChange = {content->
|
||||||
|
if (content.length <= 50) {
|
||||||
|
viewModel.setFeedbackContact(content)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.padding(start = 18.dp, top = 10.dp, end = 18.dp, bottom = 10.dp),
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = Color.Black,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
maxLines = 1,
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Text,
|
||||||
|
imeAction = androidx.compose.ui.text.input.ImeAction.Done
|
||||||
|
),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
) {
|
||||||
|
if (viewModel.feedbackContact.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
"请输入您的联系方式",
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//提交
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 33.dp, end = 33.dp, bottom = 30.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFF252525),
|
||||||
|
shape = RoundedCornerShape(359.dp),
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 提交反馈
|
||||||
|
viewModel.submitFeedback()
|
||||||
|
}
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"提交",
|
||||||
|
color = Color(0xFFC2FF43),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewFeedbackScreen(){
|
||||||
|
FeedbackScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.img.rabbit.pages.screen.mine
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnlineServiceScreen(navController: NavHostController) {
|
||||||
|
Scaffold{
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(it)
|
||||||
|
) {
|
||||||
|
// 顶部栏
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 返回按钮
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_back),
|
||||||
|
contentDescription = "返回",
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { navController.popBackStack() }
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(end = 26.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(end = 26.dp)
|
||||||
|
){
|
||||||
|
//TODO 在线客服内容
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewOnlineServiceScreen(){
|
||||||
|
OnlineServiceScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
package com.img.rabbit.pages.screen.mine
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingScreen(navController: NavHostController) {
|
||||||
|
Scaffold{
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "设置")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp)
|
||||||
|
){
|
||||||
|
//TODO 设置内容
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
RoundedCornerShape(18.dp)
|
||||||
|
)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
ambientColor = Color(0x4DE5E5E5),
|
||||||
|
spotColor = Color(0x4DE5E5E5)
|
||||||
|
)
|
||||||
|
){
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 跳转页面
|
||||||
|
//navController.navigate("feedback")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "清除缓存",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"12MB",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 跳转绑定页面
|
||||||
|
navController.navigate("bindAccount")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "账号绑定",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 跳转账号管理页面
|
||||||
|
navController.navigate("managerAccount")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "账号管理",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 跳转关于我们页面
|
||||||
|
navController.navigate("aboutMine")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "关于我们",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//切换/退出账号
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
//账号切换
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 33.dp, end = 33.dp, bottom = 18.dp)
|
||||||
|
.background(
|
||||||
|
Color(0x00000000),
|
||||||
|
shape = RoundedCornerShape(359.dp),
|
||||||
|
)
|
||||||
|
.border(width = 1.dp, color = Color(0xFF000000), shape = RoundedCornerShape(359.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 账号切换
|
||||||
|
navController.navigate("managerAccount")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"账号切换",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//退出登录
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 33.dp, end = 33.dp, bottom = 30.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFF252525),
|
||||||
|
shape = RoundedCornerShape(359.dp),
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 退出登录
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"退出登录",
|
||||||
|
color = Color(0xFFC2FF43),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewSettingScreen(){
|
||||||
|
SettingScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.img.rabbit.pages.screen.mine
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VersionUpdateScreen(navController: NavHostController) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewVersionUpdateScreen(){
|
||||||
|
VersionUpdateScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
package com.img.rabbit.pages.screen.mine.setting
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.config.Common.agreementUrl
|
||||||
|
import com.img.rabbit.config.Common.privacyUrl
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
import com.img.rabbit.utils.UrlLinkUtils.openAgreement
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AboutScreen(navController: NavHostController) {
|
||||||
|
Scaffold{
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "关于我们")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp)
|
||||||
|
){
|
||||||
|
//TODO 设置内容
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 100.dp)
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_launcher_logo),
|
||||||
|
contentDescription = "关于我们",
|
||||||
|
modifier = Modifier
|
||||||
|
.width(100.dp)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "截图兔",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(top = 24.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
RoundedCornerShape(18.dp)
|
||||||
|
)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
ambientColor = Color(0x4DE5E5E5),
|
||||||
|
spotColor = Color(0x4DE5E5E5)
|
||||||
|
)
|
||||||
|
){
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 跳转用户协议页面
|
||||||
|
openAgreement(context, agreementUrl)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "用户协议",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 跳转隐私政策页面
|
||||||
|
openAgreement(context, privacyUrl)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "隐私政策",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewAboutScreen(){
|
||||||
|
AboutScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
package com.img.rabbit.pages.screen.mine.setting
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
|
||||||
|
@SuppressLint("UnrememberedMutableState")
|
||||||
|
@Composable
|
||||||
|
fun AccountBindScreen(navController: NavHostController) {
|
||||||
|
val showDialogStatus = mutableStateOf(false)
|
||||||
|
Scaffold{
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
){
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "账号绑定")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp)
|
||||||
|
){
|
||||||
|
//TODO 设置内容
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
RoundedCornerShape(18.dp)
|
||||||
|
)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
ambientColor = Color(0x4DE5E5E5),
|
||||||
|
spotColor = Color(0x4DE5E5E5)
|
||||||
|
)
|
||||||
|
){
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 跳转绑定与解绑手机号
|
||||||
|
showDialogStatus.value = !showDialogStatus.value
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "手机号",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"+86 123****123",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
|
||||||
|
Color(0x4DBBBBBB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 跳转绑定或解绑微信
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "微信账号",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"去绑定",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_arrow_right),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showDialogStatus.value){
|
||||||
|
UnBindPhoneDialog(
|
||||||
|
showDialogStatus = showDialogStatus,
|
||||||
|
onStatusChange = {
|
||||||
|
showDialogStatus.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UnBindPhoneDialog(
|
||||||
|
showDialogStatus: MutableState<Boolean>,
|
||||||
|
onStatusChange: (Boolean) -> Unit
|
||||||
|
){
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0x80000000))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
){
|
||||||
|
showDialogStatus.value = false
|
||||||
|
},
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(horizontal = 36.dp)
|
||||||
|
.background(Color.White, shape = RoundedCornerShape(26.dp))
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
){
|
||||||
|
//什么都不用做,只是解决点击穿透问题
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_dialog_top_mask1),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(top = 46.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "解开绑定的手机号?",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(14.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "当前绑定的手机号码为",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "+86 123****456",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
//切换/退出账号
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 18.dp, end = 18.dp, bottom = 20.dp)
|
||||||
|
) {
|
||||||
|
//取消解绑手机号
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
Color(0x00000000),
|
||||||
|
shape = RoundedCornerShape(359.dp),
|
||||||
|
)
|
||||||
|
.border(width = 1.dp, color = Color(0xFF000000), shape = RoundedCornerShape(359.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 取消解绑手机号
|
||||||
|
showDialogStatus.value = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"取消",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(11.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//退出登录
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
Color(0xFF252525),
|
||||||
|
shape = RoundedCornerShape(359.dp),
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 确定解绑手机号
|
||||||
|
showDialogStatus.value = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"确定",
|
||||||
|
color = Color(0xFFC2FF43),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewAccountBindScreen(){
|
||||||
|
AccountBindScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnrememberedMutableState")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewUnBindPhoneDialog(){
|
||||||
|
UnBindPhoneDialog(showDialogStatus = mutableStateOf(true), onStatusChange = {})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
package com.img.rabbit.pages.screen.mine.setting
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.bean.UserInfo
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AccountManagerScreen(navController: NavHostController) {
|
||||||
|
val userList by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
listOf(
|
||||||
|
UserInfo(1, "张三", true),
|
||||||
|
UserInfo(2, "李四", false),
|
||||||
|
UserInfo(3, "王五", false),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Scaffold{
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
){
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_account_switch_top_mask),
|
||||||
|
contentDescription = "蒙层",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "")
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 100.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "轻触头像以切换帐号",
|
||||||
|
color = Color(0xFF3D3D3D),
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(top = 55.dp, start = 17.dp, end = 17.dp)
|
||||||
|
){
|
||||||
|
//TODO 设置内容
|
||||||
|
LazyColumn {
|
||||||
|
// 添加五个项目
|
||||||
|
items(userList.size) { index ->
|
||||||
|
ListItem(userList[index])
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
AddItem(navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListItem(item: UserInfo){
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
.background(
|
||||||
|
color = Color(0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
ambientColor = Color(0x4DE5E5E5),
|
||||||
|
spotColor = Color(0x4DE5E5E5)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 11.dp, vertical = 17.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 切换账号
|
||||||
|
//navController.navigate("feedback")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_launcher_logo),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(46.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "小林",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "ID:123456",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF767676),
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = item.login,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
//viewModel.setIsPolicyAgreement(isChecked)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.scale(0.35f)
|
||||||
|
.padding(start = 6.dp)
|
||||||
|
.background(
|
||||||
|
if (item.login) Color(0xFF252525)
|
||||||
|
else Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(36.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = if (item.login) Color(0xFF252525)
|
||||||
|
else Color(0xFFCCCCCC),
|
||||||
|
shape = RoundedCornerShape(36.dp)
|
||||||
|
)
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
colors = androidx.compose.material3.CheckboxDefaults.colors(
|
||||||
|
checkedColor = Color.Transparent, // 隐藏默认背景
|
||||||
|
uncheckedColor = Color.Transparent, // 隐藏默认背景
|
||||||
|
checkmarkColor = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddItem(navController: NavController){
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.background(
|
||||||
|
color = Color(0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
ambientColor = Color(0x4DE5E5E5),
|
||||||
|
spotColor = Color(0x4DE5E5E5)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
//TODO 添加账号
|
||||||
|
navController.navigate("login")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 11.dp, vertical = 17.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_picture_add),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(46.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.background(Color(0xFFF3F3F3), shape = RoundedCornerShape(8.dp))
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "添加账号",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewAccountManagerScreen(){
|
||||||
|
AccountManagerScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
package com.img.rabbit.pages.screen.other
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
import com.img.rabbit.pages.toolbar.TitleBar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CameraGuideScreen(navController: NavController) {
|
||||||
|
Scaffold {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_camera_mask),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.wrapContentSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TitleBar(navController = navController, paddingValues = it, title = "", showSave = false)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(top = 104.dp)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "如何拍照?",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 20.dp)
|
||||||
|
.background(Color(0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 20.dp)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = "1.注意光线均匀\n 2.不能耸肩或斜肩\n 3.正对镜头,双耳露出\n 4.不要佩戴眼镜\n 5.注意纯色墙作背景\n 6.避免衣服与背景色相同",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
modifier = Modifier.padding(top = 20.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth().padding(top = 11.dp),
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_photo_sample),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.wrapContentSize().align(Alignment.End)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "证件照背景颜色要求",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(top = 24.dp, start = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 20.dp)
|
||||||
|
.background(Color(0xFFFFFFFF),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 18.dp)
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(18.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.background(Color(0xFFFFFFFF), shape = RoundedCornerShape(56.dp))
|
||||||
|
.border(0.5.dp, Color(0xFFAAAAAA), shape = RoundedCornerShape(56.dp))
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "白色背景:",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(start = 4.dp).align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "用于护照、签证、驾驶证、身份证、二代身份证、驾驶证、黑白证件、医保卡、港澳通行证、护照等。",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 24.dp)
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(18.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.background(Color(0xFF438EDB), shape = RoundedCornerShape(56.dp))
|
||||||
|
.border(0.5.dp, Color(0xFFAAAAAA), shape = RoundedCornerShape(56.dp))
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "蓝色背景:",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(start = 4.dp).align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "用于毕业证、工作证、简历等 (蓝色数值为R:0 G:191 B:243 或 C:67 M:Z Y:0 k:0)",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 24.dp)
|
||||||
|
){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(18.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.background(Color(0xFFFF0000), shape = RoundedCornerShape(56.dp))
|
||||||
|
.border(0.5.dp, Color(0xFFAAAAAA), shape = RoundedCornerShape(56.dp))
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "红色背景:",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(start = 4.dp).align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "用于保险、医保、IC卡、暂住证、结婚照 (红色数值为R:255 G:0 B:0 或 C:0 M:99 Y:100 K: 0)",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewCameraGuideScreen() {
|
||||||
|
CameraGuideScreen(navController = NavController(LocalContext.current))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
package com.img.rabbit.pages.toolbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.img.rabbit.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, onSubmit: (() -> Unit)? = null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(paddingValues)
|
||||||
|
){
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth().padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 返回按钮
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_back),
|
||||||
|
contentDescription = "返回",
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { navController?.popBackStack() }
|
||||||
|
.padding(end = 26.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().weight(1f)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = title ?: "",
|
||||||
|
color = Color(0xFF1A1A1A),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showSave) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(60.dp)
|
||||||
|
.height(36.dp)
|
||||||
|
.background(
|
||||||
|
Color(0xFF252525),
|
||||||
|
shape = RoundedCornerShape(359.dp),
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
// 保存
|
||||||
|
onSubmit?.invoke()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"保存",
|
||||||
|
color = Color(0xFFC2FF43),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(60.dp)
|
||||||
|
.height(36.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewTitleBar() {
|
||||||
|
TitleBar(navController = rememberNavController(), paddingValues = PaddingValues(), title = "截图兔", showSave = true)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.img.rabbit.route
|
||||||
|
|
||||||
|
// 定义导航路由
|
||||||
|
sealed class ScreenRoute(val route: String) {
|
||||||
|
//Tab页面
|
||||||
|
object Home : ScreenRoute("home")
|
||||||
|
object Mine : ScreenRoute("mine")
|
||||||
|
//首页(抠图)
|
||||||
|
object Cutout : ScreenRoute("cutout")
|
||||||
|
//首页(证件)
|
||||||
|
object Resize : ScreenRoute("resize")
|
||||||
|
//首页(格式)
|
||||||
|
object Format : ScreenRoute("format")
|
||||||
|
//首页(拍照指南)
|
||||||
|
object CameraGuide : ScreenRoute("cameraGuide")
|
||||||
|
//首页(长图)
|
||||||
|
object LongImage : ScreenRoute("longImage")
|
||||||
|
|
||||||
|
//我的页面(Mine)
|
||||||
|
object Feedback : ScreenRoute("feedback")
|
||||||
|
object OnlineService : ScreenRoute("onlineService")
|
||||||
|
object Login : ScreenRoute("login")
|
||||||
|
object Setting : ScreenRoute("setting")
|
||||||
|
|
||||||
|
//设置页面(Setting)
|
||||||
|
object BindAccount : ScreenRoute("bindAccount")
|
||||||
|
object ManagerAccount : ScreenRoute("managerAccount")
|
||||||
|
object AboutMine : ScreenRoute("aboutMine")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
|
object AgreementTextHelper {
|
||||||
|
|
||||||
|
fun setupAgreementTextView(fullText: String, targets: Map<String, String>, textView: TextView, isUnderlineText: Boolean = true, onAgreementClick: (String) -> Unit) {
|
||||||
|
val spannableString = SpannableString(fullText)
|
||||||
|
|
||||||
|
// 设置各个协议的点击区域
|
||||||
|
targets.forEach { (targetKey, target) ->
|
||||||
|
setClickableSpan(spannable = spannableString, target = target, isUnderlineText = isUnderlineText, color = "#FF767676".toColorInt()) {
|
||||||
|
onAgreementClick(targetKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textView.text = spannableString
|
||||||
|
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
textView.highlightColor = Color.TRANSPARENT
|
||||||
|
|
||||||
|
// 设置长按不显示复制菜单
|
||||||
|
textView.setOnLongClickListener { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setClickableSpan(
|
||||||
|
spannable: Spannable,
|
||||||
|
target: String,
|
||||||
|
isUnderlineText: Boolean,
|
||||||
|
color: Int,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val start = spannable.indexOf(target)
|
||||||
|
if (start >= 0) {
|
||||||
|
val end = start + target.length
|
||||||
|
val clickableSpan = object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDrawState(ds: TextPaint) {
|
||||||
|
super.updateDrawState(ds)
|
||||||
|
ds.color = color
|
||||||
|
ds.isUnderlineText = isUnderlineText
|
||||||
|
ds.typeface = Typeface.DEFAULT_BOLD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
// 创建DataStore实例
|
||||||
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "app_settings")
|
||||||
|
|
||||||
|
// DataStore键定义
|
||||||
|
object AppPreferencesKeys {
|
||||||
|
val NAVIGATION_BAR_VISIBLE = booleanPreferencesKey("navigation_bar_visible")
|
||||||
|
val USER_LOGGED_IN = booleanPreferencesKey("user_logged_in")
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppDataStore(private val context: Context) {
|
||||||
|
|
||||||
|
// 获取NavigationBar显示状态
|
||||||
|
val isNavigationBarVisible: Flow<Boolean> = context.dataStore.data
|
||||||
|
.map { preferences ->
|
||||||
|
preferences[AppPreferencesKeys.NAVIGATION_BAR_VISIBLE] ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置NavigationBar显示状态
|
||||||
|
suspend fun setNavigationBarVisible(visible: Boolean) {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences[AppPreferencesKeys.NAVIGATION_BAR_VISIBLE] = visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户登录状态
|
||||||
|
val isUserLoggedIn: Flow<Boolean> = context.dataStore.data
|
||||||
|
.map { preferences ->
|
||||||
|
preferences[AppPreferencesKeys.USER_LOGGED_IN] ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户登录状态
|
||||||
|
suspend fun setUserLoggedIn(loggedIn: Boolean) {
|
||||||
|
context.dataStore.edit { preferences ->
|
||||||
|
preferences[AppPreferencesKeys.USER_LOGGED_IN] = loggedIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package com.img.rabbit.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
public class Bitmap2SVG
|
||||||
|
{
|
||||||
|
public static boolean convert(Context context, File output, Bitmap input){
|
||||||
|
Bitmap2SVG svg;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
svg = new Bitmap2SVG( new PrintWriter( new BufferedWriter( new FileWriter( output ) ) ), true );
|
||||||
|
} catch (IOException e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.printSVGHeader( input.getWidth(), input.getHeight() );
|
||||||
|
svg.printSVGBody( input );
|
||||||
|
svg.printSVGFooter();
|
||||||
|
|
||||||
|
saveToGallery(context, output, "displayName.svg");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean trc_del;
|
||||||
|
private PrintWriter pw;
|
||||||
|
private int w, h;
|
||||||
|
|
||||||
|
private Bitmap2SVG( PrintWriter pw, boolean trc_delete )
|
||||||
|
{
|
||||||
|
this.pw = pw;
|
||||||
|
trc_del = trc_delete;
|
||||||
|
}
|
||||||
|
private void printSVGHeader( int width, int height )
|
||||||
|
{
|
||||||
|
pw.println( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" );
|
||||||
|
|
||||||
|
pw.print( "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"" );
|
||||||
|
pw.print( width );
|
||||||
|
pw.print( "\" height=\"" );
|
||||||
|
pw.print( height );
|
||||||
|
pw.println( "\">" );
|
||||||
|
}
|
||||||
|
private void printSVGFooter()
|
||||||
|
{
|
||||||
|
pw.println( "</svg>" );
|
||||||
|
pw.close();
|
||||||
|
}
|
||||||
|
private void printSVGBody( Bitmap bmp )
|
||||||
|
{
|
||||||
|
int x, y, c, a;
|
||||||
|
w = bmp.getWidth();
|
||||||
|
h = bmp.getHeight();
|
||||||
|
for ( y = 0 ; y < h ; ++y )
|
||||||
|
{
|
||||||
|
for ( x = 0 ; x < w ;++x )
|
||||||
|
{
|
||||||
|
c = bmp.getPixel( x, y );
|
||||||
|
a = Color.alpha( c );
|
||||||
|
if ( trc_del && a == 0 ){ continue; }
|
||||||
|
printDot( x, y, rgb( c ), a );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void printDot( int x, int y, String rgb, int a )
|
||||||
|
{
|
||||||
|
pw.print( "<rect width=\"1\" height=\"1\" x=\"" );
|
||||||
|
pw.print( x );
|
||||||
|
pw.print( "\" y=\"" );
|
||||||
|
pw.print( y );
|
||||||
|
pw.print( "\" " );
|
||||||
|
pw.print( "style=\"fill:#" );
|
||||||
|
pw.print( rgb );
|
||||||
|
if ( a < 255 )
|
||||||
|
{
|
||||||
|
pw.print( ";fill-opacity:" );
|
||||||
|
pw.print( a / 255.0 );
|
||||||
|
}
|
||||||
|
pw.println( ";\" />" );
|
||||||
|
}
|
||||||
|
private static String rgb( int c )
|
||||||
|
{
|
||||||
|
return String.format( "%02x%02x%02x", Color.red( c ), Color.green( c ), Color.blue( c ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveToGallery(Context context, File svgFile, String displayName) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
// 关键点:显式指定 MIME 类型为 SVG
|
||||||
|
values.put(MediaStore.Images.Media.MIME_TYPE, "image/svg+xml");
|
||||||
|
values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName + ".svg");
|
||||||
|
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
|
||||||
|
|
||||||
|
ContentResolver resolver = context.getContentResolver();
|
||||||
|
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
try (OutputStream os = resolver.openOutputStream(uri);
|
||||||
|
FileInputStream fis = new FileInputStream(svgFile)) {
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = fis.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
// 此时文件已带上正确的 MIME 类型存入相册
|
||||||
|
Toast.makeText(context, "SVG已保存", Toast.LENGTH_SHORT).show();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.ImageView
|
||||||
|
import coil3.load
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.request.transformations
|
||||||
|
import coil3.transform.RoundedCornersTransformation
|
||||||
|
import com.luck.picture.lib.engine.ImageEngine
|
||||||
|
import com.luck.picture.lib.utils.ActivityCompatHelper
|
||||||
|
|
||||||
|
class CoilEngine : ImageEngine {
|
||||||
|
|
||||||
|
// 加载普通图片
|
||||||
|
override fun loadImage(context: Context, url: String, imageView: ImageView) {
|
||||||
|
if (!ActivityCompatHelper.assertValidRequest(context)) return
|
||||||
|
imageView.load(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadImage(
|
||||||
|
context: Context?,
|
||||||
|
imageView: ImageView?,
|
||||||
|
url: String?,
|
||||||
|
maxWidth: Int,
|
||||||
|
maxHeight: Int
|
||||||
|
) {
|
||||||
|
if (!ActivityCompatHelper.assertValidRequest(context)) return
|
||||||
|
imageView?.load(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载相册目录封面
|
||||||
|
override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) {
|
||||||
|
if (!ActivityCompatHelper.assertValidRequest(context)) return
|
||||||
|
imageView.load(url) {
|
||||||
|
transformations(RoundedCornersTransformation(8f))
|
||||||
|
size(180, 180) // 优化内存,封面图没必要太大
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载图片列表中的小图
|
||||||
|
override fun loadGridImage(context: Context, url: String, imageView: ImageView) {
|
||||||
|
if (!ActivityCompatHelper.assertValidRequest(context)) return
|
||||||
|
imageView.load(url) {
|
||||||
|
size(200, 200)
|
||||||
|
crossfade(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂停/恢复加载(Coil 自动处理,可留空)
|
||||||
|
override fun pauseRequests(context: Context?) {}
|
||||||
|
override fun resumeRequests(context: Context?) {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var instance: CoilEngine? = null
|
||||||
|
fun createCoilEngine(): CoilEngine {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: CoilEngine().also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片选择工具类
|
||||||
|
*/
|
||||||
|
object ImagePickerUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建相机拍照的Intent
|
||||||
|
*/
|
||||||
|
fun createCameraIntent(context: Context): Pair<Intent, File> {
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val storageDir = context.externalCacheDir ?: context.cacheDir
|
||||||
|
val imageFile = File.createTempFile(
|
||||||
|
"JPEG_${timeStamp}_",
|
||||||
|
".jpg",
|
||||||
|
storageDir
|
||||||
|
)
|
||||||
|
|
||||||
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||||
|
putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(imageFile))
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(intent, imageFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建相册选择的Intent
|
||||||
|
*/
|
||||||
|
fun createGalleryIntent(): Intent {
|
||||||
|
return Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
|
||||||
|
type = "image/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图片的Uri路径
|
||||||
|
*/
|
||||||
|
fun getImageUri(context: Context, file: File): Uri {
|
||||||
|
return androidx.core.content.FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片选择结果数据类
|
||||||
|
*/
|
||||||
|
data class ImagePickerResult(
|
||||||
|
val uri: Uri? = null,
|
||||||
|
val filePath: String? = null,
|
||||||
|
val error: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可组合函数:创建相机启动器
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberCameraLauncher(
|
||||||
|
context: Context = LocalContext.current,
|
||||||
|
onResult: (ImagePickerResult) -> Unit
|
||||||
|
) = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.TakePicture()
|
||||||
|
) { success ->
|
||||||
|
if (success) {
|
||||||
|
// 这里需要与createCameraIntent配合使用,实际使用时需要保存临时文件路径
|
||||||
|
onResult(ImagePickerResult(error = "需要配合createCameraIntent使用"))
|
||||||
|
} else {
|
||||||
|
onResult(ImagePickerResult(error = "拍照取消或失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可组合函数:创建相册启动器(单个图片选择)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberGalleryLauncher(
|
||||||
|
onResult: (ImagePickerResult) -> Unit
|
||||||
|
) = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.PickVisualMedia()
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
onResult(ImagePickerResult(uri = uri))
|
||||||
|
} else {
|
||||||
|
onResult(ImagePickerResult(error = "未选择图片"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可组合函数:创建相册启动器(多个图片选择)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberMultipleGalleryLauncher(
|
||||||
|
onResult: (List<ImagePickerResult>) -> Unit
|
||||||
|
) = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.PickMultipleVisualMedia()
|
||||||
|
) { uris ->
|
||||||
|
val results = uris.map { uri ->
|
||||||
|
ImagePickerResult(uri = uri)
|
||||||
|
}
|
||||||
|
onResult(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可组合函数:创建通用图片选择启动器(支持相机和相册)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberImagePickerLauncher(
|
||||||
|
onResult: (ImagePickerResult) -> Unit
|
||||||
|
) = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
onResult(ImagePickerResult(uri = uri))
|
||||||
|
} else {
|
||||||
|
onResult(ImagePickerResult(error = "未选择图片"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,562 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.apply
|
||||||
|
|
||||||
|
import android.graphics.*
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
import com.img.rabbit.bean.LongImageBean
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import androidx.core.graphics.withClip
|
||||||
|
|
||||||
|
object ImageUtils {
|
||||||
|
fun decodeSampledBitmapFromResource(
|
||||||
|
context: Context,
|
||||||
|
resId: Int,
|
||||||
|
reqWidth: Int,
|
||||||
|
reqHeight: Int
|
||||||
|
): Bitmap {
|
||||||
|
// 首先设置inJustDecodeBounds=true来获取图片尺寸
|
||||||
|
val options = BitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
|
BitmapFactory.decodeResource(context.resources, resId, options)
|
||||||
|
|
||||||
|
// 计算inSampleSize
|
||||||
|
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
|
||||||
|
Log.d("ImageUtils", "Sample size: ${options.inSampleSize}")
|
||||||
|
|
||||||
|
// 根据计算的inSampleSize来解码图片
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
return BitmapFactory.decodeResource(context.resources, resId, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:从byte数组加载图片的方法
|
||||||
|
fun decodeSampledBitmapFromByteArray(
|
||||||
|
byteArray: ByteArray,
|
||||||
|
reqWidth: Int,
|
||||||
|
reqHeight: Int
|
||||||
|
): Bitmap {
|
||||||
|
// 首先设置inJustDecodeBounds=true来获取图片尺寸
|
||||||
|
val options = BitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
|
BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
|
||||||
|
|
||||||
|
// 计算inSampleSize
|
||||||
|
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
|
||||||
|
|
||||||
|
// 根据计算的inSampleSize来解码图片
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateInSampleSize(
|
||||||
|
options: BitmapFactory.Options,
|
||||||
|
reqWidth: Int,
|
||||||
|
reqHeight: Int
|
||||||
|
): Int {
|
||||||
|
// 原始图片的尺寸
|
||||||
|
val height = options.outHeight
|
||||||
|
val width = options.outWidth
|
||||||
|
var inSampleSize = 1
|
||||||
|
|
||||||
|
if (height > reqHeight || width > reqWidth) {
|
||||||
|
val halfHeight = height / 2
|
||||||
|
val halfWidth = width / 2
|
||||||
|
|
||||||
|
// 计算最大的inSampleSize值,该值为2的幂,并保持
|
||||||
|
// 高度和宽度均大于请求的高度和宽度
|
||||||
|
while ((halfHeight / inSampleSize) >= reqHeight
|
||||||
|
&& (halfWidth / inSampleSize) >= reqWidth) {
|
||||||
|
inSampleSize *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inSampleSize
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Bitmap保存到设备存储
|
||||||
|
* @param bitmap 要保存的Bitmap对象
|
||||||
|
* @param context 上下文
|
||||||
|
* @param format 图片格式(Bitmap.CompressFormat.JPEG 或 Bitmap.CompressFormat.PNG)
|
||||||
|
* @param quality 压缩质量(0-100)
|
||||||
|
* @return 保存的文件路径,如果保存失败则返回null
|
||||||
|
*/
|
||||||
|
fun saveBitmapToStorage(
|
||||||
|
bitmap: Bitmap,
|
||||||
|
context: Context,
|
||||||
|
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
|
||||||
|
quality: Int = 90
|
||||||
|
): String? {
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val imageFileName = "JPEG_${timeStamp}_"
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val imageFile: File
|
||||||
|
val imagePath: String?
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
// Android 10及以上使用MediaStore
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.Images.Media.DISPLAY_NAME, "$imageFileName.${format.name.lowercase()}")
|
||||||
|
put(MediaStore.Images.Media.MIME_TYPE, "image/${format.name.lowercase()}")
|
||||||
|
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + "DoctorApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
|
||||||
|
imageUri?.let { uri ->
|
||||||
|
resolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
bitmap.compress(format, quality, outputStream)
|
||||||
|
}
|
||||||
|
return uri.toString()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Android 9及以下使用传统文件存储
|
||||||
|
val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES + File.separator + "DoctorApp")
|
||||||
|
storageDir?.let { dir ->
|
||||||
|
if (!dir.exists() && !dir.mkdirs()) {
|
||||||
|
Log.e("ImageUtils", "无法创建存储目录")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile = File(dir, "$imageFileName.${format.name.lowercase()}")
|
||||||
|
val outputStream = FileOutputStream(imageFile)
|
||||||
|
bitmap.compress(format, quality, outputStream)
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
|
||||||
|
// 将图片添加到媒体库
|
||||||
|
MediaStore.Images.Media.insertImage(context.contentResolver, imageFile.absolutePath, imageFile.name, null)
|
||||||
|
|
||||||
|
// 通知媒体扫描器扫描文件
|
||||||
|
context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(imageFile)))
|
||||||
|
|
||||||
|
return imageFile.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("ImageUtils", "保存图片失败", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字节数组直接保存为图片文件
|
||||||
|
* @param byteArray 图片字节数组
|
||||||
|
* @param context 上下文
|
||||||
|
* @param format 图片格式
|
||||||
|
* @return 保存的文件路径,如果保存失败则返回null
|
||||||
|
*/
|
||||||
|
fun saveByteArrayToStorage(
|
||||||
|
byteArray: ByteArray,
|
||||||
|
context: Context,
|
||||||
|
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG
|
||||||
|
): String? {
|
||||||
|
return try {
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
||||||
|
saveBitmapToStorage(bitmap, context, format)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImageUtils", "保存字节数组为图片失败", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从URI获取Bitmap
|
||||||
|
* @param context 上下文
|
||||||
|
* @param uri 图片URI
|
||||||
|
* @return Bitmap对象,如果获取失败则返回null
|
||||||
|
*/
|
||||||
|
fun getBitmapFromUri(context: Context, uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImageUtils", "从URI获取Bitmap失败", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将URI对应的图片保存到存储
|
||||||
|
* @param context 上下文
|
||||||
|
* @param uri 图片URI
|
||||||
|
* @param format 图片格式
|
||||||
|
* @param quality 压缩质量
|
||||||
|
* @return 保存的文件路径,如果保存失败则返回null
|
||||||
|
*/
|
||||||
|
fun saveUriToStorage(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
|
||||||
|
quality: Int = 90
|
||||||
|
): String? {
|
||||||
|
return try {
|
||||||
|
val bitmap = getBitmapFromUri(context, uri)
|
||||||
|
bitmap?.let {
|
||||||
|
saveBitmapToStorage(it, context, format, quality)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImageUtils", "保存URI图片失败", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存长图到相册
|
||||||
|
* @param context 上下文
|
||||||
|
* @param items 长图数据列表
|
||||||
|
* @param format 导出格式
|
||||||
|
* @param onSubmitResult 回调函数,用于通知保存结果
|
||||||
|
*/
|
||||||
|
fun saveLongToGallery(
|
||||||
|
context: Context,
|
||||||
|
items: List<LongImageBean>,
|
||||||
|
format: ExportFormat,
|
||||||
|
onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
|
||||||
|
// 1. 定义长图的统一输出宽度
|
||||||
|
val canvasWidth = 1080f
|
||||||
|
|
||||||
|
// 2. 计算每张图按比例缩放后的总高度
|
||||||
|
var totalHeight = 0f
|
||||||
|
val renderConfigs = items.map { item ->
|
||||||
|
// 计算当前图片填满 1080 宽度所需的缩放比例
|
||||||
|
val scale = canvasWidth / item.bitmap.width
|
||||||
|
// 缩放后的实际物理高度
|
||||||
|
val scaledHeight = item.bitmap.height * scale
|
||||||
|
totalHeight += scaledHeight
|
||||||
|
scale to scaledHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建大图画布
|
||||||
|
val resultBitmap = Bitmap.createBitmap(canvasWidth.toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(resultBitmap)
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG) // 开启抗锯齿,防止缩放模糊
|
||||||
|
var currentY = 0f
|
||||||
|
|
||||||
|
// 4. 依次绘制每一张图片
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
val (scale, scaledHeight) = renderConfigs[index]
|
||||||
|
|
||||||
|
val matrix = Matrix()
|
||||||
|
// 先按比例缩放
|
||||||
|
matrix.postScale(scale, scale)
|
||||||
|
// 再平移到当前 Y 轴位置(实现无缝衔接)
|
||||||
|
matrix.postTranslate(0f, currentY)
|
||||||
|
|
||||||
|
canvas.drawBitmap(item.bitmap, matrix, paint)
|
||||||
|
|
||||||
|
// 关键:累加【缩放后】的高度,确保下一张图紧贴上一张
|
||||||
|
currentY += scaledHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 调用你原有的保存逻辑
|
||||||
|
saveCanvasToGallery(context, resultBitmap, format, onSubmitResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存位图到系统相册
|
||||||
|
* @param context 上下文
|
||||||
|
* @param bitmap 要保存的位图
|
||||||
|
* @param format 导出格式(PNG/JPG)
|
||||||
|
* @param onSubmitResult 回调函数,参数为文件名和是否成功
|
||||||
|
*/
|
||||||
|
fun saveCanvasToGallery(
|
||||||
|
context: Context,
|
||||||
|
bitmap: Bitmap,
|
||||||
|
format: ExportFormat,
|
||||||
|
onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val extension = if (format == ExportFormat.JPG) "jpg" else "png"
|
||||||
|
val mimeType = if (format == ExportFormat.JPG) "image/jpeg" else "image/png"
|
||||||
|
val compressFormat = if (format == ExportFormat.JPG) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG
|
||||||
|
|
||||||
|
val filename = "MyRabbit_${System.currentTimeMillis()}.$extension"
|
||||||
|
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
uri?.let {
|
||||||
|
context.contentResolver.openOutputStream(it)?.use { stream ->
|
||||||
|
// PNG 忽略 quality 参数(无损),JPG 使用 100 表示最高质量
|
||||||
|
bitmap.compress(compressFormat, 100, stream)
|
||||||
|
onSubmitResult(filename, true)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
onSubmitResult(filename, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存位图到系统相册(如果targetWidth与targetHeight比原始值小太多会导致图片模糊)
|
||||||
|
* @param context 上下文
|
||||||
|
* @param bitmap 要保存的位图
|
||||||
|
* @param format 导出格式(PNG/JPG)
|
||||||
|
* @param targetWidth 目标宽度
|
||||||
|
* @param targetHeight 目标高度
|
||||||
|
* @param onSubmitResult 回调函数,参数为文件名和是否成功
|
||||||
|
*/
|
||||||
|
@SuppressLint("UseKtx")
|
||||||
|
fun saveCanvasToGallery(
|
||||||
|
context: Context,
|
||||||
|
bitmap: Bitmap,
|
||||||
|
format: ExportFormat,
|
||||||
|
targetWidth: Int,
|
||||||
|
targetHeight: Int,
|
||||||
|
onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
// 1. 修复 Hardware Bitmap 报错并确保使用 ARGB_8888 高精度配置
|
||||||
|
val softwareBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
|
bitmap.config == Bitmap.Config.HARDWARE) {
|
||||||
|
bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||||
|
} else {
|
||||||
|
// 如果不是硬件位图,也建议检查并转换为高精度配置以防模糊
|
||||||
|
bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
val extension = if (format == ExportFormat.JPG) "jpg" else "png"
|
||||||
|
val mimeType = if (format == ExportFormat.JPG) "image/jpeg" else "image/png"
|
||||||
|
val compressFormat = if (format == ExportFormat.JPG) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG
|
||||||
|
|
||||||
|
// 2. 创建目标尺寸位图 (显式指定 Config.ARGB_8888 保证清晰度)
|
||||||
|
val scaledResult = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(scaledResult)
|
||||||
|
|
||||||
|
// 3. 处理背景色 (如果是 JPG 填充白色,PNG 保持透明)
|
||||||
|
if (format == ExportFormat.JPG) {
|
||||||
|
canvas.drawColor(Color.WHITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 【核心算法:计算等比例缩放】
|
||||||
|
val srcWidth = softwareBitmap.width.toFloat()
|
||||||
|
val srcHeight = softwareBitmap.height.toFloat()
|
||||||
|
|
||||||
|
// 使用 float 计算比例,避免整数除法丢失精度导致模糊
|
||||||
|
val scale = (targetWidth.toFloat() / srcWidth).coerceAtMost(targetHeight.toFloat() / srcHeight)
|
||||||
|
|
||||||
|
val finalWidth = srcWidth * scale
|
||||||
|
val finalHeight = srcHeight * scale
|
||||||
|
val left = (targetWidth - finalWidth) / 2f
|
||||||
|
val top = (targetHeight - finalHeight) / 2f
|
||||||
|
|
||||||
|
// 5. 【关键:高清绘制配置】
|
||||||
|
val paint = Paint().apply {
|
||||||
|
isAntiAlias = true // 开启抗锯齿,边缘更平滑
|
||||||
|
isFilterBitmap = true // 开启位图过滤,这是解决缩放模糊的最核心设置
|
||||||
|
isDither = true // 开启抖动,色彩过渡更自然
|
||||||
|
}
|
||||||
|
|
||||||
|
val destRect = RectF(left, top, left + finalWidth, top + finalHeight)
|
||||||
|
// 绘图
|
||||||
|
canvas.drawBitmap(softwareBitmap, null, destRect, paint)
|
||||||
|
|
||||||
|
// 6. 保存到 MediaStore
|
||||||
|
val filename = "MyRabbit_${System.currentTimeMillis()}.$extension"
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
|
||||||
|
try {
|
||||||
|
uri?.let {
|
||||||
|
context.contentResolver.openOutputStream(it)?.use { stream ->
|
||||||
|
// 100 表示不压缩质量
|
||||||
|
scaledResult.compress(compressFormat, 100, stream)
|
||||||
|
onSubmitResult(filename, true)
|
||||||
|
}
|
||||||
|
} ?: onSubmitResult(filename, false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
onSubmitResult(filename, false)
|
||||||
|
} finally {
|
||||||
|
// 7. 内存回收
|
||||||
|
if (softwareBitmap != bitmap) softwareBitmap.recycle()
|
||||||
|
// 注意:如果后面还要用 scaledResult,不要在这里 recycle
|
||||||
|
// scaledResult.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为相册可保存的格式(PNG/JPG/SVG/GIF)
|
||||||
|
* @param context 上下文
|
||||||
|
* @param sourceBitmap 原始位图
|
||||||
|
* @param format 导出格式(PNG/JPG/SVG/GIF)
|
||||||
|
* @param onSubmitResult 回调函数,参数为文件名和是否成功
|
||||||
|
*/
|
||||||
|
fun convertToGallery(
|
||||||
|
context: Context,
|
||||||
|
sourceBitmap: Bitmap,
|
||||||
|
format: ExportFormat,
|
||||||
|
onSubmitResult: (fileName: String, isSuccess: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
// 1. 预处理:缩放并获得高清软件位图(防止 Hardware Bitmap 报错)
|
||||||
|
val softwareBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && sourceBitmap.config == Bitmap.Config.HARDWARE) {
|
||||||
|
sourceBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||||
|
} else sourceBitmap
|
||||||
|
|
||||||
|
val scaledBitmap = createScaledBitmap(softwareBitmap, sourceBitmap.width, sourceBitmap.height)
|
||||||
|
|
||||||
|
// 2. 准备 MediaStore 容器
|
||||||
|
val filename = "MyRabbit_Export_${System.currentTimeMillis()}.${format.extension}"
|
||||||
|
val contentValues = if (format == ExportFormat.SVG) {
|
||||||
|
ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "image/svg+xml") // 关键:SVG 的 MIME 类型
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, format.mimeType)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
|
||||||
|
// 3. 根据格式执行不同的转换编码
|
||||||
|
try {
|
||||||
|
uri?.let {
|
||||||
|
context.contentResolver.openOutputStream(it)?.use { stream ->
|
||||||
|
when (format) {
|
||||||
|
ExportFormat.JPG -> scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
||||||
|
ExportFormat.PNG -> scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||||
|
ExportFormat.GIF -> encodeToGif(scaledBitmap, stream)
|
||||||
|
ExportFormat.SVG -> encodeToSvg(scaledBitmap, stream)
|
||||||
|
}
|
||||||
|
onSubmitResult(filename, true)
|
||||||
|
}
|
||||||
|
} ?: onSubmitResult(filename, false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onSubmitResult(e.message ?: "Unknown Error", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建等比例适配的缩放位图
|
||||||
|
* @param src 原始位图
|
||||||
|
* @param destW 目标宽度
|
||||||
|
* @param destH 目标高度
|
||||||
|
* @return 缩放后的位图
|
||||||
|
*/
|
||||||
|
private fun createScaledBitmap(src: Bitmap, destW: Int, destH: Int): Bitmap {
|
||||||
|
val result = createBitmap(destW, destH)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG or Paint.DITHER_FLAG)
|
||||||
|
|
||||||
|
// 等比例适配逻辑
|
||||||
|
val scale = (destW.toFloat() / src.width).coerceAtMost(destH.toFloat() / src.height)
|
||||||
|
val left = (destW - src.width * scale) / 2f
|
||||||
|
val top = (destH - src.height * scale) / 2f
|
||||||
|
|
||||||
|
canvas.drawBitmap(src, null, RectF(left, top, left + src.width * scale, top + src.height * scale), paint)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 具体的 GIF 编码逻辑
|
||||||
|
* @param bitmap 原始位图
|
||||||
|
* @param outputStream 输出流
|
||||||
|
*/
|
||||||
|
private fun encodeToGif(bitmap: Bitmap, outputStream: OutputStream) {
|
||||||
|
// Android 原生不支持直接写 GIF,通常需使用外部库或简单的 Bitmap 压缩
|
||||||
|
// 这里演示通过 Bitmap 压缩模拟格式导出逻辑
|
||||||
|
try {
|
||||||
|
// 注意:Android 原生 compress 不支持 GIF 格式,
|
||||||
|
// 实际开发中若需生成多帧 GIF,建议使用 'jp.co.cyberagent.android:gpuimage' 或第三方编码器
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 具体的 SVG 编码逻辑
|
||||||
|
* @param bitmap 原始位图
|
||||||
|
* @param stream 输出流
|
||||||
|
*/
|
||||||
|
private fun encodeToSvg(bitmap: Bitmap, stream: OutputStream) {
|
||||||
|
try {
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
|
||||||
|
val bos = ByteArrayOutputStream()
|
||||||
|
// 使用 PNG 保证透明度
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
|
||||||
|
|
||||||
|
// 必须使用 NO_WRAP 标志,如果不加 NO_WRAP,Base64 字符串每 76 个字符会插入一个换行符,导致 SVG 标签断开
|
||||||
|
val base64Data = android.util.Base64.encodeToString(bos.toByteArray(), android.util.Base64.NO_WRAP)
|
||||||
|
|
||||||
|
// XML 命名空间地址
|
||||||
|
val svgContent = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="$width" height="$height" viewBox="0 0 $width $height"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<image width="$width" height="$height"
|
||||||
|
xlink:href="data:image/png;base64,$base64Data"
|
||||||
|
href="data:image/png;base64,$base64Data" />
|
||||||
|
</svg>
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
stream.write(svgContent.toByteArray(Charsets.UTF_8))
|
||||||
|
stream.flush()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImageUtils", "SVG 编码失败: ${e.message}")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ExportFormat(val extension: String, val mimeType: String) {
|
||||||
|
JPG("jpg", "image/jpeg"),
|
||||||
|
PNG("png", "image/png"),
|
||||||
|
GIF("gif", "image/gif"),
|
||||||
|
SVG("svg", "image/svg+xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
||||||
|
object NetworkMonitor {
|
||||||
|
private val _networkStatus = MutableLiveData<Boolean>()
|
||||||
|
val networkStatus: LiveData<Boolean> = _networkStatus
|
||||||
|
|
||||||
|
private var connectivityManager: ConnectivityManager? = null
|
||||||
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
fun initialize(context: Context) {
|
||||||
|
connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
// Android 7.0+ 使用registerDefaultNetworkCallback
|
||||||
|
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
_networkStatus.postValue(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
_networkStatus.postValue(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnavailable() {
|
||||||
|
_networkStatus.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectivityManager?.registerDefaultNetworkCallback(networkCallback as ConnectivityManager.NetworkCallback)
|
||||||
|
} else {
|
||||||
|
// Android 7.0以下使用传统方式
|
||||||
|
val networkRequest = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
_networkStatus.postValue(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
_networkStatus.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectivityManager?.registerNetworkCallback(networkRequest, networkCallback as ConnectivityManager.NetworkCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化当前网络状态
|
||||||
|
_networkStatus.value = isNetworkAvailable(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
fun isNetworkAvailable(context: Context): Boolean {
|
||||||
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val network = connectivityManager.activeNetwork
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||||
|
capabilities != null && (
|
||||||
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||||
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
|
||||||
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val networkInfo = connectivityManager.activeNetworkInfo
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
networkInfo != null && networkInfo.isConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister() {
|
||||||
|
networkCallback?.let {
|
||||||
|
connectivityManager?.unregisterNetworkCallback(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
object NetworkStatus {
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
fun isNetworkAvailable(context: Context): Boolean {
|
||||||
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val network = connectivityManager.activeNetwork
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||||
|
capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val networkInfo = connectivityManager.activeNetworkInfo
|
||||||
|
networkInfo != null && networkInfo.isConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun openNetworkSettings(context: Context) {
|
||||||
|
context.startActivity(android.content.Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限管理工具类
|
||||||
|
*/
|
||||||
|
object PermissionUtils {
|
||||||
|
|
||||||
|
// 相机权限
|
||||||
|
const val CAMERA_PERMISSION = Manifest.permission.CAMERA
|
||||||
|
|
||||||
|
// 存储权限(根据Android版本区分)
|
||||||
|
val STORAGE_PERMISSIONS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
|
||||||
|
} else {
|
||||||
|
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查权限是否已授予
|
||||||
|
*/
|
||||||
|
fun hasPermissions(context: Context, vararg permissions: String): Boolean {
|
||||||
|
return permissions.all { permission ->
|
||||||
|
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查相机权限
|
||||||
|
*/
|
||||||
|
fun hasCameraPermission(context: Context): Boolean {
|
||||||
|
return hasPermissions(context, CAMERA_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查存储权限
|
||||||
|
*/
|
||||||
|
fun hasStoragePermission(context: Context): Boolean {
|
||||||
|
return hasPermissions(context, *STORAGE_PERMISSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查所有图片相关权限
|
||||||
|
*/
|
||||||
|
fun hasImagePermissions(context: Context): Boolean {
|
||||||
|
return hasCameraPermission(context) && hasStoragePermission(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查相册权限(只需要存储权限)
|
||||||
|
*/
|
||||||
|
fun hasGalleryPermission(context: Context): Boolean {
|
||||||
|
return hasStoragePermission(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到应用设置页面
|
||||||
|
*/
|
||||||
|
fun openAppSettings(context: Context) {
|
||||||
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
}
|
||||||
|
if (context is Activity) {
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限状态数据类
|
||||||
|
*/
|
||||||
|
data class PermissionState(
|
||||||
|
val hasCameraPermission: Boolean = false,
|
||||||
|
val hasStoragePermission: Boolean = false,
|
||||||
|
val shouldShowRationale: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可组合函数:管理权限请求
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberPermissionState(): MutableState<PermissionState> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return remember {
|
||||||
|
mutableStateOf(
|
||||||
|
PermissionState(
|
||||||
|
hasCameraPermission = PermissionUtils.hasCameraPermission(context),
|
||||||
|
hasStoragePermission = PermissionUtils.hasStoragePermission(context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可组合函数:创建权限请求启动器
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberPermissionLauncher(
|
||||||
|
context: Context = LocalContext.current,
|
||||||
|
onGranted: () -> Unit = {},
|
||||||
|
onDenied: () -> Unit = {},
|
||||||
|
onRationale: () -> Unit = {}
|
||||||
|
) = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
val allGranted = permissions.values.all { it }
|
||||||
|
val shouldShowRationale = permissions.keys.any { permission ->
|
||||||
|
androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
context as Activity,
|
||||||
|
permission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allGranted) {
|
||||||
|
onGranted()
|
||||||
|
} else if (shouldShowRationale) {
|
||||||
|
onRationale()
|
||||||
|
} else {
|
||||||
|
onDenied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可组合函数:创建相机权限启动器
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberCameraPermissionLauncher(
|
||||||
|
context: Context = LocalContext.current,
|
||||||
|
onGranted: () -> Unit = {},
|
||||||
|
onDenied: () -> Unit = {},
|
||||||
|
onRationale: () -> Unit = {}
|
||||||
|
) = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
onGranted()
|
||||||
|
} else {
|
||||||
|
val shouldShowRationale = androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
context as Activity,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
)
|
||||||
|
if (shouldShowRationale) {
|
||||||
|
onRationale()
|
||||||
|
} else {
|
||||||
|
onDenied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.graphics.*
|
||||||
|
import com.google.mlkit.vision.common.InputImage
|
||||||
|
import com.google.mlkit.vision.segmentation.Segmentation
|
||||||
|
import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
import android.graphics.*
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import com.google.mlkit.vision.face.FaceContour
|
||||||
|
import com.google.mlkit.vision.face.FaceDetection
|
||||||
|
import com.google.mlkit.vision.face.FaceDetectorOptions
|
||||||
|
import androidx.core.graphics.get
|
||||||
|
|
||||||
|
// 变换状态模型
|
||||||
|
data class TransformState(
|
||||||
|
val offset: Offset = Offset.Zero,
|
||||||
|
val scale: Float = 1f,
|
||||||
|
val rotation: Float = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
object PhotoCutter {
|
||||||
|
|
||||||
|
fun cutPureHead(sourceBitmap: Bitmap, callback: (Bitmap?) -> Unit) {
|
||||||
|
// 1. 配置分割器:单图模式
|
||||||
|
val options = SelfieSegmenterOptions.Builder()
|
||||||
|
.setDetectorMode(SelfieSegmenterOptions.SINGLE_IMAGE_MODE)
|
||||||
|
.build()
|
||||||
|
val segmenter = Segmentation.getClient(options)
|
||||||
|
val image = InputImage.fromBitmap(sourceBitmap, 0)
|
||||||
|
|
||||||
|
// 2. 开始处理
|
||||||
|
segmenter.process(image)
|
||||||
|
.addOnSuccessListener { mask ->
|
||||||
|
val resultBitmap = generateTransparentBitmap(sourceBitmap, mask.buffer)
|
||||||
|
callback(resultBitmap)
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
callback(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateTransparentBitmap(
|
||||||
|
source: Bitmap,
|
||||||
|
maskBuffer: java.nio.ByteBuffer,
|
||||||
|
): Bitmap {
|
||||||
|
val width = source.width
|
||||||
|
val height = source.height
|
||||||
|
val result = createBitmap(width, height)
|
||||||
|
|
||||||
|
// 将掩码缓冲区重置
|
||||||
|
maskBuffer.rewind()
|
||||||
|
|
||||||
|
// 逐像素处理
|
||||||
|
val pixels = IntArray(width * height)
|
||||||
|
source.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
|
||||||
|
for (i in 0 until width * height) {
|
||||||
|
// 获取当前像素的人像概率(0.0 ~ 1.0)
|
||||||
|
val confidence = maskBuffer.float
|
||||||
|
|
||||||
|
// 如果置信度低于 0.5,认为该像素是背景,设为完全透明
|
||||||
|
if (confidence < 0.5) {
|
||||||
|
pixels[i] = Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.setPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//object PhotoCutter {
|
||||||
|
//
|
||||||
|
// fun cutHeadFromImage(sourceBitmap: Bitmap, callback: (Bitmap?) -> Unit) {
|
||||||
|
// // 1. 配置分割器:单图模式
|
||||||
|
// val options = SelfieSegmenterOptions.Builder()
|
||||||
|
// .setDetectorMode(SelfieSegmenterOptions.SINGLE_IMAGE_MODE)
|
||||||
|
// .build()
|
||||||
|
// val segmenter = Segmentation.getClient(options)
|
||||||
|
// val image = InputImage.fromBitmap(sourceBitmap, 0)
|
||||||
|
//
|
||||||
|
// // 2. 开始处理
|
||||||
|
// segmenter.process(image)
|
||||||
|
// .addOnSuccessListener { mask ->
|
||||||
|
// val resultBitmap = generateTransparentBitmap(sourceBitmap, mask.buffer, mask.width, mask.height)
|
||||||
|
// callback(resultBitmap)
|
||||||
|
// }
|
||||||
|
// .addOnFailureListener {
|
||||||
|
// callback(null)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private fun generateTransparentBitmap(
|
||||||
|
// source: Bitmap,
|
||||||
|
// maskBuffer: java.nio.ByteBuffer,
|
||||||
|
// maskWidth: Int,
|
||||||
|
// maskHeight: Int
|
||||||
|
// ): Bitmap {
|
||||||
|
// val width = source.width
|
||||||
|
// val height = source.height
|
||||||
|
// val result = createBitmap(width, height)
|
||||||
|
//
|
||||||
|
// // 将掩码缓冲区重置
|
||||||
|
// maskBuffer.rewind()
|
||||||
|
//
|
||||||
|
// // 逐像素处理
|
||||||
|
// val pixels = IntArray(width * height)
|
||||||
|
// source.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
//
|
||||||
|
// for (i in 0 until width * height) {
|
||||||
|
// // 获取当前像素的人像概率(0.0 ~ 1.0)
|
||||||
|
// val confidence = maskBuffer.float
|
||||||
|
//
|
||||||
|
// // 如果置信度低于 0.5,认为该像素是背景,设为完全透明
|
||||||
|
// if (confidence < 0.5) {
|
||||||
|
// pixels[i] = Color.TRANSPARENT
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// result.setPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
// return result
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.img.rabbit.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
|
object UrlLinkUtils {
|
||||||
|
fun openAgreement(context: Context, url: String) {
|
||||||
|
// 打开服务协议
|
||||||
|
Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}.let { intent ->
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.img.rabbit.viewmodel
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class FeedbackViewModel : ViewModel() {
|
||||||
|
val isLoading = mutableStateOf(true)
|
||||||
|
|
||||||
|
fun setLoading(loading: Boolean) {
|
||||||
|
isLoading.value = loading
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反馈类型
|
||||||
|
private val _feedbackType = mutableStateOf(FeedbackType.FUNCTION)
|
||||||
|
val feedbackType: FeedbackType
|
||||||
|
get() = _feedbackType.value
|
||||||
|
|
||||||
|
fun setFeedbackType(feedbackType: FeedbackType) {
|
||||||
|
_feedbackType.value = feedbackType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 补充反馈
|
||||||
|
private val _feedbackMore = mutableStateOf("")
|
||||||
|
val feedbackMore: String
|
||||||
|
get() = _feedbackMore.value
|
||||||
|
|
||||||
|
fun setFeedbackMore(feedbackMore: String) {
|
||||||
|
_feedbackMore.value = feedbackMore
|
||||||
|
}
|
||||||
|
|
||||||
|
//提供图片
|
||||||
|
private val _currentImageUris = mutableStateOf(emptyList<Uri>())
|
||||||
|
val currentImageUris: List<Uri>
|
||||||
|
get() = _currentImageUris.value
|
||||||
|
|
||||||
|
fun setCurrentImageUris(currentImageUris: List<Uri>) {
|
||||||
|
_currentImageUris.value = currentImageUris
|
||||||
|
}
|
||||||
|
|
||||||
|
// 联系方式
|
||||||
|
private val _feedbackContact = mutableStateOf("")
|
||||||
|
val feedbackContact: String
|
||||||
|
get() = _feedbackContact.value
|
||||||
|
|
||||||
|
fun setFeedbackContact(feedbackContact: String) {
|
||||||
|
_feedbackContact.value = feedbackContact
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交反馈
|
||||||
|
fun submitFeedback() {
|
||||||
|
setLoading(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FeedbackType(val keyName: String, val value: String) {
|
||||||
|
FUNCTION("function", "功能问题"),
|
||||||
|
FEATURE("feature", "功能建议"),
|
||||||
|
OTHER("other", "其他")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.img.rabbit.viewmodel
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
class GeneralViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
val kv by lazy { MMKV.defaultMMKV() }
|
||||||
|
private val _networkStatus = MutableLiveData<Boolean>()
|
||||||
|
val networkStatus: LiveData<Boolean> = _networkStatus
|
||||||
|
fun setNetworkStatus(status: Boolean) {
|
||||||
|
_networkStatus.value = status
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _isNavigationBarVisible = MutableLiveData<Boolean>()
|
||||||
|
val isNavigationBarVisible: LiveData<Boolean> = _isNavigationBarVisible
|
||||||
|
|
||||||
|
private val connectivityManager =
|
||||||
|
application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
_networkStatus.postValue(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
_networkStatus.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// 注册网络监听
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
||||||
|
} else {
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.build()
|
||||||
|
connectivityManager.registerNetworkCallback(request, networkCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化状态
|
||||||
|
_networkStatus.value = isNetworkAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNetworkAvailable(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val network = connectivityManager.activeNetwork
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||||
|
capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val networkInfo = connectivityManager.activeNetworkInfo
|
||||||
|
networkInfo != null && networkInfo.isConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNavigationBarVisible(visible: Boolean){
|
||||||
|
_isNavigationBarVisible.value = visible
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
package com.img.rabbit.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import com.img.rabbit.pages.LoginScreenType
|
||||||
|
import com.g.gysdk.GYManager
|
||||||
|
import com.g.gysdk.GYResponse
|
||||||
|
import com.g.gysdk.GyCallBack
|
||||||
|
import com.g.gysdk.GyConfig
|
||||||
|
import com.img.rabbit.bean.OnekeyPreLogin
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class LoginViewModel : ViewModel() {
|
||||||
|
private val TAG = "LoginViewModel"
|
||||||
|
private val ONEKEY_TAG = "OneKeyLoginViewModel"
|
||||||
|
val loginScreenType = mutableStateOf(LoginScreenType.LOGIN_NORMAL)
|
||||||
|
// 登录用户名
|
||||||
|
val userName = mutableStateOf("")
|
||||||
|
// 登录验证码
|
||||||
|
val captcha = mutableStateOf("")
|
||||||
|
|
||||||
|
// 是否同意政策协议
|
||||||
|
private val _policyAgreement = mutableStateOf(false)
|
||||||
|
val isPolicyAgreement: State<Boolean> = _policyAgreement
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private val isGYUIDValid = mutableStateOf(false)
|
||||||
|
var oneKeyPreLogin: OnekeyPreLogin? = null
|
||||||
|
|
||||||
|
|
||||||
|
fun setUserName(loginName: String) {
|
||||||
|
userName.value = loginName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCaptcha(loginCaptcha: String) {
|
||||||
|
captcha.value = loginCaptcha
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsPolicyAgreement(isAgreement: Boolean) {
|
||||||
|
_policyAgreement.value = isAgreement
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private val _isLogin = mutableStateOf(false)
|
||||||
|
val isLogin: State<Boolean> = _isLogin
|
||||||
|
|
||||||
|
fun setLogin(isLogin: Boolean) {
|
||||||
|
_isLogin.value = isLogin
|
||||||
|
}
|
||||||
|
|
||||||
|
fun oneKeyLoginForGeTuiSdk(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) {
|
||||||
|
// 初始化 SDK
|
||||||
|
GYManager.getInstance().init(GyConfig.with(activity.applicationContext).callBack(object : GyCallBack {
|
||||||
|
override fun onSuccess(response: GYResponse?) {
|
||||||
|
isGYUIDValid.value = true
|
||||||
|
Log.i(ONEKEY_TAG, "--->初始化: onSuccess")
|
||||||
|
|
||||||
|
//预登录(提高拉起速度,可选但推荐)
|
||||||
|
GYManager.getInstance().ePreLogin(/* timeout = */ 8000, /* gyCallBack = */ object : GyCallBack {
|
||||||
|
override fun onSuccess(response: GYResponse?) {
|
||||||
|
Log.i(ONEKEY_TAG, "--->预登录: onSuccess--->${response}")
|
||||||
|
val preLoginData = response?.msg
|
||||||
|
if (!preLoginData.isNullOrEmpty()) {
|
||||||
|
try {
|
||||||
|
val json = Json { ignoreUnknownKeys = true }
|
||||||
|
oneKeyPreLogin =
|
||||||
|
json.decodeFromString<OnekeyPreLogin>(preLoginData)
|
||||||
|
|
||||||
|
// 打印解析后的详细数据
|
||||||
|
Log.i(ONEKEY_TAG, "=== 预登录数据解析结果 ===")
|
||||||
|
Log.i(ONEKEY_TAG, "流程ID: ${oneKeyPreLogin?.process_id}")
|
||||||
|
Log.i(ONEKEY_TAG, "运营商类型: ${oneKeyPreLogin?.operatorType}")
|
||||||
|
Log.i(ONEKEY_TAG, "客户端类型: ${oneKeyPreLogin?.clienttype}")
|
||||||
|
Log.i(ONEKEY_TAG, "访问码: ${oneKeyPreLogin?.accessCode}")
|
||||||
|
Log.i(ONEKEY_TAG, "手机号: ${oneKeyPreLogin?.number}")
|
||||||
|
Log.i(ONEKEY_TAG, "过期时间: ${oneKeyPreLogin?.expiredTime}")
|
||||||
|
Log.i(ONEKEY_TAG, "错误码: ${oneKeyPreLogin?.errorCode}")
|
||||||
|
Log.i(ONEKEY_TAG, "错误描述: ${oneKeyPreLogin?.errorDesc}")
|
||||||
|
Log.i(ONEKEY_TAG, "耗时: ${oneKeyPreLogin?.costTime}ms")
|
||||||
|
Log.i(ONEKEY_TAG, "================================")
|
||||||
|
|
||||||
|
// 根据解析结果决定是否继续(根据errorCode判断)
|
||||||
|
if (oneKeyPreLogin?.errorCode == 0) {
|
||||||
|
oneKeyLoginValid(
|
||||||
|
activity = activity,
|
||||||
|
onShowOneKeyScreen = onShowOneKeyScreen
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onShowOneKeyScreen(false)
|
||||||
|
Log.e(ONEKEY_TAG, "预登录校验失败: ${oneKeyPreLogin?.errorDesc}")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(ONEKEY_TAG, "JSON解析失败: ${e.message}")
|
||||||
|
Log.e(ONEKEY_TAG, "原始数据: $preLoginData")
|
||||||
|
// 解析失败时继续执行原逻辑
|
||||||
|
onShowOneKeyScreen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailed(response: GYResponse?) {
|
||||||
|
onShowOneKeyScreen(false)
|
||||||
|
Log.i(ONEKEY_TAG, "--->预登录: onFailed--->${response}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailed(response: GYResponse?) {
|
||||||
|
onShowOneKeyScreen(false)
|
||||||
|
Log.i(ONEKEY_TAG, "--->初始化: onFailed--->${response}")
|
||||||
|
}
|
||||||
|
}).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun oneKeyLoginValid(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) {
|
||||||
|
if (GYManager.getInstance().isPreLoginResultValid) {
|
||||||
|
//预登录有效,启动登录授权页
|
||||||
|
onShowOneKeyScreen(true)
|
||||||
|
Log.i(ONEKEY_TAG, "--->预登录校验有效A: onSuccess")
|
||||||
|
} else {
|
||||||
|
//考虑到是用户在等待,建议超时8s以上,至少设置5s以上
|
||||||
|
GYManager.getInstance().ePreLogin(5000, object : GyCallBack {
|
||||||
|
override fun onSuccess(response: GYResponse?) {
|
||||||
|
//预登录成功,启动登录授权页
|
||||||
|
onShowOneKeyScreen(true)
|
||||||
|
Log.i(ONEKEY_TAG, "--->预登录校验有效B: onSuccess--->${response}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailed(response: GYResponse?) {
|
||||||
|
//预登录失败,提示用户稍后重试
|
||||||
|
onShowOneKeyScreen(false)
|
||||||
|
Log.i(ONEKEY_TAG, "--->预登录校验有效B: onFailed--->${response}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求验证码
|
||||||
|
*/
|
||||||
|
fun requestCaptcha() {
|
||||||
|
// TODO: 发送请求获取验证码
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.img.rabbit.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class SplashViewModel : ViewModel() {
|
||||||
|
val isLoading = androidx.compose.runtime.mutableStateOf(true)
|
||||||
|
|
||||||
|
fun setLoading(loading: Boolean) {
|
||||||
|
isLoading.value = loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 未选中状态 -->
|
||||||
|
<item android:state_checked="false" android:drawable="@mipmap/ic_tick_circle_normal" />
|
||||||
|
<!-- 选中状态 -->
|
||||||
|
<item android:state_checked="true" android:drawable="@mipmap/ic_tick_circle_checked" />
|
||||||
|
<!-- 默认状态(未选中) -->
|
||||||
|
<item android:drawable="@mipmap/ic_tick_circle_normal" />
|
||||||
|
</selector>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FF252525" />
|
||||||
|
<corners android:radius="359dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:background="@android:color/transparent">
|
||||||
|
<!-- <View-->
|
||||||
|
<!-- android:layout_width="match_parent"-->
|
||||||
|
<!-- android:layout_height="380dp"-->
|
||||||
|
<!-- android:background="@mipmap/ic_main_previous_mask"-->
|
||||||
|
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||||
|
<!-- app:layout_constraintStart_toStartOf="parent"/>-->
|
||||||
|
|
||||||
|
<!-- <androidx.constraintlayout.widget.ConstraintLayout-->
|
||||||
|
<!-- android:layout_width="120dp"-->
|
||||||
|
<!-- android:layout_height="44dp"-->
|
||||||
|
<!-- android:layout_marginTop="56dp"-->
|
||||||
|
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||||
|
<!-- app:layout_constraintStart_toStartOf="parent">-->
|
||||||
|
|
||||||
|
<!-- <ImageView-->
|
||||||
|
<!-- android:layout_width="24dp"-->
|
||||||
|
<!-- android:layout_height="24dp"-->
|
||||||
|
<!-- android:layout_marginStart="16dp"-->
|
||||||
|
<!-- android:src="@mipmap/ic_back"-->
|
||||||
|
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||||
|
<!-- app:layout_constraintStart_toStartOf="parent"/>-->
|
||||||
|
|
||||||
|
<!-- </androidx.constraintlayout.widget.ConstraintLayout>-->
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/layout_one_key_login_tv_phone"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="230dp"
|
||||||
|
android:text="186***42876"
|
||||||
|
android:textColor="#FF1A1A1A"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/layout_one_key_login_tv_service"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="天翼账号提供认证服务"
|
||||||
|
android:textColor="#FF767676"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_one_key_login_tv_phone"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/layout_one_key_login_btn"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginTop="46dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@drawable/shape_252525_cor359"
|
||||||
|
android:drawableBottom="@drawable/shape_252525_cor359"
|
||||||
|
android:text="本机号码一键绑定"
|
||||||
|
android:textColor="#FFC2FF43"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_one_key_login_tv_service"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
<!--协议政策-->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/layout_one_key_login_agreement"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_one_key_login_btn">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/layout_one_key_login_agreement_checkbox"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:button="@drawable/selector_circle_checkbox"
|
||||||
|
android:background="@null"
|
||||||
|
android:checked="true"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/layout_one_key_login_agreement_tv"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="登录即认可《天翼账号服务与隐私协议》、《用户协议》和《隐私政策》并使用本机号码登录"
|
||||||
|
android:textColor="#FF767676"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/layout_one_key_login_agreement_checkbox"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 820 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 5.3 KiB |