init:项目初始化,具备完整UI和主体功能
This commit is contained in:
shenzuqiang 2026-02-12 18:26:02 +08:00
commit 154ad548bc
171 changed files with 11066 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -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

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
rabbit

View File

@ -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>

View File

@ -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>

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -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>

13
.idea/deviceManager.xml Normal file
View File

@ -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>

19
.idea/gradle.xml Normal file
View File

@ -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>

View File

@ -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>

10
.idea/migrations.xml Normal file
View File

@ -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>

9
.idea/misc.xml Normal file
View File

@ -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>

View File

@ -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>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

BIN
app/bidinfo.keystore Normal file

Binary file not shown.

155
app/build.gradle.kts Normal file
View File

@ -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")
}

34
app/proguard-rules.pro vendored Normal file
View File

@ -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);
}
# 友盟号码认证 结束

BIN
app/release/app-release.apk Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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")
}
}

View File

@ -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())
}
}

View File

@ -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
)

View File

@ -0,0 +1,7 @@
package com.img.rabbit.bean
data class FormatBean(
//格式id
val id: Int,
val title: String
)

View File

@ -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
)

View File

@ -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() // 显示高度(像素)
)

View File

@ -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
)

View File

@ -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
)

View File

@ -0,0 +1,7 @@
package com.img.rabbit.bean
data class UserInfo(
val id: Int,
val name: String,
val login: Boolean,
)

File diff suppressed because it is too large Load Diff

View File

@ -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 }

View File

@ -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"
}

View File

@ -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"))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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 }
}
}
}
}
}
}

View File

@ -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 = {})
}

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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,
)
}
}
}
}
}

View File

@ -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))
}

View File

@ -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,
)
}
}
}
}
}

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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 = {})
}

View File

@ -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())
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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 }
}
}
}
}

View File

@ -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 = "未选择图片"))
}
}

View File

@ -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_WRAPBase64 字符串每 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")
}

View File

@ -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)
}
}
}

View File

@ -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))
}
}

View File

@ -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()
}
}
}

View File

@ -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
// }
//}

View File

@ -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)
}
}
}

View File

@ -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", "其他")
}
}

View File

@ -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)
}
}

View File

@ -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: 发送请求获取验证码
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Some files were not shown because too many files have changed in this diff Show More