上传Android项目
15
.gitignore
vendored
Normal 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
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
Key of Love
|
||||
6
.idea/AndroidProjectSystem.xml
generated
Normal 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>
|
||||
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/deploymentTargetSelector.xml
generated
Normal 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
generated
Normal 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
generated
Normal 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>
|
||||
61
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
generated
Normal 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
generated
Normal 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>
|
||||
17
.idea/runConfigurations.xml
generated
Normal 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>
|
||||
4
.kotlin/errors/errors-1762946548552.log
Normal file
@@ -0,0 +1,4 @@
|
||||
kotlin version: 2.0.21
|
||||
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||
1. Kotlin compile daemon is ready
|
||||
|
||||
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
70
app/build.gradle.kts
Normal file
@@ -0,0 +1,70 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.myapplication"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.boshan.key.of.love"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.core:core:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.8.0")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.8.0")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("de.hdodenhof:circleimageview:3.1.0")
|
||||
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||
|
||||
// Jetpack Compose
|
||||
implementation("androidx.activity:activity-compose:1.8.0")
|
||||
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.navigation:navigation-compose:2.7.5")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.myapplication
|
||||
|
||||
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.example.myapplication", appContext.packageName)
|
||||
}
|
||||
}
|
||||
50
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MyApplication">
|
||||
|
||||
<!-- 启动页 Activity -->
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MyApp.Splash">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- 引导页 -->
|
||||
<activity
|
||||
android:name=".OnboardingActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<!-- 主界面 -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
|
||||
<!-- 输入法服务 -->
|
||||
<service
|
||||
android:name=".MyInputMethodService"
|
||||
android:label="@string/app_name"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD"
|
||||
android:foregroundServiceType="connectedDevice">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.view.im"
|
||||
android:resource="@xml/method" />
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
app/src/main/assets/bi_cols.bin
Normal file
BIN
app/src/main/assets/bi_logp.bin
Normal file
BIN
app/src/main/assets/bi_rowptr.bin
Normal file
BIN
app/src/main/assets/uni_logp.bin
Normal file
50000
app/src/main/assets/vocab.txt
Normal file
158
app/src/main/java/com/example/myapplication/BigramPredictor.kt
Normal file
@@ -0,0 +1,158 @@
|
||||
package com.example.myapplication.data
|
||||
|
||||
import android.content.Context
|
||||
import com.example.myapplication.Trie
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.PriorityQueue
|
||||
import kotlin.math.max
|
||||
|
||||
class BigramPredictor(
|
||||
private val context: Context,
|
||||
private val trie: Trie
|
||||
) {
|
||||
@Volatile private var model: BigramModel? = null
|
||||
|
||||
private val loading = AtomicBoolean(false)
|
||||
|
||||
// 词 ↔ id 映射
|
||||
@Volatile private var word2id: Map<String, Int> = emptyMap()
|
||||
|
||||
@Volatile private var id2word: List<String> = emptyList()
|
||||
|
||||
//预先加载语言模型,并构建词到ID和ID到词的双向映射。
|
||||
fun preload() {
|
||||
if (!loading.compareAndSet(false, true)) return
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val m = LanguageModelLoader.load(context)
|
||||
|
||||
model = m
|
||||
|
||||
// 建索引(vocab 与 bigram 索引对齐,注意不丢前三个符号)
|
||||
val map = HashMap<String, Int>(m.vocab.size * 2)
|
||||
|
||||
m.vocab.forEachIndexed { idx, w -> map[w] = idx }
|
||||
|
||||
word2id = map
|
||||
|
||||
id2word = m.vocab
|
||||
} catch (_: Throwable) {
|
||||
// 保持静默,允许无模型运行(仅 Trie 起作用)
|
||||
} finally {
|
||||
loading.set(false)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// 模型是否已准备好
|
||||
fun isReady(): Boolean = model != null
|
||||
|
||||
//基于上文 lastWord(可空)与前缀 prefix 联想,优先:bigram 条件概率 → Trie 过滤 → Top-K,兜底:unigram Top-K(同样做 Trie 过滤)
|
||||
fun suggest(prefix: String, lastWord: String?, topK: Int = 10): List<String> {
|
||||
val m = model
|
||||
|
||||
val pfx = prefix.trim()
|
||||
|
||||
if (m == null) {
|
||||
// 模型未载入时,纯 Trie 前缀联想(你的 Trie 应提供类似 startsWith)
|
||||
return safeTriePrefix(pfx, topK)
|
||||
}
|
||||
|
||||
val candidates = mutableListOf<Pair<String, Float>>()
|
||||
|
||||
val lastId = lastWord?.let { word2id[it] }
|
||||
|
||||
if (lastId != null) {
|
||||
// 1) bigram 邻域
|
||||
val start = m.biRowptr[lastId]
|
||||
|
||||
val end = m.biRowptr[lastId + 1]
|
||||
|
||||
if (start in 0..end && end <= m.biCols.size) {
|
||||
// 先把 bigram 候选过一遍前缀过滤
|
||||
for (i in start until end) {
|
||||
val nextId = m.biCols[i]
|
||||
|
||||
val w = m.vocab[nextId]
|
||||
if (pfx.isEmpty() || w.startsWith(pfx, ignoreCase = true)) {
|
||||
val score = m.biLogp[i] // logP(next|last)
|
||||
|
||||
candidates += w to score
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 如果有 bigram 过滤后的候选,直接取 topK
|
||||
if (candidates.isNotEmpty()) {
|
||||
return topKByScore(candidates, topK)
|
||||
}
|
||||
|
||||
// 3) 兜底:用 unigram + 前缀过滤
|
||||
val heap = topKHeap(topK)
|
||||
|
||||
for (i in m.vocab.indices) {
|
||||
val w = m.vocab[i]
|
||||
|
||||
if (pfx.isEmpty() || w.startsWith(pfx, ignoreCase = true)) {
|
||||
heap.offer(w to m.uniLogp[i])
|
||||
|
||||
if (heap.size > topK) heap.poll()
|
||||
}
|
||||
}
|
||||
return heap.toSortedListDescending()
|
||||
}
|
||||
|
||||
//供上层在用户选中词时更新“上文”状态
|
||||
fun normalizeWordForContext(word: String): String? {
|
||||
// 你可以在这里做大小写/符号处理,或将 OOV 映射为 <unk>
|
||||
return if (word2id.containsKey(word)) word else "<unk>"
|
||||
}
|
||||
|
||||
//在Trie数据结构中查找与给定前缀匹配的字符串,并返回其中评分最高的topK个结果。
|
||||
private fun safeTriePrefix(prefix: String, topK: Int): List<String> {
|
||||
if (prefix.isEmpty()) return emptyList()
|
||||
|
||||
return try {
|
||||
trie.startsWith(prefix).take(topK)
|
||||
} catch (_: Throwable) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
//从给定的候选词对列表中,通过一个小顶堆来过滤出评分最高的前k个词
|
||||
private fun topKByScore(pairs: List<Pair<String, Float>>, k: Int): List<String> {
|
||||
val heap = topKHeap(k)
|
||||
|
||||
for (p in pairs) {
|
||||
heap.offer(p)
|
||||
|
||||
if (heap.size > k) heap.poll()
|
||||
}
|
||||
|
||||
return heap.toSortedListDescending()
|
||||
}
|
||||
|
||||
//创建一个优先队列,用于在一组候选词对中保持评分最高的 k 个词。
|
||||
private fun topKHeap(k: Int): PriorityQueue<Pair<String, Float>> {
|
||||
// 小顶堆,比较 Float 分数
|
||||
return PriorityQueue(k.coerceAtLeast(1)) { a, b ->
|
||||
a.second.compareTo(b.second) // 分数小的优先被弹出
|
||||
}
|
||||
}
|
||||
|
||||
// 排序后的候选词列表
|
||||
private fun PriorityQueue<Pair<String, Float>>.toSortedListDescending(): List<String> {
|
||||
val list = ArrayList<Pair<String, Float>>(this.size)
|
||||
|
||||
while (this.isNotEmpty()) {
|
||||
val p = this.poll() ?: continue // 防御性判断,避免 null
|
||||
list.add(p)
|
||||
}
|
||||
|
||||
list.reverse() // 从高分到低分
|
||||
|
||||
return list.map { it.first }
|
||||
}
|
||||
}
|
||||
47
app/src/main/java/com/example/myapplication/MainActivity.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.example.myapplication
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var bottomNav: BottomNavigationView
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// 1. 找到 NavHostFragment
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
|
||||
// 2. 找到 BottomNavigationView
|
||||
bottomNav = findViewById(R.id.bottom_nav)
|
||||
|
||||
// 3. 绑定导航控制器(负责切换 Fragment、保持选中状态)
|
||||
bottomNav.setupWithNavController(navController)
|
||||
|
||||
// 4. 取消图标颜色 tint —— 使用原图标颜色
|
||||
bottomNav.itemIconTintList = null
|
||||
|
||||
// 5. 添加导航监听(用于某些 Fragment 隐藏底部导航栏)
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
when (destination.id) {
|
||||
R.id.rechargeFragment, R.id.goldCoinRechargeFragment,R.id.PersonalSettings,R.id.MySkin,R.id.searchFragment,R.id.MyKeyboard,R.id.searchResultFragment,R.id.keyboardDetailFragment,R.id.feedbackFragment,R.id.notificationFragment-> {
|
||||
bottomNav.visibility = View.GONE
|
||||
}
|
||||
else -> {
|
||||
bottomNav.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/src/main/java/com/example/myapplication/MyApplication.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.example.myapplication
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
|
||||
class MyApplication : Application() {
|
||||
companion object {
|
||||
const val TAG = "MyKeyboard"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Application created")
|
||||
// 初始化日志系统等全局配置
|
||||
}
|
||||
}
|
||||
1092
app/src/main/java/com/example/myapplication/MyInputMethodService.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.example.myapplication
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class OnboardingActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_onboarding)
|
||||
|
||||
val btnStart = findViewById<TextView>(R.id.tv_skip)
|
||||
|
||||
btnStart.setOnClickListener {
|
||||
// 标记已经不是第一次启动了
|
||||
val prefs = getSharedPreferences("app_prefs", MODE_PRIVATE)
|
||||
prefs.edit().putBoolean("is_first_launch", false).apply()
|
||||
|
||||
// 跳转到主界面
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.myapplication
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val prefs = getSharedPreferences("app_prefs", MODE_PRIVATE)
|
||||
val isFirstLaunch = prefs.getBoolean("is_first_launch", true)
|
||||
if (isFirstLaunch) {
|
||||
// 第一次启动 → 进入引导页
|
||||
startActivity(Intent(this, OnboardingActivity::class.java))
|
||||
} else {
|
||||
// 不是第一次 → 直接进入主界面
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.myapplication
|
||||
|
||||
import android.content.Context
|
||||
|
||||
class SuggestionStats(ctx: Context) {
|
||||
|
||||
// 永远持有 applicationContext,避免泄漏/早期异常
|
||||
private val app = ctx.applicationContext
|
||||
|
||||
private val prefs = app.getSharedPreferences("suggestion_stats", Context.MODE_PRIVATE)
|
||||
|
||||
// 点击次数统计
|
||||
fun incClick(word: String?) {
|
||||
val w = word?.trim().orEmpty()
|
||||
|
||||
if (w.isEmpty()) return
|
||||
|
||||
val key = "cnt_$w"
|
||||
|
||||
val old = prefs.getInt(key, 0)
|
||||
|
||||
prefs.edit().putInt(key, old + 1).apply()
|
||||
}
|
||||
|
||||
// 从应用程序的 SharedPreferences 中获取指定单词的点击次数。
|
||||
fun getCount(word: String?): Int {
|
||||
val w = word?.trim().orEmpty()
|
||||
|
||||
if (w.isEmpty()) return 0
|
||||
|
||||
return prefs.getInt("cnt_$w", 0)
|
||||
}
|
||||
|
||||
// 按点击次数从大到小排序候选词。
|
||||
fun sortByCount(candidates: List<String?>): List<String> {
|
||||
val clean = candidates.mapNotNull { it?.trim() }.filter { it.isNotEmpty() }
|
||||
|
||||
if (clean.isEmpty()) return emptyList()
|
||||
|
||||
// 本轮候选的计数缓存(避免在 Comparator 里频繁读 SP)
|
||||
val counts = clean.asSequence().distinct().associateWith { w ->
|
||||
prefs.getInt("cnt_$w", 0)
|
||||
}
|
||||
|
||||
// 稳定排序:是否有记录(1/0) -> 次数降序
|
||||
return clean.sortedWith(
|
||||
compareByDescending<String> { w -> if ((counts[w] ?: 0) > 0) 1 else 0 }
|
||||
.thenByDescending { w -> counts[w] ?: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
60
app/src/main/java/com/example/myapplication/Trie.kt
Normal file
@@ -0,0 +1,60 @@
|
||||
package com.example.myapplication
|
||||
|
||||
class Trie {
|
||||
//表示Trie数据结构中的一个节点,该节点可以存储其子节点,并且可以标记是否是一个完整单词的结尾
|
||||
private data class TrieNode(
|
||||
val children: MutableMap<Char, TrieNode> = mutableMapOf(),
|
||||
|
||||
var isEndOfWord: Boolean = false
|
||||
)
|
||||
|
||||
private val root = TrieNode()//根节点
|
||||
|
||||
//将一个单词插入到Trie数据结构中。通过遍历单词的每个字符,创建并连接相应的节点,最终在最后一个字符的节点上标记该路径代表一个完整单词。
|
||||
fun insert(word: String) {
|
||||
var current = root
|
||||
|
||||
for (char in word.lowercase()) {
|
||||
current = current.children.getOrPut(char) { TrieNode() }
|
||||
}
|
||||
|
||||
current.isEndOfWord = true
|
||||
}
|
||||
|
||||
//在Trie数据结构中查找指定的单词是否存在。
|
||||
fun search(word: String): Boolean {
|
||||
var current = root
|
||||
|
||||
for (char in word.lowercase()) {
|
||||
current = current.children[char] ?: return false
|
||||
}
|
||||
|
||||
return current.isEndOfWord
|
||||
}
|
||||
|
||||
//查找以prefix为前缀的所有单词。通过遍历prefix的每个字符,找到相应的节点,然后从该节点开始递归查找所有以该节点为起点的单词。
|
||||
fun startsWith(prefix: String): List<String> {
|
||||
var current = root
|
||||
|
||||
for (char in prefix.lowercase()) {
|
||||
current = current.children[char] ?: return emptyList()
|
||||
}
|
||||
|
||||
return getAllWordsFromNode(current, prefix)
|
||||
}
|
||||
|
||||
//从给定节点开始递归查找所有以该节点为起点的单词。
|
||||
private fun getAllWordsFromNode(node: TrieNode, prefix: String): List<String> {
|
||||
val words = mutableListOf<String>()
|
||||
|
||||
if (node.isEndOfWord) {
|
||||
words.add(prefix)
|
||||
}
|
||||
|
||||
for ((char, child) in node.children) {
|
||||
words.addAll(getAllWordsFromNode(child, prefix + char))
|
||||
}
|
||||
|
||||
return words
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.example.myapplication.data
|
||||
|
||||
import android.content.Context
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
|
||||
data class BigramModel(
|
||||
val vocab: List<String>, // 保留全部词(含 <unk>, <s>, </s>),与二元矩阵索引对齐
|
||||
val uniLogp: FloatArray, // 长度 = vocab.size
|
||||
val biRowptr: IntArray, // 长度 = vocab.size + 1 (CSR)
|
||||
val biCols: IntArray, // 长度 = nnz
|
||||
val biLogp: FloatArray // 长度 = nnz
|
||||
)
|
||||
|
||||
object LanguageModelLoader {
|
||||
fun load(context: Context): BigramModel {
|
||||
val vocab = context.assets.open("vocab.txt").bufferedReader()
|
||||
.use(BufferedReader::readLines)
|
||||
|
||||
val uniLogp = readFloat32(context, "uni_logp.bin")
|
||||
val biRowptr = readInt32(context, "bi_rowptr.bin")
|
||||
val biCols = readInt32(context, "bi_cols.bin")
|
||||
val biLogp = readFloat32(context, "bi_logp.bin")
|
||||
|
||||
require(uniLogp.size == vocab.size) { "uni_logp length != vocab size" }
|
||||
require(biRowptr.size == vocab.size + 1) { "bi_rowptr length invalid" }
|
||||
require(biCols.size == biLogp.size) { "bi cols/logp nnz mismatch" }
|
||||
|
||||
return BigramModel(vocab, uniLogp, biRowptr, biCols, biLogp)
|
||||
}
|
||||
|
||||
private fun readInt32(context: Context, name: String): IntArray {
|
||||
context.assets.open(name).use { input ->
|
||||
val bytes = input.readBytes()
|
||||
val n = bytes.size / 4
|
||||
val out = IntArray(n)
|
||||
var i = 0; var j = 0
|
||||
while (i < n) {
|
||||
// 小端序
|
||||
val v = (bytes[j].toInt() and 0xFF) or
|
||||
((bytes[j+1].toInt() and 0xFF) shl 8) or
|
||||
((bytes[j+2].toInt() and 0xFF) shl 16) or
|
||||
((bytes[j+3].toInt() and 0xFF) shl 24)
|
||||
out[i++] = v
|
||||
j += 4
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFloat32(context: Context, name: String): FloatArray {
|
||||
context.assets.open(name).use { input ->
|
||||
val bytes = input.readBytes()
|
||||
val n = bytes.size / 4
|
||||
val out = FloatArray(n)
|
||||
var i = 0; var j = 0
|
||||
while (i < n) {
|
||||
val bits = (bytes[j].toInt() and 0xFF) or
|
||||
((bytes[j+1].toInt() and 0xFF) shl 8) or
|
||||
((bytes[j+2].toInt() and 0xFF) shl 16) or
|
||||
((bytes[j+3].toInt() and 0xFF) shl 24)
|
||||
out[i++] = Float.fromBits(bits)
|
||||
j += 4
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 输入法读取词库文件并构建 Trie 树
|
||||
package com.example.myapplication.data
|
||||
|
||||
import android.content.Context
|
||||
import com.example.myapplication.Trie
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class WordDictionary(private val context: Context) {
|
||||
|
||||
// 初始化给空 Trie,避免构造期读取导致崩溃
|
||||
val wordTrie: Trie = Trie()
|
||||
|
||||
private val loaded = AtomicBoolean(false)
|
||||
|
||||
/** 背景安全加载,多次调用只会生效一次 */
|
||||
fun loadIfNeeded() {
|
||||
if (!loaded.compareAndSet(false, true)) return
|
||||
try {
|
||||
context.assets.open("vocab.txt").use { input ->
|
||||
BufferedReader(InputStreamReader(input)).useLines { lines ->
|
||||
var idx = 0
|
||||
lines.forEach { line ->
|
||||
idx++
|
||||
if (idx <= 3) return@forEach // 跳过 <unk>, <s>, </s>
|
||||
val w = line.trim()
|
||||
if (w.isNotEmpty()) {
|
||||
wordTrie.insert(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
// 失败也不抛,让输入法继续运行
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.myapplication.ui.circle
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
|
||||
class CircleFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_circle, container, false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
package com.example.myapplication.ui.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.example.myapplication.R
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import android.graphics.drawable.TransitionDrawable
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.HorizontalScrollView
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import kotlin.math.abs
|
||||
import android.content.Intent
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
private lateinit var bottomSheet: MaterialCardView
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehavior<MaterialCardView>
|
||||
private lateinit var scrim: View
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var tagContainer: LinearLayout
|
||||
private lateinit var tagScroll: HorizontalScrollView
|
||||
private lateinit var header: View
|
||||
private lateinit var tabList1: TextView
|
||||
private lateinit var tabList2: TextView
|
||||
private lateinit var backgroundImage: ImageView
|
||||
private var parentWidth = 0
|
||||
private var parentHeight = 0
|
||||
|
||||
private val dragToCloseThreshold by lazy {
|
||||
val dp = 40f
|
||||
(dp * resources.displayMetrics.density)
|
||||
}
|
||||
|
||||
// 第二个列表的“标签页”,数量不固定,可以从服务端/本地配置来
|
||||
private val tags = listOf("标签一", "标签二", "标签三", "标签四", "标签五", "标签六", "标签七", "标签八", "标签九", "标签十")
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 充值按钮点击
|
||||
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_global_rechargeFragment)
|
||||
}
|
||||
|
||||
scrim = view.findViewById(R.id.view_scrim)
|
||||
bottomSheet = view.findViewById(R.id.bottomSheet)
|
||||
tagContainer = view.findViewById(R.id.tagContainer)
|
||||
tagScroll = view.findViewById(R.id.tagScroll)
|
||||
header = view.findViewById(R.id.bottomSheetHeader)
|
||||
tabList1 = view.findViewById(R.id.tab_list1)
|
||||
tabList2 = view.findViewById(R.id.tab_list2)
|
||||
viewPager = view.findViewById(R.id.viewPager)
|
||||
backgroundImage = bottomSheet.findViewById(R.id.backgroundImage)
|
||||
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
|
||||
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
|
||||
// 拿到父布局的宽高(需要等布局完成)
|
||||
root.post {
|
||||
parentWidth = root.width
|
||||
parentHeight = root.height
|
||||
}
|
||||
|
||||
initDrag(floatingImage, root)
|
||||
setupBottomSheet(view)
|
||||
setupViewPager()
|
||||
setupTopTabs()
|
||||
setupTags()
|
||||
}
|
||||
// ---------------- 拖拽效果 ----------------
|
||||
private fun initDrag(target: View, parent: ViewGroup) {
|
||||
var dX = 0f
|
||||
var dY = 0f
|
||||
var lastRawX = 0f
|
||||
var lastRawY = 0f
|
||||
var isDragging = false
|
||||
|
||||
val touchSlop = ViewConfiguration.get(requireContext()).scaledTouchSlop
|
||||
|
||||
target.setOnTouchListener { v, event ->
|
||||
when (event.actionMasked) {
|
||||
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// 告诉 CoordinatorLayout:别拦截这次事件
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
// 暂时禁止 BottomSheet 拖动
|
||||
if (::bottomSheetBehavior.isInitialized) {
|
||||
bottomSheetBehavior.isDraggable = false
|
||||
}
|
||||
|
||||
dX = v.x - event.rawX
|
||||
dY = v.y - event.rawY
|
||||
lastRawX = event.rawX
|
||||
lastRawY = event.rawY
|
||||
isDragging = false
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val dxMove = event.rawX - lastRawX
|
||||
val dyMove = event.rawY - lastRawY
|
||||
if (!isDragging && (abs(dxMove) > touchSlop || abs(dyMove) > touchSlop)) {
|
||||
isDragging = true
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
var newX = event.rawX + dX
|
||||
var newY = event.rawY + dY
|
||||
|
||||
// 限制在父布局范围内
|
||||
val maxX = parentWidth - v.width
|
||||
val maxY = parentHeight - v.height
|
||||
|
||||
newX = newX.coerceIn(0f, maxX.toFloat())
|
||||
newY = newY.coerceIn(0f, maxY.toFloat())
|
||||
|
||||
v.x = newX
|
||||
v.y = newY
|
||||
}
|
||||
|
||||
lastRawX = event.rawX
|
||||
lastRawY = event.rawY
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
// 允许父布局继续拦截之后的事件
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
// 恢复 BottomSheet 可拖动
|
||||
if (::bottomSheetBehavior.isInitialized) {
|
||||
bottomSheetBehavior.isDraggable = true
|
||||
}
|
||||
|
||||
if (!isDragging) {
|
||||
v.performClick()
|
||||
}
|
||||
|
||||
// 手指抬起:吸边
|
||||
snapToEdge(v)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 吸边逻辑:左右贴边(需要上下也吸边可以再扩展)
|
||||
*/
|
||||
private fun snapToEdge(v: View) {
|
||||
if (parentWidth == 0 || parentHeight == 0) return
|
||||
|
||||
val centerX = v.x + v.width / 2f
|
||||
val toLeft = centerX < parentWidth / 2f
|
||||
|
||||
val targetX = if (toLeft) 0f else (parentWidth - v.width).toFloat()
|
||||
|
||||
// 如果你还想限制上下边距,比如离底部留 80dp 不遮挡 BottomSheet,可以再处理 y
|
||||
val minTop = 0f
|
||||
val maxBottom = (parentHeight - v.height).toFloat()
|
||||
val targetY = v.y.coerceIn(minTop, maxBottom)
|
||||
|
||||
v.animate()
|
||||
.x(targetX)
|
||||
.y(targetY)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
}
|
||||
// ---------------- BottomSheet 行为 ----------------
|
||||
|
||||
private fun setupBottomSheet(root: View) {
|
||||
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
|
||||
|
||||
// 允许拖拽,允许嵌套滚动控制
|
||||
bottomSheetBehavior.isDraggable = true
|
||||
bottomSheetBehavior.isHideable = false
|
||||
bottomSheetBehavior.isFitToContents = false
|
||||
// 展开时高度占屏幕 70%
|
||||
bottomSheetBehavior.halfExpandedRatio = 0.7f
|
||||
|
||||
// 先等布局完成之后,计算“按钮下面剩余空间”作为 peekHeight
|
||||
root.post {
|
||||
val coordinatorHeight = root.height-40
|
||||
val button = root.findViewById<View>(R.id.rechargeButton)
|
||||
val buttonBottom = button.bottom
|
||||
val peek = (coordinatorHeight - buttonBottom).coerceAtLeast(200)
|
||||
bottomSheetBehavior.peekHeight = peek
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
||||
// 监听状态变化,用来控制遮罩显示/隐藏
|
||||
bottomSheetBehavior.addBottomSheetCallback(object :
|
||||
BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
when (newState) {
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||
scrim.isVisible = false
|
||||
}
|
||||
BottomSheetBehavior.STATE_DRAGGING,
|
||||
BottomSheetBehavior.STATE_EXPANDED,
|
||||
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
|
||||
scrim.isVisible = true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
// 跟随滑动渐变遮罩透明度
|
||||
if (slideOffset >= 0f) {
|
||||
scrim.alpha = slideOffset.coerceIn(0f, 1f)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 点击遮罩,关闭回原位
|
||||
scrim.setOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
||||
// 简单的“空白区域下滑”关闭:在遮罩上响应手势(简单版,只要 move 就关)
|
||||
scrim.setOnTouchListener { _, event ->
|
||||
// 这里可以更精细地判断手势方向,这里简单处理为:有滑动就关闭
|
||||
// 如果你想更准,可以根据 down / move 的 dy 判断
|
||||
// 为了示例就写得简单一点
|
||||
// MotionEvent.ACTION_MOVE = 2
|
||||
if (event.action == 2) {
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// 点击底部盒子的“头部”,在折叠 / 半展开之间切换
|
||||
header.setOnClickListener {
|
||||
when (bottomSheetBehavior.state) {
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||
}
|
||||
BottomSheetBehavior.STATE_HALF_EXPANDED,
|
||||
BottomSheetBehavior.STATE_EXPANDED -> {
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- ViewPager2 + 列表 ----------------
|
||||
|
||||
private fun setupViewPager() {
|
||||
val pageCount = 1 + tags.size // 1 = 第一个列表,剩下的是第二个列表的标签页
|
||||
viewPager.adapter = SheetPagerAdapter(pageCount)
|
||||
|
||||
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
updateTabsAndTags(position) // 里面会调用 highlightTag,把标签高亮并滚动
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 顶部“列表一 / 列表二”选项栏点击
|
||||
private fun setupTopTabs() {
|
||||
tabList1.setOnClickListener {
|
||||
viewPager.currentItem = 0 // 列表一
|
||||
}
|
||||
tabList2.setOnClickListener {
|
||||
viewPager.currentItem = 1 // 列表二的第一个标签页
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部标签行(只在第二个列表时可见)
|
||||
private fun setupTags() {
|
||||
tagContainer.removeAllViews()
|
||||
tags.forEachIndexed { index, tag ->
|
||||
val tv = layoutInflater.inflate(
|
||||
R.layout.item_tag,
|
||||
tagContainer,
|
||||
false
|
||||
) as TextView
|
||||
tv.text = tag
|
||||
tv.setOnClickListener {
|
||||
// 当前位置 = 1 + 标签下标
|
||||
viewPager.currentItem = 1 + index
|
||||
}
|
||||
tagContainer.addView(tv)
|
||||
}
|
||||
// 默认选中列表一,所以标签行默认隐藏
|
||||
tagScroll.isVisible = false
|
||||
}
|
||||
|
||||
// 根据当前 page 更新上方两个选项 & 标签高亮/显隐
|
||||
private fun updateTabsAndTags(position: Int) {
|
||||
|
||||
if (position == 0) {
|
||||
tabList1.setTextColor(requireContext().getColor(R.color.black))
|
||||
tabList2.setTextColor(requireContext().getColor(R.color.light_black))
|
||||
tagScroll.isVisible = false
|
||||
|
||||
fadeImage(backgroundImage, R.drawable.option_background)
|
||||
|
||||
} else {
|
||||
|
||||
tabList1.setTextColor(requireContext().getColor(R.color.light_black))
|
||||
tabList2.setTextColor(requireContext().getColor(R.color.black))
|
||||
tagScroll.isVisible = true
|
||||
|
||||
fadeImage(backgroundImage, R.drawable.option_background_two)
|
||||
|
||||
val tagIndex = position - 1
|
||||
highlightTag(tagIndex)
|
||||
}
|
||||
}
|
||||
//背景淡入淡出
|
||||
private fun fadeImage(imageView: ImageView, newImageRes: Int) {
|
||||
val oldDrawable = imageView.drawable
|
||||
val newDrawable = ContextCompat.getDrawable(requireContext(), newImageRes)
|
||||
|
||||
if (newDrawable == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// 第一次还没有旧图,直接设置就好
|
||||
if (oldDrawable == null) {
|
||||
imageView.setImageDrawable(newDrawable)
|
||||
return
|
||||
}
|
||||
|
||||
val transitionDrawable = TransitionDrawable(arrayOf(oldDrawable, newDrawable)).apply {
|
||||
// 关键:启用交叉淡入淡出,旧图才会一起淡出
|
||||
isCrossFadeEnabled = true
|
||||
}
|
||||
|
||||
imageView.setImageDrawable(transitionDrawable)
|
||||
transitionDrawable.startTransition(300) // 300ms 淡入淡出
|
||||
}
|
||||
|
||||
|
||||
private fun highlightTag(index: Int) {
|
||||
for (i in 0 until tagContainer.childCount) {
|
||||
val child = tagContainer.getChildAt(i) as TextView
|
||||
if (i == index) {
|
||||
child.setBackgroundResource(R.drawable.tag_selected_bg)
|
||||
child.setTextColor(requireContext().getColor(android.R.color.white))
|
||||
|
||||
// 关键:把选中的标签滚动到可见(这里我用“居中”效果)
|
||||
tagScroll.post {
|
||||
val scrollViewWidth = tagScroll.width
|
||||
val childCenter = child.left + child.width / 2
|
||||
val targetScrollX = childCenter - scrollViewWidth / 2
|
||||
tagScroll.smoothScrollTo(targetScrollX.coerceAtLeast(0), 0)
|
||||
}
|
||||
|
||||
} else {
|
||||
child.setBackgroundResource(R.drawable.tag_unselected_bg)
|
||||
child.setTextColor(requireContext().getColor(R.color.light_black))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------- 共享的 ViewHolder 类 ----------------
|
||||
|
||||
inner class PageViewHolder(val recyclerView: RecyclerView) :
|
||||
RecyclerView.ViewHolder(recyclerView)
|
||||
|
||||
// ---------------- ViewPager2 的 Adapter ----------------
|
||||
|
||||
/**
|
||||
* 每一页都是一个 RecyclerView 卡片列表:
|
||||
* - position = 0:列表一(数据 A)
|
||||
* - position >= 1:列表二的第 index 个标签页(数据 B[index])
|
||||
*/
|
||||
inner class SheetPagerAdapter(
|
||||
private val pageCount: Int
|
||||
) : RecyclerView.Adapter<SheetPagerAdapter.PageViewHolder>() {
|
||||
|
||||
inner class PageViewHolder(val root: View) : RecyclerView.ViewHolder(root)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
// 0:第一个列表页,>0:第二个列表的各标签页
|
||||
return if (position == 0) 0 else 1
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
|
||||
val layoutId = when (viewType) {
|
||||
0 -> R.layout.bottom_page_list1 // 第一个列表的自定义内容
|
||||
else -> R.layout.bottom_page_list2 // 第二个列表各标签页的自定义内容
|
||||
}
|
||||
|
||||
val root = LayoutInflater.from(parent.context)
|
||||
.inflate(layoutId, parent, false)
|
||||
|
||||
// 如果需要,禁用嵌套滚动(对 NestedScrollView 一般问题不大,可以不写)
|
||||
// root.findViewById<NestedScrollView>(R.id.scrollContent)?.isNestedScrollingEnabled = false
|
||||
|
||||
return PageViewHolder(root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
|
||||
val root = holder.root
|
||||
|
||||
if (position == 0) {
|
||||
// 这里可以拿到 bottom_page_list1 中的控件,做一些初始化
|
||||
// val someView = root.findViewById<TextView>(R.id.xxx)
|
||||
// someView.text = "xxx"
|
||||
|
||||
} else {
|
||||
// // 第二个列表对应的标签页
|
||||
// val tagIndex = position - 1
|
||||
// val tagName = tags[tagIndex]
|
||||
|
||||
// // 示例:把标题改成“标签一的内容 / 标签二的内容 ……”
|
||||
// val titleView = root.findViewById<TextView>(R.id.pageTitle)
|
||||
// titleView?.text = "$tagName 的自定义内容"
|
||||
|
||||
// // 你也可以根据 tagIndex,显示/隐藏不同区域
|
||||
}
|
||||
|
||||
//让当前页里的滚动容器具备“下拉关闭 BottomSheet”的能力
|
||||
val scrollContent = root.findViewById<View>(R.id.scrollContent)
|
||||
if (scrollContent != null) {
|
||||
setupPullToClose(scrollContent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pageCount
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private fun setupPullToClose(scrollable: View) {
|
||||
var downY = 0f
|
||||
var isDraggingToClose = false
|
||||
|
||||
scrollable.setOnTouchListener { _, event ->
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
downY = event.rawY
|
||||
isDraggingToClose = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
// 已经是折叠状态,不拦截,交给内容自己滚(其实也滚不动多少)
|
||||
if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
return@setOnTouchListener false
|
||||
}
|
||||
|
||||
val dy = event.rawY - downY
|
||||
|
||||
if (!scrollable.canScrollVertically(-1) && // 已在顶部
|
||||
dy > dragToCloseThreshold && // 向下拉超过阈值
|
||||
(bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
bottomSheetBehavior.state == BottomSheetBehavior.STATE_HALF_EXPANDED)
|
||||
) {
|
||||
isDraggingToClose = true
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
isDraggingToClose = false
|
||||
}
|
||||
}
|
||||
|
||||
isDraggingToClose
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.myapplication.ui.keyboard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.widget.FrameLayout
|
||||
import com.example.myapplication.R
|
||||
|
||||
class KeyboardDetailFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.keyboard_detail, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.myapplication.ui.keyboard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.widget.FrameLayout
|
||||
import com.example.myapplication.R
|
||||
|
||||
class MyKeyboard : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.my_keyboard, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.example.myapplication.ui.mine
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.example.myapplication.R
|
||||
import android.widget.LinearLayout
|
||||
import de.hdodenhof.circleimageview.CircleImageView
|
||||
|
||||
class MineFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_mine, container, false)
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 会员充值按钮点击
|
||||
view.findViewById<ImageView>(R.id.imgLeft).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_global_rechargeFragment)
|
||||
}
|
||||
|
||||
// 金币充值按钮点击
|
||||
view.findViewById<ImageView>(R.id.imgRight).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_global_goldCoinRechargeFragment)
|
||||
}
|
||||
|
||||
// 头像点击
|
||||
view.findViewById<CircleImageView>(R.id.avatar).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_mineFragment_to_personalSettings)
|
||||
}
|
||||
|
||||
//我的键盘
|
||||
view.findViewById<LinearLayout>(R.id.keyboard_settings).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_mineFragment_to_mykeyboard)
|
||||
}
|
||||
|
||||
// 反馈按钮点击
|
||||
view.findViewById<LinearLayout>(R.id.click_Feedback).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_mineFragment_to_feedbackFragment)
|
||||
}
|
||||
|
||||
// 反馈按钮点击
|
||||
view.findViewById<LinearLayout>(R.id.click_Notice).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_mineFragment_to_notificationFragment)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.myapplication.ui.mine.myotherpages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import java.util.*
|
||||
|
||||
class FeedbackFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.feedback_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 设置关闭按钮点击事件
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.myapplication.ui.mine.myotherpages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import java.util.*
|
||||
|
||||
class NotificationFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.notification_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 设置关闭按钮点击事件
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.myapplication.ui.mine.myotherpages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import java.util.*
|
||||
|
||||
class PersonalSettings : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.personal_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 设置关闭按钮点击事件
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.myapplication.ui.recharge
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
|
||||
class GoldCoinRechargeFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.gold_coin_recharge, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 设置关闭按钮点击事件
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.myapplication.ui.recharge
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.ImageView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
|
||||
class RechargeFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.activity_recharge, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 找到旧价格 TextView
|
||||
val tvOldPrice = view.findViewById<TextView>(R.id.tvOldPrice)
|
||||
// 旧价格加删除线
|
||||
tvOldPrice.paintFlags = tvOldPrice.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
|
||||
// 设置关闭按钮点击事件
|
||||
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.example.myapplication.ui.shop
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.HorizontalScrollView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.example.myapplication.R
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.animation.ValueAnimator
|
||||
import android.animation.ArgbEvaluator
|
||||
|
||||
class ShopFragment : Fragment() {
|
||||
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var tagScroll: HorizontalScrollView
|
||||
private lateinit var tagContainer: LinearLayout
|
||||
|
||||
// 标签标题,可以根据需要修改
|
||||
private val tabTitles = listOf("全部", "数码", "服饰", "家居", "美食","数码", "服饰", "家居", "美食")
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_shop, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 金币充值按钮点击
|
||||
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_global_goldCoinRechargeFragment)
|
||||
}
|
||||
// 我的皮肤按钮点击
|
||||
view.findViewById<View>(R.id.skinButton).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_shopfragment_to_myskin)
|
||||
}
|
||||
// 搜索按钮点击
|
||||
view.findViewById<View>(R.id.searchButton).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_shopfragment_to_searchfragment)
|
||||
}
|
||||
|
||||
|
||||
|
||||
tagScroll = view.findViewById(R.id.tagScroll)
|
||||
tagContainer = view.findViewById(R.id.tagContainer)
|
||||
viewPager = view.findViewById(R.id.viewPager)
|
||||
val rechargeButton = view.findViewById<LinearLayout>(R.id.rechargeButton)
|
||||
rechargeButton.setOnClickListener {
|
||||
findNavController().navigate(R.id.action_global_goldCoinRechargeFragment)
|
||||
}
|
||||
// 1. 设置 ViewPager2 的 Adapter
|
||||
viewPager.adapter = ShopPagerAdapter(this, tabTitles.size)
|
||||
|
||||
// 2. 创建顶部标签
|
||||
setupTags()
|
||||
|
||||
// 3. 绑定 ViewPager2 滑动 & 标签联动
|
||||
setupViewPager()
|
||||
}
|
||||
|
||||
/** 动态创建标签 TextView */
|
||||
private fun setupTags() {
|
||||
tagContainer.removeAllViews()
|
||||
|
||||
val context = requireContext()
|
||||
val density = context.resources.displayMetrics.density
|
||||
|
||||
// ⬇⬇⬇ 你要求的 padding 值(已适配 dp)
|
||||
val paddingHorizontal = (16 * density).toInt() // 左右 16dp
|
||||
val paddingVertical = (6 * density).toInt() // 上下 6dp
|
||||
val marginEnd = (8 * density).toInt() // 标签之间 8dp 间距
|
||||
|
||||
tabTitles.forEachIndexed { index, title ->
|
||||
val tv = TextView(context).apply {
|
||||
text = title
|
||||
textSize = 12f // 字体大小 12sp
|
||||
|
||||
// ✅ 设置内边距(左右16dp,上下6dp)
|
||||
setPadding(
|
||||
paddingHorizontal,
|
||||
paddingVertical,
|
||||
paddingHorizontal,
|
||||
paddingVertical
|
||||
)
|
||||
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
).apply {
|
||||
setMargins(0, 0, marginEnd, 0) // 右侧 8dp 间距
|
||||
}
|
||||
|
||||
gravity = android.view.Gravity.CENTER
|
||||
|
||||
// 胶囊大圆角背景
|
||||
background = createCapsuleBackground()
|
||||
|
||||
// 初始化选中状态
|
||||
isSelected = index == 0
|
||||
updateTagStyleNoAnim(this, isSelected) // 初始化不用动画,避免闪烁
|
||||
|
||||
// 点击切换页面
|
||||
setOnClickListener {
|
||||
if (viewPager.currentItem != index) {
|
||||
viewPager.currentItem = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagContainer.addView(tv)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun createCapsuleBackground(): GradientDrawable {
|
||||
val density = resources.displayMetrics.density
|
||||
return GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = 50f * density // 大圆角
|
||||
setColor(Color.parseColor("#F1F1F1")) // 默认未选中背景
|
||||
setStroke((2 * density).toInt(), Color.parseColor("#F1F1F1"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 设置 ViewPager2 的监听,实现滑动联动标签 */
|
||||
private fun setupViewPager() {
|
||||
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
updateTagState(position)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据当前位置更新所有标签的选中状态 */
|
||||
private fun updateTagState(position: Int) {
|
||||
for (i in 0 until tagContainer.childCount) {
|
||||
val child = tagContainer.getChildAt(i) as TextView
|
||||
val newSelected = i == position
|
||||
|
||||
// ✅ 如果这个标签的选中状态没有变化,就不要动它,避免“闪一下”
|
||||
if (child.isSelected == newSelected) continue
|
||||
|
||||
child.isSelected = newSelected
|
||||
updateTagStyleWithAnim(child, newSelected)
|
||||
|
||||
if (newSelected) {
|
||||
// 让选中项尽量居中显示
|
||||
child.post {
|
||||
val scrollX = child.left - (tagScroll.width - child.width) / 2
|
||||
tagScroll.smoothScrollTo(scrollX, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 统一控制标签样式,可根据自己项目主题改颜色/大小 **/
|
||||
private fun updateTagStyle(textView: TextView, selected: Boolean) {
|
||||
val context = textView.context
|
||||
val density = context.resources.displayMetrics.density
|
||||
|
||||
// 确保背景是 GradientDrawable,方便改边框和背景色
|
||||
val bg = (textView.background as? GradientDrawable)
|
||||
?: createCapsuleBackground().also { textView.background = it }
|
||||
|
||||
// 颜色配置(按你要求)
|
||||
val selectedTextColor = Color.parseColor("#1B1F1A")
|
||||
val unselectedTextColor = Color.parseColor("#9F9F9F")
|
||||
|
||||
val selectedStrokeColor = Color.parseColor("#02BEAC")
|
||||
val unselectedStrokeColor = Color.parseColor("#F1F1F1")
|
||||
|
||||
val selectedBgColor = Color.parseColor("#FFFFFF")
|
||||
val unselectedBgColor = Color.parseColor("#F1F1F1")
|
||||
|
||||
// 当前颜色作为起点
|
||||
val startTextColor = textView.currentTextColor
|
||||
val startStrokeColor = try {
|
||||
// 没有方便的 getter,这里通过 isSelected 反推一个“起点”
|
||||
if (selected) unselectedStrokeColor else selectedStrokeColor
|
||||
} catch (e: Exception) {
|
||||
if (selected) unselectedStrokeColor else selectedStrokeColor
|
||||
}
|
||||
val startBgColor = if (selected) unselectedBgColor else selectedBgColor
|
||||
|
||||
// 目标颜色
|
||||
val endTextColor = if (selected) selectedTextColor else unselectedTextColor
|
||||
val endStrokeColor = if (selected) selectedStrokeColor else unselectedStrokeColor
|
||||
val endBgColor = if (selected) selectedBgColor else unselectedBgColor
|
||||
|
||||
val strokeWidth = (2 * density).toInt()
|
||||
|
||||
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 200L // 动画时长可以自己调
|
||||
addUpdateListener { va ->
|
||||
val fraction = va.animatedFraction
|
||||
val evaluator = ArgbEvaluator()
|
||||
|
||||
val currentTextColor =
|
||||
evaluator.evaluate(fraction, startTextColor, endTextColor) as Int
|
||||
val currentStrokeColor =
|
||||
evaluator.evaluate(fraction, startStrokeColor, endStrokeColor) as Int
|
||||
val currentBgColor =
|
||||
evaluator.evaluate(fraction, startBgColor, endBgColor) as Int
|
||||
|
||||
textView.setTextColor(currentTextColor)
|
||||
bg.setStroke(strokeWidth, currentStrokeColor)
|
||||
bg.setColor(currentBgColor)
|
||||
}
|
||||
}
|
||||
animator.start()
|
||||
|
||||
// 字重变化
|
||||
textView.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** ViewPager2 的 Adapter,可以替换成你的真实 Fragment */
|
||||
private class ShopPagerAdapter(
|
||||
fragment: Fragment,
|
||||
private val pageCount: Int
|
||||
) : FragmentStateAdapter(fragment) {
|
||||
|
||||
override fun getItemCount(): Int = pageCount
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
// 根据 position 返回不同的页面 Fragment
|
||||
|
||||
// 这里先用一个简单的占位示例
|
||||
return SimplePageFragment.newInstance("当前页:${position + 1}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTagStyleNoAnim(textView: TextView, selected: Boolean) {
|
||||
val density = resources.displayMetrics.density
|
||||
val bg = (textView.background as? GradientDrawable)
|
||||
?: createCapsuleBackground().also { textView.background = it }
|
||||
|
||||
val strokeWidth = (2 * density).toInt()
|
||||
|
||||
if (selected) {
|
||||
bg.setColor(Color.parseColor("#FFFFFF")) // 背景白色
|
||||
bg.setStroke(strokeWidth, Color.parseColor("#02BEAC")) // 边框 #02BEAC
|
||||
textView.setTextColor(Color.parseColor("#1B1F1A")) // 字体 #1B1F1A
|
||||
textView.setTypeface(null, Typeface.BOLD)
|
||||
} else {
|
||||
bg.setColor(Color.parseColor("#F1F1F1")) // 背景 #F1F1F1
|
||||
bg.setStroke(strokeWidth, Color.parseColor("#F1F1F1")) // 边框 #F1F1F1
|
||||
textView.setTextColor(Color.parseColor("#9F9F9F")) // 字体 #9F9F9F
|
||||
textView.setTypeface(null, Typeface.NORMAL)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTagStyleWithAnim(textView: TextView, selected: Boolean) {
|
||||
val density = resources.displayMetrics.density
|
||||
val bg = (textView.background as? GradientDrawable)
|
||||
?: createCapsuleBackground().also { textView.background = it }
|
||||
|
||||
val strokeWidth = (2 * density).toInt()
|
||||
|
||||
// 颜色配置
|
||||
val selectedTextColor = Color.parseColor("#1B1F1A")
|
||||
val unselectedTextColor = Color.parseColor("#9F9F9F")
|
||||
|
||||
val selectedStrokeColor = Color.parseColor("#02BEAC")
|
||||
val unselectedStrokeColor = Color.parseColor("#F1F1F1")
|
||||
|
||||
val selectedBgColor = Color.parseColor("#FFFFFF")
|
||||
val unselectedBgColor = Color.parseColor("#F1F1F1")
|
||||
|
||||
// 起点、终点颜色我们自己定义,而不是乱读当前值,避免抖动
|
||||
val startTextColor: Int
|
||||
val endTextColor: Int
|
||||
val startStrokeColor: Int
|
||||
val endStrokeColor: Int
|
||||
val startBgColor: Int
|
||||
val endBgColor: Int
|
||||
|
||||
if (selected) {
|
||||
// 未选中 -> 选中
|
||||
startTextColor = unselectedTextColor
|
||||
endTextColor = selectedTextColor
|
||||
|
||||
startStrokeColor = unselectedStrokeColor
|
||||
endStrokeColor = selectedStrokeColor
|
||||
|
||||
startBgColor = unselectedBgColor
|
||||
endBgColor = selectedBgColor
|
||||
} else {
|
||||
// 选中 -> 未选中
|
||||
startTextColor = selectedTextColor
|
||||
endTextColor = unselectedTextColor
|
||||
|
||||
startStrokeColor = selectedStrokeColor
|
||||
endStrokeColor = unselectedStrokeColor
|
||||
|
||||
startBgColor = selectedBgColor
|
||||
endBgColor = unselectedBgColor
|
||||
}
|
||||
|
||||
val evaluator = ArgbEvaluator()
|
||||
|
||||
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 200L
|
||||
addUpdateListener { va ->
|
||||
val fraction = va.animatedFraction
|
||||
|
||||
val currentTextColor =
|
||||
evaluator.evaluate(fraction, startTextColor, endTextColor) as Int
|
||||
val currentStrokeColor =
|
||||
evaluator.evaluate(fraction, startStrokeColor, endStrokeColor) as Int
|
||||
val currentBgColor =
|
||||
evaluator.evaluate(fraction, startBgColor, endBgColor) as Int
|
||||
|
||||
textView.setTextColor(currentTextColor)
|
||||
bg.setStroke(strokeWidth, currentStrokeColor)
|
||||
bg.setColor(currentBgColor)
|
||||
}
|
||||
}
|
||||
animator.start()
|
||||
|
||||
textView.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.myapplication.ui.shop
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
|
||||
class SimplePageFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
fun newInstance(text: String): SimplePageFragment {
|
||||
val fragment = SimplePageFragment()
|
||||
val args = Bundle()
|
||||
args.putString("text", text)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.simple_page_layout, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// val text = arguments?.getString("text") ?: ""
|
||||
// view.findViewById<TextView>(R.id.textView).text = text
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.myapplication.ui.shop.myskin
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.widget.FrameLayout
|
||||
import com.example.myapplication.R
|
||||
|
||||
class MySkin : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.my_skin, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.example.myapplication.ui.shop.search
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.FlexboxLayout.LayoutParams
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.navigation.fragment.findNavController
|
||||
|
||||
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
|
||||
private lateinit var historyLayout: FlexboxLayout
|
||||
private lateinit var etInput: EditText
|
||||
private val prefsName = "search_history"
|
||||
private lateinit var historySection: LinearLayout
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// 返回
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
// 详情跳转
|
||||
view.findViewById<CardView>(R.id.card_view).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_global_keyboardDetailFragment)
|
||||
}
|
||||
|
||||
|
||||
|
||||
historySection = view.findViewById(R.id.layout_history_section)
|
||||
historyLayout = view.findViewById(R.id.layout_history_list)
|
||||
etInput = view.findViewById(R.id.et_input)
|
||||
|
||||
// 加载历史记录
|
||||
loadHistory()
|
||||
|
||||
// 点击 Search 按钮
|
||||
val tvSearch = view.findViewById<TextView>(R.id.tv_search)
|
||||
tvSearch.setOnClickListener {
|
||||
val keyword = etInput.text.toString().trim()
|
||||
if (keyword.isNotEmpty()) {
|
||||
saveHistory(keyword)
|
||||
|
||||
// 把搜索词放进 Bundle
|
||||
val bundle = bundleOf("search_keyword" to keyword)
|
||||
|
||||
// 跳转时带上 bundle
|
||||
findNavController().navigate(
|
||||
R.id.action_searchFragment_to_searchResultFragment,
|
||||
bundle
|
||||
)
|
||||
|
||||
etInput.setText("")
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "请输入搜索内容", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空历史记录
|
||||
view.findViewById<FrameLayout>(R.id.iv_delete_history).setOnClickListener {
|
||||
clearHistory()
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存历史记录到 SharedPreferences */
|
||||
private fun saveHistory(keyword: String) {
|
||||
val prefs = requireContext().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
val oldList = prefs.getStringSet("list", mutableSetOf())!!.toMutableSet()
|
||||
|
||||
oldList.add(keyword)
|
||||
|
||||
prefs.edit().putStringSet("list", oldList).apply()
|
||||
}
|
||||
|
||||
/** 加载历史记录 */
|
||||
private fun loadHistory() {
|
||||
historyLayout.removeAllViews()
|
||||
|
||||
val prefs = requireContext().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
val list = prefs.getStringSet("list", emptySet())?.toList() ?: emptyList()
|
||||
|
||||
if (list.isEmpty()) {
|
||||
historySection.visibility = View.GONE // 没有历史 → 隐藏区域
|
||||
return
|
||||
} else {
|
||||
historySection.visibility = View.VISIBLE // 有历史 → 显示区域
|
||||
}
|
||||
|
||||
for (item in list.reversed()) {
|
||||
historyLayout.addView(createHistoryItem(item))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 创建历史记录 item */
|
||||
private fun createHistoryItem(keyword: String): View {
|
||||
val tv = TextView(requireContext())
|
||||
tv.text = keyword
|
||||
tv.textSize = 11f // 字体大小 11sp
|
||||
tv.setTextColor(Color.parseColor("#1B1F1A"))
|
||||
tv.setPadding(30, 16, 30, 16)
|
||||
tv.setBackgroundResource(R.drawable.history_item_bg)
|
||||
|
||||
val params = FlexboxLayout.LayoutParams(
|
||||
FlexboxLayout.LayoutParams.WRAP_CONTENT,
|
||||
FlexboxLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.setMargins(10, 10, 10, 10)
|
||||
tv.layoutParams = params
|
||||
|
||||
tv.setOnClickListener {
|
||||
etInput.setText(keyword)
|
||||
etInput.setSelection(keyword.length)
|
||||
}
|
||||
|
||||
return tv
|
||||
}
|
||||
|
||||
|
||||
/** 清空历史记录 */
|
||||
private fun clearHistory() {
|
||||
val prefs = requireContext().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
prefs.edit().clear().apply()
|
||||
|
||||
historyLayout.removeAllViews()
|
||||
historySection.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.myapplication.ui.shop.search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.example.myapplication.R
|
||||
|
||||
class SearchResultFragment : Fragment() {
|
||||
|
||||
private lateinit var etInput: EditText
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_search_result, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// 返回按钮
|
||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
etInput = view.findViewById(R.id.et_input)
|
||||
|
||||
// ⭐ 接收从上一个页面传来的搜索词
|
||||
val keyword = arguments?.getString("search_keyword") ?: ""
|
||||
etInput.setText(keyword)
|
||||
// etInput.setSelection(keyword.length) // 光标移动到最后
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.myapplication.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.myapplication.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MyApplicationTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/com/example/myapplication/ui/theme/Type.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.example.myapplication.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
5
app/src/main/res/anim/fade_in.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
5
app/src/main/res/anim/fade_in_fast.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
5
app/src/main/res/anim/fade_out.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
5
app/src/main/res/anim/fade_out_fast.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
7
app/src/main/res/color/nav_text_color.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 选中时文字颜色 #4EDBC5 -->
|
||||
<item android:color="#4EDBC5" android:state_checked="true" />
|
||||
<!-- 未选中时的文字颜色,自定义一个你喜欢的 -->
|
||||
<item android:color="#8A8A8A" />
|
||||
</selector>
|
||||
5
app/src/main/res/color/track_color.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#30CC65" android:state_checked="true"/> <!-- ON 绿色 -->
|
||||
<item android:color="#D6D6D6"/> <!-- OFF 灰色 -->
|
||||
</selector>
|
||||
8
app/src/main/res/drawable/activity_onboarding_bg.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:startColor="#F3FFF8"
|
||||
android:centerColor="#E1FDFF"
|
||||
android:endColor="#FFFFFF"
|
||||
android:angle="-90"/>
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/agreement.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable/ai_dialogue.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/drawable/avatar_modification.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/drawable/bg.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
11
app/src/main/res/drawable/bg_splash.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<!-- 启动页 -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 背景图 -->
|
||||
<item>
|
||||
<bitmap
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/startup_page"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
BIN
app/src/main/res/drawable/blue_star.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
5
app/src/main/res/drawable/bs_handle_bg.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="#33000000"/>
|
||||
</shape>
|
||||
20
app/src/main/res/drawable/btn_keyboard.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 按下状态 -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#CCCCCC"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<stroke android:width="1dp" android:color="#999999"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- 默认状态 -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<stroke android:width="1dp" android:color="#DDDDDD"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
29
app/src/main/res/drawable/btn_keyboard_function.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Shift键激活状态 -->
|
||||
<item android:state_activated="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#4CAF50"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<stroke android:width="1dp" android:color="#388E3C"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- 按下状态 -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#9E9E9E"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<stroke android:width="1dp" android:color="#757575"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- 默认状态 -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#E0E0E0"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<stroke android:width="1dp" android:color="#BDBDBD"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
BIN
app/src/main/res/drawable/button_bg.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
app/src/main/res/drawable/chat_persona.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/drawable/check_the_box.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
app/src/main/res/drawable/circle.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
5
app/src/main/res/drawable/circle_selector.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/selected_circle" android:state_checked="true"/>
|
||||
<item android:drawable="@drawable/circle"/>
|
||||
</selector>
|
||||
BIN
app/src/main/res/drawable/copy.png
Normal file
|
After Width: | Height: | Size: 702 B |
BIN
app/src/main/res/drawable/delete_icon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable/e_mail.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable/emotional_counseling.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/drawable/feedback.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable/female.png
Normal file
|
After Width: | Height: | Size: 344 KiB |
BIN
app/src/main/res/drawable/first_place.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
app/src/main/res/drawable/five_star_review.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/drawable/floating.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
5
app/src/main/res/drawable/gender_background.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#FAFAFA"/>
|
||||
<corners android:radius="11dp"/>
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/gold_coin.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#EDFFFD"/>
|
||||
<corners android:radius="50dp"/>
|
||||
<!-- 白色边框 -->
|
||||
<stroke android:width="1dp" android:color="#FFFFFF"/>
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/gold_coin_button.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
14
app/src/main/res/drawable/gold_coin_recharge_bt_bg.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF" />
|
||||
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#FFFFFF" />
|
||||
|
||||
<corners
|
||||
android:topLeftRadius="36dp"
|
||||
android:topRightRadius="36dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp" />
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/gold_coin_recharge_button_bg.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
11
app/src/main/res/drawable/gold_coin_recharge_package_bg.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<gradient
|
||||
android:startColor="#F1F1F1"
|
||||
android:endColor="#FFFFFF"
|
||||
android:angle="-90" /> <!-- 90 = 从上到下渐变 -->
|
||||
|
||||
<corners android:radius="10dp" />
|
||||
|
||||
</shape>
|
||||
6
app/src/main/res/drawable/history_item_bg.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#F1F1F1"/>
|
||||
<corners android:radius="50dp"/>
|
||||
<padding android:left="15dp" android:right="15dp" android:top="8dp" android:bottom="8dp"/>
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/home.png
Normal file
|
After Width: | Height: | Size: 766 B |
7
app/src/main/res/drawable/home_selector.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 选中状态图标 -->
|
||||
<item android:drawable="@drawable/selected_home" android:state_checked="true"/>
|
||||
<!-- 默认图标 -->
|
||||
<item android:drawable="@drawable/home"/>
|
||||
</selector>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
4
app/src/main/res/drawable/keyboard_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/white" />
|
||||
</selector>
|
||||
4
app/src/main/res/drawable/keyboard_datail_display.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="7dp"/>
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/keyboard_elements.png
Normal file
|
After Width: | Height: | Size: 268 B |
5
app/src/main/res/drawable/keyboard_ettings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#02BEAC"/>
|
||||
<corners android:radius="50dp"/>
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/list_two_bg.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#111111" /> <!-- 背景色 -->
|
||||
<corners android:radius="5dp" /> <!-- 圆角半径,越大越圆 -->
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/male.png
Normal file
|
After Width: | Height: | Size: 318 KiB |
BIN
app/src/main/res/drawable/member_recharge.png
Normal file
|
After Width: | Height: | Size: 165 KiB |