基本优化完毕
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".SplashActivity"
|
android:name=".SplashActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.MyApp.Splash">
|
android:theme="@style/Theme.MyApp.Splash">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -30,22 +33,26 @@
|
|||||||
<!-- 输入法激活页(强烈建议增加) -->
|
<!-- 输入法激活页(强烈建议增加) -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ImeGuideActivity"
|
android:name=".ImeGuideActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:exported="true"/>
|
android:exported="true"/>
|
||||||
|
|
||||||
<!-- 输入法体验页 -->
|
<!-- 输入法体验页 -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".GuideActivity"
|
android:name=".GuideActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="stateHidden|adjustNothing" />
|
android:windowSoftInputMode="stateHidden|adjustNothing" />
|
||||||
|
|
||||||
<!-- 引导页 -->
|
<!-- 引导页 -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".OnboardingActivity"
|
android:name=".OnboardingActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<!-- 主界面 -->
|
<!-- 主界面 -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
@@ -63,5 +70,16 @@
|
|||||||
android:name="android.view.im"
|
android:name="android.view.im"
|
||||||
android:resource="@xml/method" />
|
android:resource="@xml/method" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<!-- FileProvider for camera image capture -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -105,9 +105,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
openGlobal(R.id.loginFragment)
|
openGlobal(R.id.loginFragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is AuthEvent.GenericError -> {
|
is AuthEvent.GenericError -> {
|
||||||
Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录成功事件处理
|
// 登录成功事件处理
|
||||||
is AuthEvent.LoginSuccess -> {
|
is AuthEvent.LoginSuccess -> {
|
||||||
// 关闭 global overlay:回到 empty
|
// 关闭 global overlay:回到 empty
|
||||||
@@ -123,7 +125,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingTabAfterLogin = null
|
pendingTabAfterLogin = null
|
||||||
|
|
||||||
|
// ✅ 登录成功后也刷新一次
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登出事件处理
|
// 登出事件处理
|
||||||
is AuthEvent.Logout -> {
|
is AuthEvent.Logout -> {
|
||||||
pendingTabAfterLogin = event.returnTabTag
|
pendingTabAfterLogin = event.returnTabTag
|
||||||
@@ -136,11 +142,20 @@ class MainActivity : AppCompatActivity() {
|
|||||||
openGlobal(R.id.loginFragment) // ✅ 退出登录后立刻打开登录页
|
openGlobal(R.id.loginFragment) // ✅ 退出登录后立刻打开登录页
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开全局页面事件处理
|
// 打开全局页面事件处理
|
||||||
is AuthEvent.OpenGlobalPage -> {
|
is AuthEvent.OpenGlobalPage -> {
|
||||||
// 打开指定的全局页面
|
|
||||||
openGlobal(event.destinationId, event.bundle)
|
openGlobal(event.destinationId, event.bundle)
|
||||||
}
|
}
|
||||||
|
is AuthEvent.UserUpdated -> {
|
||||||
|
// 不需要处理
|
||||||
|
}
|
||||||
|
is AuthEvent.CharacterDeleted -> {
|
||||||
|
// 不需要处理
|
||||||
|
}
|
||||||
|
is AuthEvent.CharacterAdded -> {
|
||||||
|
// 不需要处理,由HomeFragment处理
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,9 +173,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
TAB_MINE -> R.id.mine_graph
|
TAB_MINE -> R.id.mine_graph
|
||||||
else -> R.id.home_graph
|
else -> R.id.home_graph
|
||||||
}
|
}
|
||||||
|
updateBottomNavVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// ✅ 最终兜底:从后台回来 / 某些场景没触发 listener,也能恢复底栏
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun initHosts() {
|
private fun initHosts() {
|
||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
|
|
||||||
@@ -199,6 +221,38 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// 绑定底部导航栏可见性监听
|
// 绑定底部导航栏可见性监听
|
||||||
bindBottomNavVisibilityForTabs()
|
bindBottomNavVisibilityForTabs()
|
||||||
|
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这些页面需要隐藏底部导航栏:你按需加/减
|
||||||
|
*/
|
||||||
|
private fun shouldHideBottomNav(destId: Int): Boolean {
|
||||||
|
return destId in setOf(
|
||||||
|
R.id.searchFragment,
|
||||||
|
R.id.searchResultFragment,
|
||||||
|
R.id.MySkin,
|
||||||
|
R.id.notificationFragment,
|
||||||
|
R.id.feedbackFragment,
|
||||||
|
R.id.MyKeyboard,
|
||||||
|
R.id.PersonalSettings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ 统一底栏显隐逻辑:任何地方状态变化都调用它
|
||||||
|
*/
|
||||||
|
private fun updateBottomNavVisibility() {
|
||||||
|
// ✅ 只要 global overlay 不在 empty,底栏必须隐藏(用 NavController 判断,别用 View.visibility)
|
||||||
|
if (isGlobalVisible()) {
|
||||||
|
bottomNav.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则按“当前可见 tab 的当前目的地”判断
|
||||||
|
val destId = currentTabNavController.currentDestination?.id
|
||||||
|
bottomNav.visibility = if (destId != null && shouldHideBottomNav(destId)) View.GONE else View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindGlobalVisibility() {
|
private fun bindGlobalVisibility() {
|
||||||
@@ -207,11 +261,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
findViewById<View>(R.id.global_container).visibility =
|
findViewById<View>(R.id.global_container).visibility =
|
||||||
if (isEmpty) View.GONE else View.VISIBLE
|
if (isEmpty) View.GONE else View.VISIBLE
|
||||||
bottomNav.visibility =
|
|
||||||
if (isEmpty) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
// ✅ 只在"刚从某个全局页关闭回 empty"时触发回退逻辑
|
// ✅ 底栏统一走 update
|
||||||
val justClosedOverlay = (dest.id == R.id.globalEmptyFragment && lastGlobalDestId != R.id.globalEmptyFragment)
|
updateBottomNavVisibility()
|
||||||
|
|
||||||
|
val justClosedOverlay =
|
||||||
|
(dest.id == R.id.globalEmptyFragment && lastGlobalDestId != R.id.globalEmptyFragment)
|
||||||
lastGlobalDestId = dest.id
|
lastGlobalDestId = dest.id
|
||||||
|
|
||||||
if (justClosedOverlay) {
|
if (justClosedOverlay) {
|
||||||
@@ -220,16 +275,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
TAB_MINE -> R.id.mine_graph
|
TAB_MINE -> R.id.mine_graph
|
||||||
else -> R.id.home_graph
|
else -> R.id.home_graph
|
||||||
}
|
}
|
||||||
// 未登录且当前处在受保护tab:强制回首页
|
|
||||||
if (!isLoggedIn() && currentTabGraphId in protectedTabs) {
|
if (!isLoggedIn() && currentTabGraphId in protectedTabs) {
|
||||||
switchTab(TAB_HOME, force = true)
|
switchTab(TAB_HOME, force = true)
|
||||||
bottomNav.selectedItemId = R.id.home_graph
|
bottomNav.selectedItemId = R.id.home_graph
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 只有"没登录就关闭登录页"才清 pending
|
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
pendingTabAfterLogin = null
|
pendingTabAfterLogin = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +294,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (!force && targetTag == currentTabTag) return
|
if (!force && targetTag == currentTabTag) return
|
||||||
|
|
||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.isStateSaved) return // ✅ 防崩:stateSaved 时不做事务
|
if (fm.isStateSaved) return
|
||||||
|
|
||||||
currentTabTag = targetTag
|
currentTabTag = targetTag
|
||||||
|
|
||||||
@@ -255,40 +311,30 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
|
// ✅ 关键:hide/show 切 tab 不会触发 destinationChanged,所以手动刷新
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开全局页(login/recharge等) */
|
/** 打开全局页(login/recharge等) */
|
||||||
private fun openGlobal(destId: Int, bundle: Bundle? = null) {
|
private fun openGlobal(destId: Int, bundle: Bundle? = null) {
|
||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.isStateSaved) return // ✅ 防崩
|
if (fm.isStateSaved) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (bundle != null) {
|
if (bundle != null) globalNavController.navigate(destId, bundle)
|
||||||
globalNavController.navigate(destId, bundle)
|
else globalNavController.navigate(destId)
|
||||||
} else {
|
|
||||||
globalNavController.navigate(destId)
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
// 可选:防止偶发重复 navigate 崩溃
|
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关闭全局页:pop到 empty */
|
/** Tab 内页面变化时刷新底栏显隐 */
|
||||||
private fun bindBottomNavVisibilityForTabs() {
|
private fun bindBottomNavVisibilityForTabs() {
|
||||||
fun shouldHideBottomNav(destId: Int): Boolean {
|
val listener = NavController.OnDestinationChangedListener { _, _, _ ->
|
||||||
return destId in setOf(
|
updateBottomNavVisibility()
|
||||||
R.id.searchFragment,
|
|
||||||
R.id.searchResultFragment,
|
|
||||||
R.id.MySkin
|
|
||||||
// 你还有其他需要全屏的页,也加在这里
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val listener = NavController.OnDestinationChangedListener { _, dest, _ ->
|
|
||||||
// 只要 global overlay 打开了,仍然以 overlay 为准(你已有逻辑)
|
|
||||||
if (isGlobalVisible()) return@OnDestinationChangedListener
|
|
||||||
bottomNav.visibility = if (shouldHideBottomNav(dest.id)) View.GONE else View.VISIBLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
homeHost.navController.addOnDestinationChangedListener(listener)
|
homeHost.navController.addOnDestinationChangedListener(listener)
|
||||||
@@ -300,12 +346,20 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (!isGlobalVisible()) return false
|
if (!isGlobalVisible()) return false
|
||||||
|
|
||||||
val popped = globalNavController.popBackStack()
|
val popped = globalNavController.popBackStack()
|
||||||
val stillVisible = globalNavController.currentDestination?.id != R.id.globalEmptyFragment
|
|
||||||
return popped || stillVisible
|
// ✅ pop 后刷新一次(注意:currentDestination 可能要等一帧才更新,所以 post)
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
|
|
||||||
|
// popped = true 表示确实 pop 了;即使 popped=false 也可能已经在 empty 了
|
||||||
|
return popped || !isGlobalVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ 改这里:不要再用 View.visibility 判断 overlay
|
||||||
|
* 以 NavController 的目的地为准
|
||||||
|
*/
|
||||||
private fun isGlobalVisible(): Boolean {
|
private fun isGlobalVisible(): Boolean {
|
||||||
return findViewById<View>(R.id.global_container).visibility == View.VISIBLE
|
return globalNavController.currentDestination?.id != R.id.globalEmptyFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupBackPress() {
|
private fun setupBackPress() {
|
||||||
@@ -316,14 +370,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// 2) 再 pop 当前tab
|
// 2) 再 pop 当前tab
|
||||||
val popped = currentTabNavController.popBackStack()
|
val popped = currentTabNavController.popBackStack()
|
||||||
if (popped) return
|
if (popped) {
|
||||||
|
bottomNav.post { updateBottomNavVisibility() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 3) 当前tab到根了:如果不是home,切回home;否则退出
|
// 3) 当前tab到根了:如果不是home,切回home;否则退出
|
||||||
if (currentTabTag != TAB_HOME) {
|
if (currentTabTag != TAB_HOME) {
|
||||||
bottomNav.post {
|
bottomNav.post { bottomNav.selectedItemId = R.id.home_graph }
|
||||||
bottomNav.selectedItemId = R.id.home_graph
|
switchTab(TAB_HOME)
|
||||||
}
|
|
||||||
switchTab(TAB_HOME)
|
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ import android.graphics.drawable.GradientDrawable
|
|||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import java.text.BreakIterator
|
import java.text.BreakIterator
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|
||||||
|
|
||||||
class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
||||||
@@ -264,23 +266,6 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
|
|
||||||
createNotificationChannelIfNeeded()
|
createNotificationChannelIfNeeded()
|
||||||
tryStartForegroundSafe()
|
tryStartForegroundSafe()
|
||||||
|
|
||||||
// 监听认证事件
|
|
||||||
// CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
// AuthEventBus.events.collectLatest { event ->
|
|
||||||
// if (event is AuthEvent.TokenExpired) {
|
|
||||||
// // 启动 MainActivity 并跳转到登录页面
|
|
||||||
// val intent = Intent(this@MyInputMethodService, MainActivity::class.java).apply {
|
|
||||||
// flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
// putExtra("navigate_to", "loginFragment")
|
|
||||||
// }
|
|
||||||
// startActivity(intent)
|
|
||||||
// } else if (event is AuthEvent.GenericError) {
|
|
||||||
// // 显示错误提示
|
|
||||||
// android.widget.Toast.makeText(this@MyInputMethodService, "请求失败: ${event.message}", android.widget.Toast.LENGTH_SHORT).show()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入法状态变化
|
// 输入法状态变化
|
||||||
@@ -319,6 +304,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
val keyboard = ensureMainKeyboard()
|
val keyboard = ensureMainKeyboard()
|
||||||
currentKeyboardView = keyboard.rootView
|
currentKeyboardView = keyboard.rootView
|
||||||
mainKeyboardView = keyboard.rootView
|
mainKeyboardView = keyboard.rootView
|
||||||
|
(keyboard.rootView.parent as? ViewGroup)?.removeView(keyboard.rootView)
|
||||||
return keyboard.rootView
|
return keyboard.rootView
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +391,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
|
|
||||||
// 初始状态:隐藏联想条,显示控制面板
|
// 初始状态:隐藏联想条,显示控制面板
|
||||||
mainKeyboardView
|
mainKeyboardView
|
||||||
?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
|
?.findViewById<ConstraintLayout>(R.id.completion_scroll)
|
||||||
?.visibility = View.GONE
|
?.visibility = View.GONE
|
||||||
|
|
||||||
mainKeyboardView
|
mainKeyboardView
|
||||||
@@ -612,7 +598,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
clearEditorState()
|
clearEditorState()
|
||||||
val kb = ensureMainKeyboard()
|
val kb = ensureMainKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputViewSafely(kb.rootView)
|
||||||
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +606,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
clearEditorState()
|
clearEditorState()
|
||||||
val kb = ensureNumberKeyboard()
|
val kb = ensureNumberKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputViewSafely(kb.rootView)
|
||||||
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,7 +614,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
clearEditorState()
|
clearEditorState()
|
||||||
val kb = ensureSymbolKeyboard()
|
val kb = ensureSymbolKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputViewSafely(kb.rootView)
|
||||||
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,7 +622,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
clearEditorState()
|
clearEditorState()
|
||||||
val kb = ensureAiKeyboard()
|
val kb = ensureAiKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputViewSafely(kb.rootView)
|
||||||
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,10 +630,39 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
clearEditorState()
|
clearEditorState()
|
||||||
val kb = ensureEmojiKeyboard()
|
val kb = ensureEmojiKeyboard()
|
||||||
currentKeyboardView = kb.rootView
|
currentKeyboardView = kb.rootView
|
||||||
setInputView(kb.rootView)
|
setInputViewSafely(kb.rootView)
|
||||||
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
kb.applyTheme(currentTextColor, currentBorderColor, currentBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun associateClose() {
|
||||||
|
clearEditorState()
|
||||||
|
val kb = ensureEmojiKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
// 先清理缓存,避免复用旧 View
|
||||||
|
currentKeyboardView = null
|
||||||
|
|
||||||
|
mainKeyboardView = null
|
||||||
|
numberKeyboardView = null
|
||||||
|
symbolKeyboardView = null
|
||||||
|
aiKeyboardView = null
|
||||||
|
emojiKeyboardView = null
|
||||||
|
|
||||||
|
mainKeyboard = null
|
||||||
|
numberKeyboard = null
|
||||||
|
symbolKeyboard = null
|
||||||
|
aiKeyboard = null
|
||||||
|
emojiKeyboard = null
|
||||||
|
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInputViewSafely(v: View) {
|
||||||
|
(v.parent as? ViewGroup)?.removeView(v)
|
||||||
|
super.setInputView(v)
|
||||||
|
}
|
||||||
|
|
||||||
// Emoji 键盘
|
// Emoji 键盘
|
||||||
private fun ensureEmojiKeyboard(): com.example.myapplication.keyboard.EmojiKeyboard {
|
private fun ensureEmojiKeyboard(): com.example.myapplication.keyboard.EmojiKeyboard {
|
||||||
if (emojiKeyboard == null) {
|
if (emojiKeyboard == null) {
|
||||||
@@ -943,7 +958,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
|
|
||||||
// 新增:联想滚动条 & 控制面板
|
// 新增:联想滚动条 & 控制面板
|
||||||
val completionScroll =
|
val completionScroll =
|
||||||
mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
|
mainKeyboardView?.findViewById<ConstraintLayout>(R.id.completion_scroll)
|
||||||
val controlLayout =
|
val controlLayout =
|
||||||
mainKeyboardView?.findViewById<LinearLayout>(R.id.control_layout)
|
mainKeyboardView?.findViewById<LinearLayout>(R.id.control_layout)
|
||||||
|
|
||||||
@@ -1006,7 +1021,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
|
|
||||||
// 自动滚回到最左边
|
// 自动滚回到最左边
|
||||||
private fun scrollSuggestionsToStart() {
|
private fun scrollSuggestionsToStart() {
|
||||||
val sv = mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
|
val sv = mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_HorizontalScrollView)
|
||||||
sv?.post { sv.fullScroll(View.FOCUS_LEFT) }
|
sv?.post { sv.fullScroll(View.FOCUS_LEFT) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1402,7 +1417,7 @@ class MyInputMethodService : InputMethodService(), KeyboardEnvironment {
|
|||||||
// 4. UI:联想条隐藏 & 控制面板显示
|
// 4. UI:联想条隐藏 & 控制面板显示
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
val completionScroll =
|
val completionScroll =
|
||||||
mainKeyboardView?.findViewById<HorizontalScrollView>(R.id.completion_scroll)
|
mainKeyboardView?.findViewById<ConstraintLayout>(R.id.completion_scroll)
|
||||||
val controlLayout =
|
val controlLayout =
|
||||||
mainKeyboardView?.findViewById<LinearLayout>(R.id.control_layout)
|
mainKeyboardView?.findViewById<LinearLayout>(R.id.control_layout)
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,64 @@ package com.example.myapplication
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.example.myapplication.ui.common.LoadingOverlay
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
|
||||||
class OnboardingActivity : AppCompatActivity() {
|
class OnboardingActivity : AppCompatActivity() {
|
||||||
|
private var selectedGender = -1 // 0: male, 1: female, 2: third
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_onboarding)
|
setContentView(R.layout.activity_onboarding)
|
||||||
|
|
||||||
val btnStart = findViewById<TextView>(R.id.tv_skip)
|
val btnStart = findViewById<TextView>(R.id.tv_skip)
|
||||||
|
val maleLayout = findViewById<LinearLayout>(R.id.male_layout)
|
||||||
|
val femaleLayout = findViewById<LinearLayout>(R.id.female_layout)
|
||||||
|
val thirdLayout = findViewById<LinearLayout>(R.id.third_layout)
|
||||||
|
val tvDescription = findViewById<TextView>(R.id.tv_description)
|
||||||
|
|
||||||
|
// 设置性别选择点击事件
|
||||||
|
maleLayout.setOnClickListener {
|
||||||
|
resetAllLayouts()
|
||||||
|
maleLayout.setBackgroundResource(R.drawable.gender_background_select)
|
||||||
|
selectedGender = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
femaleLayout.setOnClickListener {
|
||||||
|
resetAllLayouts()
|
||||||
|
femaleLayout.setBackgroundResource(R.drawable.gender_background_select)
|
||||||
|
selectedGender = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
thirdLayout.setOnClickListener {
|
||||||
|
resetAllLayouts()
|
||||||
|
thirdLayout.setBackgroundResource(R.drawable.gender_background_select)
|
||||||
|
selectedGender = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
tvDescription.setOnClickListener {
|
||||||
|
if (selectedGender != -1) {
|
||||||
|
// 这里可以获取selectedGender的值(0,1,2)
|
||||||
|
// 标记已经不是第一次启动了
|
||||||
|
val prefs = getSharedPreferences("app_prefs", MODE_PRIVATE)
|
||||||
|
prefs.edit().putBoolean("is_first_launch", false).apply()
|
||||||
|
EncryptedSharedPreferencesUtil.save(this, "gender", selectedGender.toString())
|
||||||
|
// 跳转到主界面
|
||||||
|
val rootView = window.decorView.findViewById<ViewGroup>(android.R.id.content)
|
||||||
|
LoadingOverlay.attach(rootView).show()
|
||||||
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}else{
|
||||||
|
Toast.makeText(this, "Please select your gender.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
btnStart.setOnClickListener {
|
btnStart.setOnClickListener {
|
||||||
// 标记已经不是第一次启动了
|
// 标记已经不是第一次启动了
|
||||||
@@ -20,8 +68,16 @@ class OnboardingActivity : AppCompatActivity() {
|
|||||||
prefs.edit().putBoolean("is_first_launch", false).apply()
|
prefs.edit().putBoolean("is_first_launch", false).apply()
|
||||||
|
|
||||||
// 跳转到主界面
|
// 跳转到主界面
|
||||||
|
val rootView = window.decorView.findViewById<ViewGroup>(android.R.id.content)
|
||||||
|
LoadingOverlay.attach(rootView).show()
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resetAllLayouts() {
|
||||||
|
findViewById<LinearLayout>(R.id.male_layout).setBackgroundResource(R.drawable.gender_background)
|
||||||
|
findViewById<LinearLayout>(R.id.female_layout).setBackgroundResource(R.drawable.gender_background)
|
||||||
|
findViewById<LinearLayout>(R.id.third_layout).setBackgroundResource(R.drawable.gender_background)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ interface KeyboardEnvironment {
|
|||||||
fun showAiKeyboard()
|
fun showAiKeyboard()
|
||||||
//emoji键盘
|
//emoji键盘
|
||||||
fun showEmojiKeyboard()
|
fun showEmojiKeyboard()
|
||||||
|
// 关闭联想
|
||||||
|
fun associateClose()
|
||||||
|
|
||||||
// 音效
|
// 音效
|
||||||
fun playKeyClick()
|
fun playKeyClick()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.PopupWindow
|
import android.widget.PopupWindow
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import com.example.myapplication.theme.ThemeManager
|
import com.example.myapplication.theme.ThemeManager
|
||||||
|
|
||||||
class MainKeyboard(
|
class MainKeyboard(
|
||||||
@@ -159,6 +160,10 @@ class MainKeyboard(
|
|||||||
updateRevokeButtonVisibility(view, res, pkg)
|
updateRevokeButtonVisibility(view, res, pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view.findViewById<LinearLayout?>(res.getIdentifier("associate_close", "id", pkg))?.setOnClickListener {
|
||||||
|
vibrateKey();env.associateClose()
|
||||||
|
}
|
||||||
|
|
||||||
view.findViewById<View?>(res.getIdentifier("key_emoji", "id", pkg))?.setOnClickListener {
|
view.findViewById<View?>(res.getIdentifier("key_emoji", "id", pkg))?.setOnClickListener {
|
||||||
vibrateKey(); env.showEmojiKeyboard()
|
vibrateKey(); env.showEmojiKeyboard()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// 请求方法
|
// 请求方法
|
||||||
package com.example.myapplication.network
|
package com.example.myapplication.network
|
||||||
|
|
||||||
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
@@ -65,9 +66,41 @@ interface ApiService {
|
|||||||
@POST("user/updateInfo")
|
@POST("user/updateInfo")
|
||||||
suspend fun updateUserInfo(
|
suspend fun updateUserInfo(
|
||||||
@Body body: updateInfoRequest
|
@Body body: updateInfoRequest
|
||||||
): ApiResponse<User>
|
): ApiResponse<Boolean>
|
||||||
|
|
||||||
|
//分页查询钱包交易记录
|
||||||
|
@POST("wallet/transactions")
|
||||||
|
suspend fun transactions(
|
||||||
|
@Body body: transactionsRequest
|
||||||
|
): ApiResponse<transactionsResponse>
|
||||||
|
|
||||||
|
|
||||||
|
//用户人设列表
|
||||||
|
@GET("character/listByUser")
|
||||||
|
suspend fun listByUser(
|
||||||
|
): ApiResponse<List<ListByUserWithNot>>
|
||||||
|
|
||||||
|
//更新用户人设排序
|
||||||
|
@POST("character/updateUserCharacterSort")
|
||||||
|
suspend fun updateUserCharacterSort(
|
||||||
|
@Body body: updateUserCharacterSortRequest
|
||||||
|
): ApiResponse<Boolean>
|
||||||
|
|
||||||
|
// 删除用户人设
|
||||||
|
@GET("character/delUserCharacter")
|
||||||
|
suspend fun delUserCharacter(
|
||||||
|
@Query("id") id: Int
|
||||||
|
): ApiResponse<Boolean>
|
||||||
|
|
||||||
|
//提交反馈
|
||||||
|
@POST("user/feedback")
|
||||||
|
suspend fun feedback(
|
||||||
|
@Body body: feedbackRequest
|
||||||
|
): ApiResponse<Boolean>
|
||||||
|
|
||||||
|
@GET("user/inviteCode")
|
||||||
|
suspend fun inviteCode(
|
||||||
|
): ApiResponse<ShareResponse>
|
||||||
//===========================================首页=================================
|
//===========================================首页=================================
|
||||||
// 标签列表
|
// 标签列表
|
||||||
@GET("tag/list")
|
@GET("tag/list")
|
||||||
@@ -102,17 +135,11 @@ interface ApiService {
|
|||||||
@Query("id") id: Int
|
@Query("id") id: Int
|
||||||
): ApiResponse<CharacterDetailResponse>
|
): ApiResponse<CharacterDetailResponse>
|
||||||
|
|
||||||
//删除用户人设
|
|
||||||
@GET("character/delUserCharacter")
|
|
||||||
suspend fun delUserCharacter(
|
|
||||||
@Query("id") id: Int
|
|
||||||
): ApiResponse<Unit>
|
|
||||||
|
|
||||||
//添加用户人设
|
//添加用户人设
|
||||||
@POST("character/addUserCharacter")
|
@POST("character/addUserCharacter")
|
||||||
suspend fun addUserCharacter(
|
suspend fun addUserCharacter(
|
||||||
@Body body: AddPersonaClick
|
@Body body: AddPersonaClick
|
||||||
): ApiResponse<Unit>
|
): ApiResponse<Boolean>
|
||||||
|
|
||||||
//==========================================商城===========================================
|
//==========================================商城===========================================
|
||||||
|
|
||||||
@@ -188,4 +215,18 @@ interface ApiService {
|
|||||||
suspend fun downloadZipFromUrl(
|
suspend fun downloadZipFromUrl(
|
||||||
@Url url: String // 完整的下载 URL
|
@Url url: String // 完整的下载 URL
|
||||||
): Response<ResponseBody>
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传服务接口
|
||||||
|
*/
|
||||||
|
interface FileUploadService {
|
||||||
|
@Multipart
|
||||||
|
@POST("file/upload")
|
||||||
|
suspend fun uploadFile(
|
||||||
|
@Query("file") fileQuery: String,
|
||||||
|
@Part file: MultipartBody.Part
|
||||||
|
): ApiResponse<String>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||||||
|
|
||||||
object AuthEventBus {
|
object AuthEventBus {
|
||||||
|
|
||||||
// replay=0:不缓存历史事件;extraBufferCapacity:避免瞬时丢事件
|
// replay=1:缓存最近一次事件;extraBufferCapacity=64:增加缓冲区防止瞬时事件丢失
|
||||||
private val _events = MutableSharedFlow<AuthEvent>(
|
private val _events = MutableSharedFlow<AuthEvent>(
|
||||||
replay = 0,
|
replay = 0,
|
||||||
extraBufferCapacity = 1
|
extraBufferCapacity = 1
|
||||||
@@ -21,8 +21,11 @@ object AuthEventBus {
|
|||||||
|
|
||||||
sealed class AuthEvent {
|
sealed class AuthEvent {
|
||||||
data class TokenExpired(val message: String? = null) : AuthEvent()
|
data class TokenExpired(val message: String? = null) : AuthEvent()
|
||||||
|
data class CharacterAdded(val personaId: Int, val newAdded: Boolean = false) : AuthEvent()
|
||||||
data class GenericError(val message: String) : AuthEvent()
|
data class GenericError(val message: String) : AuthEvent()
|
||||||
object LoginSuccess : AuthEvent()
|
object LoginSuccess : AuthEvent()
|
||||||
data class Logout(val returnTabTag: String) : AuthEvent()
|
data class Logout(val returnTabTag: String) : AuthEvent()
|
||||||
data class OpenGlobalPage(val destinationId: Int, val bundle: Bundle? = null) : AuthEvent()
|
data class OpenGlobalPage(val destinationId: Int, val bundle: Bundle? = null) : AuthEvent()
|
||||||
}
|
object UserUpdated : AuthEvent()
|
||||||
|
data class CharacterDeleted(val characterId: Int) : AuthEvent()
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,16 @@ import okhttp3.Response
|
|||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.example.myapplication.network.security.BodyParamsExtractor
|
||||||
|
import com.example.myapplication.network.security.NonceUtils
|
||||||
|
import com.example.myapplication.network.security.SignUtils
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import okio.Buffer
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
// * 不需要登录的接口路径(相对完整路径)
|
// * 不需要登录的接口路径(相对完整路径)
|
||||||
// * 只写 /api/ 后面的部分
|
// * 只写 /api/ 后面的部分
|
||||||
@@ -20,57 +29,92 @@ private val NO_LOGIN_REQUIRED_PATHS = setOf(
|
|||||||
"/wallet/balance",
|
"/wallet/balance",
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun noLoginRequired(url: HttpUrl): Boolean {
|
private val NO_SIGN_REQUIRED_PATHS = setOf(
|
||||||
val path = url.encodedPath // 例:/api/home/banner
|
"/auth/login",
|
||||||
|
)
|
||||||
|
|
||||||
// 统一裁掉 /api 前缀
|
private fun apiPath(url: HttpUrl): String {
|
||||||
val apiPath = path.substringAfter("/api", path)
|
val path = url.encodedPath
|
||||||
|
return path.substringAfter("/api", path)
|
||||||
return NO_LOGIN_REQUIRED_PATHS.contains(apiPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun noLoginRequired(url: HttpUrl): Boolean =
|
||||||
|
NO_LOGIN_REQUIRED_PATHS.contains(apiPath(url))
|
||||||
|
|
||||||
|
private fun noSignRequired(url: HttpUrl): Boolean =
|
||||||
|
NO_SIGN_REQUIRED_PATHS.contains(apiPath(url))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求拦截器:统一加 Header、token 等
|
* 请求拦截器:统一加 Header、token 等
|
||||||
*/
|
*/
|
||||||
fun requestInterceptor(appContext: Context) = Interceptor { chain ->
|
fun requestInterceptor(appContext: Context) = Interceptor { chain ->
|
||||||
val original = chain.request()
|
val original = chain.request()
|
||||||
|
val url = original.url
|
||||||
|
|
||||||
val user = EncryptedSharedPreferencesUtil.get(appContext, "user", LoginResponse::class.java)
|
val user = EncryptedSharedPreferencesUtil.get(appContext, "user", LoginResponse::class.java)
|
||||||
val token = user?.token.orEmpty()
|
val token = user?.token.orEmpty()
|
||||||
|
|
||||||
val newRequest = original.newBuilder()
|
val builder = original.newBuilder()
|
||||||
.apply {
|
.apply {
|
||||||
if (token.isNotBlank()) {
|
if (token.isNotBlank()) addHeader("auth-token", token)
|
||||||
addHeader("auth-token", "$token")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.addHeader("Accept-Language", "lang")
|
.addHeader("Accept-Language", "lang")
|
||||||
.build()
|
|
||||||
|
|
||||||
// ===== 打印请求信息 =====
|
// ======= ✅ 按你规则加签名(header + query + body)=======
|
||||||
val request = newRequest
|
if (!noSignRequired(url)) {
|
||||||
val url = request.url
|
val appId = "loveKeyboard"
|
||||||
|
val secret = "kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H" // TODO 正式环境建议下发/混淆/NDK
|
||||||
|
|
||||||
|
val timestamp = (System.currentTimeMillis() / 1000).toString()
|
||||||
|
val nonce = java.util.UUID.randomUUID().toString().replace("-", "").take(16)
|
||||||
|
|
||||||
|
// 1) 合并成 Map(去掉 sign 本身)
|
||||||
|
val params = linkedMapOf<String, String>()
|
||||||
|
params["appId"] = appId
|
||||||
|
params["timestamp"] = timestamp
|
||||||
|
params["nonce"] = nonce
|
||||||
|
|
||||||
|
// 2) query 参数
|
||||||
|
for (i in 0 until url.querySize) {
|
||||||
|
params[url.queryParameterName(i)] = url.queryParameterValue(i).orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) body 参数(json / form)
|
||||||
|
params.putAll(extractBodyParams(original))
|
||||||
|
|
||||||
|
// 4) 生成 sign
|
||||||
|
val sign = calcSign(params, secret)
|
||||||
|
|
||||||
|
builder
|
||||||
|
.addHeader("X-App-Id", appId)
|
||||||
|
.addHeader("X-Timestamp", timestamp)
|
||||||
|
.addHeader("X-Nonce", nonce)
|
||||||
|
.addHeader("X-Sign", sign)
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = builder.build()
|
||||||
|
|
||||||
|
// ===== 打印请求信息(保留你原来的)=====
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("\n======== HTTP Request ========\n")
|
sb.append("\n======== HTTP Request ========\n")
|
||||||
sb.append("Method: ${request.method}\n")
|
sb.append("Method: ${request.method}\n")
|
||||||
sb.append("URL: $url\n")
|
sb.append("URL: ${request.url}\n")
|
||||||
|
|
||||||
sb.append("Headers:\n")
|
sb.append("Headers:\n")
|
||||||
for (name in request.headers.names()) {
|
for (name in request.headers.names()) {
|
||||||
sb.append(" $name: ${request.header(name)}\n")
|
sb.append(" $name: ${request.header(name)}\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.querySize > 0) {
|
if (request.url.querySize > 0) {
|
||||||
sb.append("Query Params:\n")
|
sb.append("Query Params:\n")
|
||||||
for (i in 0 until url.querySize) {
|
for (i in 0 until request.url.querySize) {
|
||||||
sb.append(" ${url.queryParameterName(i)} = ${url.queryParameterValue(i)}\n")
|
sb.append(" ${request.url.queryParameterName(i)} = ${request.url.queryParameterValue(i)}\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBody = request.body
|
val requestBody = request.body
|
||||||
if (requestBody != null) {
|
if (requestBody != null) {
|
||||||
val buffer = okio.Buffer()
|
val buffer = Buffer()
|
||||||
requestBody.writeTo(buffer)
|
requestBody.writeTo(buffer)
|
||||||
sb.append("Body:\n")
|
sb.append("Body:\n")
|
||||||
sb.append(buffer.readUtf8())
|
sb.append(buffer.readUtf8())
|
||||||
@@ -83,6 +127,132 @@ fun requestInterceptor(appContext: Context) = Interceptor { chain ->
|
|||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ================== 签名工具(严格按你描述规则) ==================
|
||||||
|
//
|
||||||
|
|
||||||
|
private fun calcSign(params: Map<String, String>, secret: String): String {
|
||||||
|
// 去空值 + 去 sign
|
||||||
|
val filtered = params
|
||||||
|
.filter { (k, v) -> v.isNotBlank() && !k.equals("sign", ignoreCase = true) }
|
||||||
|
|
||||||
|
// 按 key 字典序排序
|
||||||
|
val sorted = filtered.toSortedMap()
|
||||||
|
|
||||||
|
// 拼接:k=v&...&secret=xxx(value 统一做 URL encode 防止 & = 破坏结构)
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sorted.forEach { (k, v) ->
|
||||||
|
if (sb.isNotEmpty()) sb.append("&")
|
||||||
|
sb.append(k).append("=").append(urlEncode(v))
|
||||||
|
}
|
||||||
|
sb.append("&secret=").append(urlEncode(secret))
|
||||||
|
|
||||||
|
// HMAC-SHA256 -> hex小写
|
||||||
|
return hmacSha256Hex(sb.toString(), secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hmacSha256Hex(data: String, secret: String): String {
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
val keySpec = SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256")
|
||||||
|
mac.init(keySpec)
|
||||||
|
val bytes = mac.doFinal(data.toByteArray(Charsets.UTF_8))
|
||||||
|
return bytes.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun urlEncode(v: String): String =
|
||||||
|
URLEncoder.encode(v, "UTF-8")
|
||||||
|
|
||||||
|
//
|
||||||
|
// ================== Body 参数提取:json / form ==================
|
||||||
|
//
|
||||||
|
|
||||||
|
private fun extractBodyParams(request: okhttp3.Request): Map<String, String> {
|
||||||
|
val body = request.body ?: return emptyMap()
|
||||||
|
val ct = body.contentType()?.toString()?.lowercase().orEmpty()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
ct.contains("application/json") -> extractJsonBody(body)
|
||||||
|
ct.contains("application/x-www-form-urlencoded") -> extractFormBody(body)
|
||||||
|
else -> emptyMap() // multipart / stream 等默认不签 body(如需可再扩展)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractJsonBody(body: okhttp3.RequestBody): Map<String, String> {
|
||||||
|
val raw = bodyToString(body).trim()
|
||||||
|
if (raw.isBlank()) return emptyMap()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val root: JsonElement = JsonParser.parseString(raw)
|
||||||
|
val out = linkedMapOf<String, String>()
|
||||||
|
flattenJson(root, "", out)
|
||||||
|
out
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractFormBody(body: okhttp3.RequestBody): Map<String, String> {
|
||||||
|
val raw = bodyToString(body)
|
||||||
|
if (raw.isBlank()) return emptyMap()
|
||||||
|
|
||||||
|
val map = linkedMapOf<String, String>()
|
||||||
|
raw.split("&")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.forEach { pair ->
|
||||||
|
val idx = pair.indexOf("=")
|
||||||
|
if (idx > 0) {
|
||||||
|
val k = pair.substring(0, idx)
|
||||||
|
val v = pair.substring(idx + 1)
|
||||||
|
// form 这里解码回“原值”,后续签名阶段再统一 encode
|
||||||
|
map[k] = URLDecoder.decode(v, "UTF-8")
|
||||||
|
} else {
|
||||||
|
map[pair] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bodyToString(body: okhttp3.RequestBody): String {
|
||||||
|
return try {
|
||||||
|
val buffer = Buffer()
|
||||||
|
body.writeTo(buffer)
|
||||||
|
val charset = body.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8
|
||||||
|
buffer.readString(charset)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 扁平化规则:
|
||||||
|
* object: a.b.c
|
||||||
|
* array : items[0].id
|
||||||
|
*/
|
||||||
|
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
|
||||||
|
when {
|
||||||
|
elem.isJsonNull -> {
|
||||||
|
// null 不参与签名(服务端也要一致)
|
||||||
|
}
|
||||||
|
elem.isJsonPrimitive -> {
|
||||||
|
if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"')
|
||||||
|
}
|
||||||
|
elem.isJsonObject -> {
|
||||||
|
val obj = elem.asJsonObject
|
||||||
|
for ((k, v) in obj.entrySet()) {
|
||||||
|
val newKey = if (prefix.isBlank()) k else "$prefix.$k"
|
||||||
|
flattenJson(v, newKey, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elem.isJsonArray -> {
|
||||||
|
val arr = elem.asJsonArray
|
||||||
|
for (i in 0 until arr.size()) {
|
||||||
|
val newKey = "$prefix[$i]"
|
||||||
|
flattenJson(arr[i], newKey, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应拦截器:统一打印日志、做一些简单的错误处理
|
* 响应拦截器:统一打印日志、做一些简单的错误处理
|
||||||
@@ -121,7 +291,7 @@ val responseInterceptor = Interceptor { chain ->
|
|||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
val errorResponse = gson.fromJson(bodyString, ErrorResponse::class.java)
|
val errorResponse = gson.fromJson(bodyString, ErrorResponse::class.java)
|
||||||
|
|
||||||
if (errorResponse.code == 40102) {
|
if (errorResponse.code == 40102|| errorResponse.code == 40103) {
|
||||||
val isNoLoginApi = noLoginRequired(request.url)
|
val isNoLoginApi = noLoginRequired(request.url)
|
||||||
|
|
||||||
Log.w(
|
Log.w(
|
||||||
|
|||||||
@@ -71,16 +71,70 @@ data class User(
|
|||||||
val email: String,
|
val email: String,
|
||||||
val emailVerified: Boolean,
|
val emailVerified: Boolean,
|
||||||
val isVip: Boolean,
|
val isVip: Boolean,
|
||||||
val vipExpiry: String,
|
val vipExpiry: String?,
|
||||||
val token: String
|
val token: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
//更新用户
|
//更新用户
|
||||||
data class updateInfoRequest(
|
data class updateInfoRequest(
|
||||||
val uid: Long,
|
val uid: Long? = null,
|
||||||
val nickName: String,
|
val nickName: String? = null,
|
||||||
val gender: Int,
|
val gender: Int? = null,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
//分页查询钱包交易记录
|
||||||
|
data class transactionsRequest(
|
||||||
|
val pageNum: Int,
|
||||||
|
val pageSize: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
//分页查询钱包交易记录响应
|
||||||
|
data class transactionsResponse(
|
||||||
|
val records: List<TransactionRecord>,
|
||||||
|
val total: Int,
|
||||||
|
val size: Int,
|
||||||
|
val current: Int,
|
||||||
|
val pages: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
//分页查询钱包交易记录响应
|
||||||
|
data class TransactionRecord(
|
||||||
|
val id: Long,
|
||||||
|
val type: Int,
|
||||||
|
val amount: Number,
|
||||||
|
val beforeBalance: Number,
|
||||||
|
val afterBalance: Number,
|
||||||
|
val description: String,
|
||||||
|
val createdAt: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
//用户人设列表响应
|
||||||
|
data class ListByUserWithNot(
|
||||||
|
val id: Int,
|
||||||
|
val characterName: String,
|
||||||
|
val emoji: String,
|
||||||
|
val characterId: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
//更新用户人设排序
|
||||||
|
data class updateUserCharacterSortRequest(
|
||||||
|
val sort: List<Int>
|
||||||
|
)
|
||||||
|
|
||||||
|
//提交反馈
|
||||||
|
data class feedbackRequest(
|
||||||
|
val content: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
//分享响应
|
||||||
|
data class ShareResponse(
|
||||||
|
val code: String,
|
||||||
|
val status: Int,
|
||||||
|
val usedCount: Int,
|
||||||
|
val maxUses: Int,
|
||||||
|
val expiresAt: String,
|
||||||
|
val h5Link: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
// =======================================首页======================================
|
// =======================================首页======================================
|
||||||
@@ -115,7 +169,7 @@ data class listByTagWithNotLogin(
|
|||||||
|
|
||||||
// 人设详情响应
|
// 人设详情响应
|
||||||
data class CharacterDetailResponse(
|
data class CharacterDetailResponse(
|
||||||
val id: Long? = null,
|
val id: Int? = null,
|
||||||
val characterName: String? = null,
|
val characterName: String? = null,
|
||||||
val characterBackground: String? = null,
|
val characterBackground: String? = null,
|
||||||
val avatarUrl: String? = null,
|
val avatarUrl: String? = null,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import okhttp3.logging.HttpLoggingInterceptor
|
|||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import com.example.myapplication.network.FileUploadService
|
||||||
|
|
||||||
object RetrofitClient {
|
object RetrofitClient {
|
||||||
|
|
||||||
@@ -50,6 +51,13 @@ object RetrofitClient {
|
|||||||
retrofit.create(ApiService::class.java)
|
retrofit.create(ApiService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建文件上传服务
|
||||||
|
*/
|
||||||
|
fun createFileUploadService(): FileUploadService {
|
||||||
|
return retrofit.create(FileUploadService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建支持完整 URL 下载的 Retrofit 实例
|
* 创建支持完整 URL 下载的 Retrofit 实例
|
||||||
* @param baseUrl 完整的下载 URL
|
* @param baseUrl 完整的下载 URL
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package com.example.myapplication.network.security
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okio.Buffer
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
object BodyParamsExtractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽取 Request 的 body 参数到 map
|
||||||
|
* - JSON: 扁平化(a.b[0].c)
|
||||||
|
* - Form: 直接 key=value
|
||||||
|
* - Multipart: 只签文本字段(文件字段跳过)
|
||||||
|
*/
|
||||||
|
fun extractBodyParams(request: Request): Map<String, String> {
|
||||||
|
val body = request.body ?: return emptyMap()
|
||||||
|
val contentType = body.contentType()?.toString()?.lowercase().orEmpty()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
contentType.contains("application/json") -> extractJsonBody(body)
|
||||||
|
contentType.contains("application/x-www-form-urlencoded") -> extractFormBody(body)
|
||||||
|
contentType.contains("multipart/form-data") -> extractMultipartBody(body)
|
||||||
|
else -> {
|
||||||
|
// 其他类型(例如 stream、protobuf、octet-stream)
|
||||||
|
// 建议不签或签一个摘要(需要服务端同样实现)
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractJsonBody(body: RequestBody): Map<String, String> {
|
||||||
|
val json = bodyToString(body).trim()
|
||||||
|
if (json.isBlank()) return emptyMap()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val root: JsonElement = JsonParser.parseString(json)
|
||||||
|
val out = linkedMapOf<String, String>()
|
||||||
|
flattenJson(root, "", out)
|
||||||
|
out
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// JSON 解析失败就不签 body(也可以选择直接把原文作为 bodyRaw 参与签名)
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractFormBody(body: RequestBody): Map<String, String> {
|
||||||
|
// x-www-form-urlencoded 本质就是 querystring:a=1&b=2
|
||||||
|
val raw = bodyToString(body)
|
||||||
|
if (raw.isBlank()) return emptyMap()
|
||||||
|
|
||||||
|
val map = linkedMapOf<String, String>()
|
||||||
|
raw.split("&")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.forEach { pair ->
|
||||||
|
val idx = pair.indexOf("=")
|
||||||
|
if (idx > 0) {
|
||||||
|
val k = pair.substring(0, idx)
|
||||||
|
val v = pair.substring(idx + 1)
|
||||||
|
// 注意:这里 raw 是已经 urlencoded 的内容
|
||||||
|
// 为了与服务端一致,推荐:服务端拿到 form 参数的“解码后值”再参与签名
|
||||||
|
// 客户端这里可以不 decode,改为后续签名阶段统一 encode(SignUtils 做了 encode)
|
||||||
|
map[k] = java.net.URLDecoder.decode(v, "UTF-8")
|
||||||
|
} else {
|
||||||
|
map[pair] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractMultipartBody(body: RequestBody): Map<String, String> {
|
||||||
|
if (body !is MultipartBody) return emptyMap()
|
||||||
|
|
||||||
|
val map = linkedMapOf<String, String>()
|
||||||
|
for (i in 0 until body.parts.size) {
|
||||||
|
val part = body.parts[i]
|
||||||
|
val headers = part.headers
|
||||||
|
val disp = headers?.get("Content-Disposition").orEmpty()
|
||||||
|
|
||||||
|
// 取 name="xxx"
|
||||||
|
val name = Regex("""name="([^"]+)"""").find(disp)?.groupValues?.getOrNull(1) ?: continue
|
||||||
|
val filename = Regex("""filename="([^"]+)"""").find(disp)?.groupValues?.getOrNull(1)
|
||||||
|
|
||||||
|
// 有 filename 认为是文件字段:默认不签(避免读文件流/超大)
|
||||||
|
if (!filename.isNullOrBlank()) continue
|
||||||
|
|
||||||
|
// 文本字段:读出内容
|
||||||
|
val value = bodyToString(part.body).trim()
|
||||||
|
map[name] = value
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bodyToString(body: RequestBody): String {
|
||||||
|
return try {
|
||||||
|
val buffer = Buffer()
|
||||||
|
body.writeTo(buffer)
|
||||||
|
val charset: Charset = body.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8
|
||||||
|
buffer.readString(charset)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 扁平化规则:
|
||||||
|
* object: a.b.c
|
||||||
|
* array: items[0].id
|
||||||
|
*/
|
||||||
|
private fun flattenJson(elem: JsonElement, prefix: String, out: MutableMap<String, String>) {
|
||||||
|
when {
|
||||||
|
elem.isJsonNull -> {
|
||||||
|
// null 不参与(也可以 out[prefix] = "null" 但需要服务端一致)
|
||||||
|
}
|
||||||
|
elem.isJsonPrimitive -> {
|
||||||
|
if (prefix.isNotBlank()) out[prefix] = elem.asJsonPrimitive.toString().trim('"')
|
||||||
|
}
|
||||||
|
elem.isJsonObject -> {
|
||||||
|
val obj = elem.asJsonObject
|
||||||
|
for ((k, v) in obj.entrySet()) {
|
||||||
|
val newKey = if (prefix.isBlank()) k else "$prefix.$k"
|
||||||
|
flattenJson(v, newKey, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elem.isJsonArray -> {
|
||||||
|
val arr = elem.asJsonArray
|
||||||
|
for (i in 0 until arr.size()) {
|
||||||
|
val newKey = "$prefix[$i]"
|
||||||
|
flattenJson(arr[i], newKey, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.myapplication.network.security
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object NonceUtils {
|
||||||
|
fun genNonce(): String =
|
||||||
|
UUID.randomUUID().toString().replace("-", "").take(16)
|
||||||
|
|
||||||
|
fun genTimestampSeconds(): String =
|
||||||
|
(System.currentTimeMillis() / 1000).toString()
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.example.myapplication.network.security
|
||||||
|
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object SignUtils {
|
||||||
|
|
||||||
|
fun calcSign(params: Map<String, String>, secret: String): String {
|
||||||
|
val signStr = buildSignString(params, secret)
|
||||||
|
return hmacSha256Hex(signStr, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSignString(params: Map<String, String>, secret: String): String {
|
||||||
|
val filtered = params
|
||||||
|
.filter { (k, v) -> v.isNotBlank() && !k.equals("sign", ignoreCase = true) }
|
||||||
|
.toSortedMap()
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
filtered.forEach { (k, v) ->
|
||||||
|
if (sb.isNotEmpty()) sb.append("&")
|
||||||
|
sb.append(k).append("=").append(urlEncode(v))
|
||||||
|
}
|
||||||
|
sb.append("&secret=").append(urlEncode(secret))
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hmacSha256Hex(data: String, secret: String): String {
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
val keySpec = SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256")
|
||||||
|
mac.init(keySpec)
|
||||||
|
val bytes = mac.doFinal(data.toByteArray(Charsets.UTF_8))
|
||||||
|
return bytes.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun urlEncode(v: String): String =
|
||||||
|
URLEncoder.encode(v, "UTF-8")
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.example.myapplication.ui.common
|
package com.example.myapplication.ui.common
|
||||||
|
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -12,20 +14,33 @@ class LoadingOverlay private constructor(
|
|||||||
companion object {
|
companion object {
|
||||||
fun attach(parent: ViewGroup): LoadingOverlay {
|
fun attach(parent: ViewGroup): LoadingOverlay {
|
||||||
val overlay = LayoutInflater.from(parent.context)
|
val overlay = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.view_fullscreen_loading, parent, false)
|
.inflate(R.layout.view_fullscreen_loading, parent, false).apply {
|
||||||
|
visibility = View.GONE
|
||||||
|
bringToFront()
|
||||||
|
elevation = 100f // 确保在其它视图之上
|
||||||
|
}
|
||||||
|
|
||||||
overlay.visibility = View.GONE
|
parent.addView(overlay)
|
||||||
parent.addView(overlay) // 加到最上层(最后添加的在最上面)
|
|
||||||
return LoadingOverlay(parent, overlay)
|
return LoadingOverlay(parent, overlay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show() {
|
fun show() {
|
||||||
overlay.visibility = View.VISIBLE
|
if (Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||||
|
overlay.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
overlay.post { overlay.visibility = View.VISIBLE }
|
||||||
|
}
|
||||||
|
Log.d("LoadingOverlay", "Show loading")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hide() {
|
fun hide() {
|
||||||
overlay.visibility = View.GONE
|
if (Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||||
|
overlay.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
overlay.post { overlay.visibility = View.GONE }
|
||||||
|
}
|
||||||
|
Log.d("LoadingOverlay", "Hide loading")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove() {
|
fun remove() {
|
||||||
|
|||||||
@@ -25,13 +25,20 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.example.myapplication.ImeGuideActivity
|
import com.example.myapplication.ImeGuideActivity
|
||||||
|
import com.example.myapplication.ui.common.LoadingOverlay
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.*
|
import com.example.myapplication.network.AddPersonaClick
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
|
import com.example.myapplication.network.RetrofitClient
|
||||||
|
import com.example.myapplication.network.listByTagWithNotLogin
|
||||||
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.example.myapplication.network.PersonaClick
|
||||||
|
import com.example.myapplication.ui.home.PersonaAdapter
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
@@ -47,6 +54,7 @@ class HomeFragment : Fragment() {
|
|||||||
private lateinit var tabList2: TextView
|
private lateinit var tabList2: TextView
|
||||||
private lateinit var backgroundImage: ImageView
|
private lateinit var backgroundImage: ImageView
|
||||||
private var lastList1RenderKey: String? = null
|
private var lastList1RenderKey: String? = null
|
||||||
|
private lateinit var loadingOverlay: LoadingOverlay
|
||||||
|
|
||||||
private var preloadJob: Job? = null
|
private var preloadJob: Job? = null
|
||||||
private var allPersonaCache: List<listByTagWithNotLogin> = emptyList()
|
private var allPersonaCache: List<listByTagWithNotLogin> = emptyList()
|
||||||
@@ -114,13 +122,82 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AuthEvent.CharacterAdded -> {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
// 1) 列表一:重新拉
|
||||||
|
allPersonaCache = fetchAllPersonaList()
|
||||||
|
lastList1RenderKey = null
|
||||||
|
notifyPageChangedOnMain(0)
|
||||||
|
|
||||||
|
// 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”)
|
||||||
|
personaCache.clear()
|
||||||
|
|
||||||
|
// 3) 如果当前就在某个 tag 页:让它立刻更新(显示 loading -> 拉取 -> 刷新)
|
||||||
|
val pos = viewPager.currentItem
|
||||||
|
if (pos > 0) {
|
||||||
|
val tagId = tags.getOrNull(pos - 1)?.id
|
||||||
|
if (tagId != null) {
|
||||||
|
// 先刷新一次,让页面进入 loading(因为缓存被清了)
|
||||||
|
notifyPageChangedOnMain(pos)
|
||||||
|
|
||||||
|
// 再拉当前 tag 的新数据
|
||||||
|
val list = fetchPersonaByTag(tagId)
|
||||||
|
personaCache[tagId] = list
|
||||||
|
notifyPageChangedOnMain(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 可选:你如果希望“删除后列表二也立刻全量更新”,就顺手再预加载一遍
|
||||||
|
startPreloadAllTagsFillCacheOnly()
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is AuthEvent.CharacterDeleted -> {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
// 1) 列表一:重新拉
|
||||||
|
allPersonaCache = fetchAllPersonaList()
|
||||||
|
lastList1RenderKey = null
|
||||||
|
notifyPageChangedOnMain(0)
|
||||||
|
|
||||||
|
// 2) 列表二:清缓存(最可靠,避免“删除后还显示旧数据”)
|
||||||
|
personaCache.clear()
|
||||||
|
|
||||||
|
// 3) 如果当前就在某个 tag 页:让它立刻更新(显示 loading -> 拉取 -> 刷新)
|
||||||
|
val pos = viewPager.currentItem
|
||||||
|
if (pos > 0) {
|
||||||
|
val tagId = tags.getOrNull(pos - 1)?.id
|
||||||
|
if (tagId != null) {
|
||||||
|
// 先刷新一次,让页面进入 loading(因为缓存被清了)
|
||||||
|
notifyPageChangedOnMain(pos)
|
||||||
|
|
||||||
|
// 再拉当前 tag 的新数据
|
||||||
|
val list = fetchPersonaByTag(tagId)
|
||||||
|
personaCache[tagId] = list
|
||||||
|
notifyPageChangedOnMain(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 可选:你如果希望“删除后列表二也立刻全量更新”,就顺手再预加载一遍
|
||||||
|
startPreloadAllTagsFillCacheOnly()
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 充值按钮点击 - 使用事件总线打开全局页面
|
// 充值按钮点击 - 使用事件总线打开全局页面
|
||||||
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
|
view.findViewById<View>(R.id.rechargeButton).setOnClickListener {
|
||||||
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.rechargeFragment))
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.rechargeFragment))
|
||||||
@@ -146,6 +223,8 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
|
val root = view.findViewById<CoordinatorLayout>(R.id.rootCoordinator)
|
||||||
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
|
val floatingImage = view.findViewById<ImageView>(R.id.floatingImage)
|
||||||
|
loadingOverlay = LoadingOverlay.attach(root)
|
||||||
|
Log.d("HomeFragment", "LoadingOverlay initialized")
|
||||||
|
|
||||||
root.post {
|
root.post {
|
||||||
if (!isAdded) return@post
|
if (!isAdded) return@post
|
||||||
@@ -169,6 +248,7 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
// 加载列表一
|
// 加载列表一
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
try {
|
try {
|
||||||
val list = fetchAllPersonaList()
|
val list = fetchAllPersonaList()
|
||||||
if (!isAdded) return@launch
|
if (!isAdded) return@launch
|
||||||
@@ -180,11 +260,14 @@ class HomeFragment : Fragment() {
|
|||||||
notifyPageChangedOnMain(0)
|
notifyPageChangedOnMain(0)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("1314520-HomeFragment", "获取列表一失败", e)
|
Log.e("1314520-HomeFragment", "获取列表一失败", e)
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拉标签 + 预加载
|
// 拉标签 + 预加载
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
try {
|
try {
|
||||||
val response = RetrofitClient.apiService.tagList()
|
val response = RetrofitClient.apiService.tagList()
|
||||||
if (!isAdded) return@launch
|
if (!isAdded) return@launch
|
||||||
@@ -204,11 +287,13 @@ class HomeFragment : Fragment() {
|
|||||||
startPreloadAllTagsFillCacheOnly()
|
startPreloadAllTagsFillCacheOnly()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("1314520-HomeFragment", "获取标签失败", e)
|
Log.e("1314520-HomeFragment", "获取标签失败", e)
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== 你要求的核心优化:setupViewPager 只初始化一次 ==================
|
// ================== 核心:setupViewPager 只初始化一次 ==================
|
||||||
|
|
||||||
private fun setupViewPagerOnce() {
|
private fun setupViewPagerOnce() {
|
||||||
if (sheetAdapter != null) return
|
if (sheetAdapter != null) return
|
||||||
@@ -245,6 +330,52 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- 方案A:成功后“造新数据(copy)替换缓存”并刷新 ----------------
|
||||||
|
|
||||||
|
private fun applyAddedToggle(personaId: Int, newAdded: Boolean) {
|
||||||
|
// 1) 更新列表一缓存
|
||||||
|
run {
|
||||||
|
val oldAll = allPersonaCache
|
||||||
|
val idxAll = oldAll.indexOfFirst { it.id == personaId }
|
||||||
|
if (idxAll >= 0) {
|
||||||
|
val newList = oldAll.toMutableList()
|
||||||
|
val oldItem = newList[idxAll]
|
||||||
|
newList[idxAll] = oldItem.copy(added = newAdded)
|
||||||
|
allPersonaCache = newList
|
||||||
|
|
||||||
|
// renderList1 有 renderKey,必须清一下
|
||||||
|
lastList1RenderKey = null
|
||||||
|
notifyPageChangedOnMain(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 更新所有 tag 缓存(personaCache)
|
||||||
|
val keys = personaCache.keys.toList()
|
||||||
|
var changedCurrentTagPage = false
|
||||||
|
|
||||||
|
for (tagId in keys) {
|
||||||
|
val old = personaCache[tagId] ?: continue
|
||||||
|
val idx = old.indexOfFirst { it.id == personaId }
|
||||||
|
if (idx >= 0) {
|
||||||
|
val newList = old.toMutableList()
|
||||||
|
val oldItem = newList[idx]
|
||||||
|
newList[idx] = oldItem.copy(added = newAdded)
|
||||||
|
personaCache[tagId] = newList
|
||||||
|
|
||||||
|
// 如果当前就在这个 tag 页,标记需要刷新
|
||||||
|
val pos = viewPager.currentItem
|
||||||
|
val currentTagId = tags.getOrNull(pos - 1)?.id
|
||||||
|
if (pos > 0 && currentTagId == tagId) {
|
||||||
|
changedCurrentTagPage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedCurrentTagPage) {
|
||||||
|
notifyPageChangedOnMain(viewPager.currentItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------- 拖拽效果 ----------------
|
// ---------------- 拖拽效果 ----------------
|
||||||
|
|
||||||
private fun initDrag(target: View, parent: ViewGroup) {
|
private fun initDrag(target: View, parent: ViewGroup) {
|
||||||
@@ -520,7 +651,6 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------- ViewPager Adapter ----------------
|
// ---------------- ViewPager Adapter ----------------
|
||||||
|
|
||||||
inner class SheetPagerAdapter(
|
inner class SheetPagerAdapter(
|
||||||
@@ -564,10 +694,7 @@ class HomeFragment : Fragment() {
|
|||||||
val loadingView = root.findViewById<View>(R.id.loadingView)
|
val loadingView = root.findViewById<View>(R.id.loadingView)
|
||||||
|
|
||||||
rv2.setHasFixedSize(true)
|
rv2.setHasFixedSize(true)
|
||||||
|
|
||||||
// ✅ 禁止 itemAnimator(减少 layout 抖动)
|
|
||||||
rv2.itemAnimator = null
|
rv2.itemAnimator = null
|
||||||
|
|
||||||
rv2.isNestedScrollingEnabled = false
|
rv2.isNestedScrollingEnabled = false
|
||||||
|
|
||||||
var adapter = rv2.adapter as? PersonaAdapter
|
var adapter = rv2.adapter as? PersonaAdapter
|
||||||
@@ -584,22 +711,31 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
is PersonaClick.Add -> {
|
is PersonaClick.Add -> {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val personaId = click.persona.id?: return@launch
|
||||||
|
val oldAdded = click.persona.added
|
||||||
|
val newAdded = !oldAdded
|
||||||
try {
|
try {
|
||||||
if (click.persona.added == true) {
|
if (oldAdded) {
|
||||||
click.persona.id?.let { id ->
|
// RetrofitClient.apiService.delUserCharacter(personaId)
|
||||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
// // ✅ 成功后替换缓存并刷新
|
||||||
}
|
// applyAddedToggle(personaId, newAdded)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
|
Log.d("1314520-HomeFragment", "add persona id=${click.persona.id}")
|
||||||
val req = AddPersonaClick(
|
val req = AddPersonaClick(
|
||||||
characterId = click.persona.id?.toInt() ?: 0,
|
characterId = personaId,
|
||||||
emoji = click.persona.emoji ?: ""
|
emoji = click.persona.emoji ?: ""
|
||||||
)
|
)
|
||||||
RetrofitClient.apiService.addUserCharacter(req)
|
RetrofitClient.apiService.addUserCharacter(req)
|
||||||
|
// ✅ 成功后替换缓存并刷新
|
||||||
|
applyAddedToggle(personaId, newAdded)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("1314520-HomeFragment", "grid toggle add failed id=$personaId", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rv2.layoutManager = GridLayoutManager(root.context, 2)
|
rv2.layoutManager = GridLayoutManager(root.context, 2)
|
||||||
@@ -629,7 +765,7 @@ class HomeFragment : Fragment() {
|
|||||||
override fun getItemCount(): Int = pageCount
|
override fun getItemCount(): Int = pageCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- 列表一渲染(原逻辑不动) ----------------
|
// ---------------- 列表一渲染 ----------------
|
||||||
|
|
||||||
private fun renderList1(root: View, list: List<listByTagWithNotLogin>) {
|
private fun renderList1(root: View, list: List<listByTagWithNotLogin>) {
|
||||||
val key = buildString {
|
val key = buildString {
|
||||||
@@ -641,6 +777,7 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
if (key == lastList1RenderKey) return
|
if (key == lastList1RenderKey) return
|
||||||
lastList1RenderKey = key
|
lastList1RenderKey = key
|
||||||
|
|
||||||
val sorted = list.sortedBy { it.rank ?: Int.MAX_VALUE }
|
val sorted = list.sortedBy { it.rank ?: Int.MAX_VALUE }
|
||||||
val top3 = sorted.take(3)
|
val top3 = sorted.take(3)
|
||||||
val others = if (sorted.size > 3) sorted.drop(3) else emptyList()
|
val others = if (sorted.size > 3) sorted.drop(3) else emptyList()
|
||||||
@@ -650,6 +787,7 @@ class HomeFragment : Fragment() {
|
|||||||
avatarId = R.id.avatar_first,
|
avatarId = R.id.avatar_first,
|
||||||
nameId = R.id.name_first,
|
nameId = R.id.name_first,
|
||||||
addBtnId = R.id.btn_add_first,
|
addBtnId = R.id.btn_add_first,
|
||||||
|
addBtnIcon = R.id.add_first_icon,
|
||||||
containerId = R.id.container_first,
|
containerId = R.id.container_first,
|
||||||
item = top3.getOrNull(0)
|
item = top3.getOrNull(0)
|
||||||
)
|
)
|
||||||
@@ -659,6 +797,7 @@ class HomeFragment : Fragment() {
|
|||||||
avatarId = R.id.avatar_second,
|
avatarId = R.id.avatar_second,
|
||||||
nameId = R.id.name_second,
|
nameId = R.id.name_second,
|
||||||
addBtnId = R.id.btn_add_second,
|
addBtnId = R.id.btn_add_second,
|
||||||
|
addBtnIcon = R.id.add_second_icon,
|
||||||
containerId = R.id.container_second,
|
containerId = R.id.container_second,
|
||||||
item = top3.getOrNull(1)
|
item = top3.getOrNull(1)
|
||||||
)
|
)
|
||||||
@@ -668,6 +807,7 @@ class HomeFragment : Fragment() {
|
|||||||
avatarId = R.id.avatar_third,
|
avatarId = R.id.avatar_third,
|
||||||
nameId = R.id.name_third,
|
nameId = R.id.name_third,
|
||||||
addBtnId = R.id.btn_add_third,
|
addBtnId = R.id.btn_add_third,
|
||||||
|
addBtnIcon = R.id.add_third_icon,
|
||||||
containerId = R.id.container_third,
|
containerId = R.id.container_third,
|
||||||
item = top3.getOrNull(2)
|
item = top3.getOrNull(2)
|
||||||
)
|
)
|
||||||
@@ -686,6 +826,62 @@ class HomeFragment : Fragment() {
|
|||||||
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
|
val iv = itemView.findViewById<de.hdodenhof.circleimageview.CircleImageView>(R.id.iv_avatar)
|
||||||
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv)
|
com.bumptech.glide.Glide.with(iv).load(p.avatarUrl).into(iv)
|
||||||
|
|
||||||
|
// ---------------- add 按钮(失败回滚 + 防连点) ----------------
|
||||||
|
val addBtn = itemView.findViewById<LinearLayout>(R.id.btn_add)
|
||||||
|
val addIcon = itemView.findViewById<ImageView>(R.id.add_icon)
|
||||||
|
|
||||||
|
val originBg = addBtn.background
|
||||||
|
val originIcon = addIcon.drawable
|
||||||
|
|
||||||
|
fun renderAddState(added: Boolean) {
|
||||||
|
if (added) {
|
||||||
|
addBtn.setBackgroundResource(R.drawable.round_bg_others_already)
|
||||||
|
addIcon.setImageResource(R.drawable.ime_guide_activity_btn_completed_img)
|
||||||
|
} else {
|
||||||
|
addBtn.background = originBg
|
||||||
|
addIcon.setImageDrawable(originIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 首次渲染
|
||||||
|
renderAddState(p.added == true)
|
||||||
|
|
||||||
|
addBtn.setOnClickListener {
|
||||||
|
if (!addBtn.isEnabled) return@setOnClickListener
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val personaId = p.id?: return@launch
|
||||||
|
val oldAdded = p.added
|
||||||
|
val newAdded = !oldAdded
|
||||||
|
|
||||||
|
addBtn.isEnabled = false
|
||||||
|
renderAddState(newAdded)
|
||||||
|
try {
|
||||||
|
if (oldAdded) {
|
||||||
|
// RetrofitClient.apiService.delUserCharacter(personaId)
|
||||||
|
// // ✅ 只有成功才更新缓存 + 更新UI(失败则保持原样)
|
||||||
|
// applyAddedToggle(personaId, newAdded)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.d("1314520-HomeFragment", "add persona id=${p.id}")
|
||||||
|
val req = AddPersonaClick(
|
||||||
|
characterId = personaId,
|
||||||
|
emoji = p.emoji ?: ""
|
||||||
|
)
|
||||||
|
RetrofitClient.apiService.addUserCharacter(req)
|
||||||
|
// ✅ 只有成功才更新缓存 + 更新UI(失败则保持原样)
|
||||||
|
applyAddedToggle(personaId, newAdded)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("1314520-HomeFragment", "others toggle add failed id=$personaId", e)
|
||||||
|
renderAddState(oldAdded)
|
||||||
|
} finally {
|
||||||
|
addBtn.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- item 点击 ----------------
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
if (!isAdded || childFragmentManager.isStateSaved) return@setOnClickListener
|
if (!isAdded || childFragmentManager.isStateSaved) return@setOnClickListener
|
||||||
PersonaDetailDialogFragment
|
PersonaDetailDialogFragment
|
||||||
@@ -693,23 +889,6 @@ class HomeFragment : Fragment() {
|
|||||||
.show(childFragmentManager, "persona_detail")
|
.show(childFragmentManager, "persona_detail")
|
||||||
}
|
}
|
||||||
|
|
||||||
itemView.findViewById<View>(R.id.btn_add).setOnClickListener {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
if (p.added == true) {
|
|
||||||
p.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
|
|
||||||
} else {
|
|
||||||
val req = AddPersonaClick(
|
|
||||||
characterId = p.id?.toInt() ?: 0,
|
|
||||||
emoji = p.emoji ?: ""
|
|
||||||
)
|
|
||||||
RetrofitClient.apiService.addUserCharacter(req)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container.addView(itemView)
|
container.addView(itemView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,12 +898,14 @@ class HomeFragment : Fragment() {
|
|||||||
avatarId: Int,
|
avatarId: Int,
|
||||||
nameId: Int,
|
nameId: Int,
|
||||||
addBtnId: Int,
|
addBtnId: Int,
|
||||||
|
addBtnIcon: Int,
|
||||||
containerId: Int,
|
containerId: Int,
|
||||||
item: listByTagWithNotLogin?
|
item: listByTagWithNotLogin?
|
||||||
) {
|
) {
|
||||||
val avatar = root.findViewById<de.hdodenhof.circleimageview.CircleImageView>(avatarId)
|
val avatar = root.findViewById<de.hdodenhof.circleimageview.CircleImageView>(avatarId)
|
||||||
val name = root.findViewById<TextView>(nameId)
|
val name = root.findViewById<TextView>(nameId)
|
||||||
val addBtn = root.findViewById<View>(addBtnId)
|
val addBtn = root.findViewById<LinearLayout>(addBtnId)
|
||||||
|
val addIcon = root.findViewById<ImageView>(addBtnIcon)
|
||||||
val container = root.findViewById<LinearLayout>(containerId)
|
val container = root.findViewById<LinearLayout>(containerId)
|
||||||
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
@@ -739,19 +920,60 @@ class HomeFragment : Fragment() {
|
|||||||
name.text = item.characterName ?: ""
|
name.text = item.characterName ?: ""
|
||||||
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar)
|
com.bumptech.glide.Glide.with(avatar).load(item.avatarUrl).into(avatar)
|
||||||
|
|
||||||
|
// ✅ 记录“原始背景/原始icon”,用于 added=false 时恢复
|
||||||
|
val originBg = addBtn.background
|
||||||
|
val originIconRes = when (addBtnId) {
|
||||||
|
R.id.btn_add_first -> R.drawable.first_add
|
||||||
|
R.id.btn_add_second -> R.drawable.second_add
|
||||||
|
R.id.btn_add_third -> R.drawable.third_add
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renderAddState(added: Boolean) {
|
||||||
|
if (added) {
|
||||||
|
addBtn.setBackgroundResource(R.drawable.round_bg_others_already)
|
||||||
|
addIcon.setImageResource(R.drawable.ime_guide_activity_btn_completed_img)
|
||||||
|
} else {
|
||||||
|
addBtn.background = originBg
|
||||||
|
if (originIconRes != 0) addIcon.setImageResource(originIconRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 首次渲染
|
||||||
|
renderAddState(item.added == true)
|
||||||
|
|
||||||
|
// ✅ 点击:失败回滚 + 防连点(请求中禁用按钮)
|
||||||
addBtn.setOnClickListener {
|
addBtn.setOnClickListener {
|
||||||
|
if (!addBtn.isEnabled) return@setOnClickListener
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val personaId = item.id?: return@launch
|
||||||
|
val oldAdded = item.added
|
||||||
|
val newAdded = !oldAdded
|
||||||
|
|
||||||
|
addBtn.isEnabled = false
|
||||||
|
renderAddState(newAdded)
|
||||||
try {
|
try {
|
||||||
if (item.added == true) {
|
if (oldAdded) {
|
||||||
item.id?.let { id -> RetrofitClient.apiService.delUserCharacter(id.toInt()) }
|
// RetrofitClient.apiService.delUserCharacter(personaId)
|
||||||
} else {
|
// // ✅ 只有成功才更新缓存 + 更新UI
|
||||||
|
// applyAddedToggle(personaId, newAdded)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.d("1314520-HomeFragment", "add persona id=${item.id}")
|
||||||
val req = AddPersonaClick(
|
val req = AddPersonaClick(
|
||||||
characterId = item.id?.toInt() ?: 0,
|
characterId = personaId,
|
||||||
emoji = item.emoji ?: ""
|
emoji = item.emoji ?: ""
|
||||||
)
|
)
|
||||||
RetrofitClient.apiService.addUserCharacter(req)
|
RetrofitClient.apiService.addUserCharacter(req)
|
||||||
|
// ✅ 只有成功才更新缓存 + 更新UI
|
||||||
|
applyAddedToggle(personaId, newAdded)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("1314520-HomeFragment", "others toggle add failed id=$personaId", e)
|
||||||
|
renderAddState(oldAdded)
|
||||||
|
} finally {
|
||||||
|
addBtn.isEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ package com.example.myapplication.ui.home
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.listByTagWithNotLogin
|
|
||||||
import com.example.myapplication.network.PersonaClick
|
import com.example.myapplication.network.PersonaClick
|
||||||
|
import com.example.myapplication.network.listByTagWithNotLogin
|
||||||
import de.hdodenhof.circleimageview.CircleImageView
|
import de.hdodenhof.circleimageview.CircleImageView
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
class PersonaAdapter(
|
class PersonaAdapter(
|
||||||
private val onClick: (PersonaClick) -> Unit
|
private val onClick: (PersonaClick) -> Unit
|
||||||
@@ -26,16 +27,14 @@ class PersonaAdapter(
|
|||||||
|
|
||||||
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
val ivAvatar: CircleImageView = itemView.findViewById(R.id.ivAvatar)
|
private val ivAvatar: CircleImageView = itemView.findViewById(R.id.ivAvatar)
|
||||||
val tvName: TextView = itemView.findViewById(R.id.tvName)
|
private val tvName: TextView = itemView.findViewById(R.id.tvName)
|
||||||
val characterBackground: TextView =
|
private val characterBackground: TextView = itemView.findViewById(R.id.characterBackground)
|
||||||
itemView.findViewById(R.id.characterBackground)
|
private val download: TextView = itemView.findViewById(R.id.download)
|
||||||
val download: TextView = itemView.findViewById(R.id.download)
|
private val operation: LinearLayout = itemView.findViewById(R.id.operation)
|
||||||
val operation: TextView = itemView.findViewById(R.id.operation)
|
private val operationIcon: ImageView = itemView.findViewById(R.id.operation_add_icon)
|
||||||
|
|
||||||
/** ✅ 统一绑定 + 点击逻辑 */
|
|
||||||
fun bind(item: listByTagWithNotLogin) {
|
fun bind(item: listByTagWithNotLogin) {
|
||||||
|
|
||||||
tvName.text = item.characterName
|
tvName.text = item.characterName
|
||||||
characterBackground.text = item.characterBackground
|
characterBackground.text = item.characterBackground
|
||||||
download.text = item.download
|
download.text = item.download
|
||||||
@@ -46,15 +45,19 @@ class PersonaAdapter(
|
|||||||
.error(R.drawable.default_avatar)
|
.error(R.drawable.default_avatar)
|
||||||
.into(ivAvatar)
|
.into(ivAvatar)
|
||||||
|
|
||||||
// ✅ 整个 item:跳详情
|
val isAdded = item.added
|
||||||
itemView.setOnClickListener {
|
|
||||||
onClick(PersonaClick.Item(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 添加 / 下载按钮
|
// ✅ 背景改 operation(外层容器)
|
||||||
operation.setOnClickListener {
|
operation.setBackgroundResource(
|
||||||
onClick(PersonaClick.Add(item))
|
if (isAdded) R.drawable.list_two_bg_already else R.drawable.list_two_bg
|
||||||
}
|
)
|
||||||
|
|
||||||
|
// ✅ 图标改 operationIcon(中间图)
|
||||||
|
operationIcon.setImageResource(
|
||||||
|
if (isAdded) R.drawable.ime_guide_activity_btn_completed_img else R.drawable.operation_add
|
||||||
|
)
|
||||||
|
itemView.setOnClickListener { onClick(PersonaClick.Item(item)) }
|
||||||
|
operation.setOnClickListener { onClick(PersonaClick.Add(item)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import com.example.myapplication.network.RetrofitClient
|
|||||||
import com.example.myapplication.network.CharacterDetailResponse
|
import com.example.myapplication.network.CharacterDetailResponse
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.example.myapplication.network.AddPersonaClick
|
import com.example.myapplication.network.AddPersonaClick
|
||||||
|
import android.util.Log
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
|
|
||||||
class PersonaDetailDialogFragment : DialogFragment() {
|
class PersonaDetailDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ class PersonaDetailDialogFragment : DialogFragment() {
|
|||||||
tvBackground.text = data.characterBackground ?: ""
|
tvBackground.text = data.characterBackground ?: ""
|
||||||
btnAdd.text = data.added?.let { "Added" } ?: "Add"
|
btnAdd.text = data.added?.let { "Added" } ?: "Add"
|
||||||
btnAdd.setBackgroundResource(data.added?.let { R.drawable.ic_added } ?: R.drawable.keyboard_ettings)
|
btnAdd.setBackgroundResource(data.added?.let { R.drawable.ic_added } ?: R.drawable.keyboard_ettings)
|
||||||
|
val newAdded = !(data.added ?: false)
|
||||||
|
|
||||||
Glide.with(requireContext())
|
Glide.with(requireContext())
|
||||||
.load(data.avatarUrl)
|
.load(data.avatarUrl)
|
||||||
@@ -72,13 +76,13 @@ class PersonaDetailDialogFragment : DialogFragment() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
if(data.added == true){
|
if(data.added == true){
|
||||||
//取消收藏
|
//取消收藏
|
||||||
data.id?.let { id ->
|
// data.id?.let { id ->
|
||||||
try {
|
// try {
|
||||||
RetrofitClient.apiService.delUserCharacter(id.toInt())
|
// RetrofitClient.apiService.delUserCharacter(id.toInt())
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
// 处理错误
|
// // 处理错误
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}else{
|
}else{
|
||||||
val addPersonaRequest = AddPersonaClick(
|
val addPersonaRequest = AddPersonaClick(
|
||||||
characterId = data.id?.toInt() ?: 0,
|
characterId = data.id?.toInt() ?: 0,
|
||||||
@@ -86,8 +90,11 @@ class PersonaDetailDialogFragment : DialogFragment() {
|
|||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
|
RetrofitClient.apiService.addUserCharacter(addPersonaRequest)
|
||||||
|
data.id?.let { personaId ->
|
||||||
|
AuthEventBus.emit(AuthEvent.CharacterAdded(personaId,newAdded))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 处理错误
|
Log.e("1314520-PersonaDetailDialogFragment", "addUserCharacter error", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.example.myapplication.ui.keyboard
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Window
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.example.myapplication.R
|
||||||
|
|
||||||
|
class ConfirmDeleteDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_NAME = "arg_name"
|
||||||
|
private const val ARG_EMOJI = "arg_emoji"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
characterName: String?,
|
||||||
|
emoji: String?,
|
||||||
|
onConfirm: () -> Unit
|
||||||
|
): ConfirmDeleteDialogFragment {
|
||||||
|
return ConfirmDeleteDialogFragment().apply {
|
||||||
|
this.onConfirm = onConfirm
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(ARG_NAME, characterName ?: "")
|
||||||
|
putString(ARG_EMOJI, emoji ?: "🙂")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var onConfirm: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val dialog = Dialog(requireContext())
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
|
||||||
|
val v = LayoutInflater.from(requireContext())
|
||||||
|
.inflate(R.layout.dialog_confirm_delete_character, null, false)
|
||||||
|
dialog.setContentView(v)
|
||||||
|
|
||||||
|
dialog.setCancelable(true)
|
||||||
|
dialog.setCanceledOnTouchOutside(true)
|
||||||
|
|
||||||
|
val name = arguments?.getString(ARG_NAME).orEmpty()
|
||||||
|
val emoji = arguments?.getString(ARG_EMOJI) ?: "🙂"
|
||||||
|
|
||||||
|
v.findViewById<TextView>(R.id.tv_name).text = name
|
||||||
|
v.findViewById<TextView>(R.id.tv_emoji).text = emoji
|
||||||
|
|
||||||
|
v.findViewById<TextView>(R.id.btn_cancel).setOnClickListener {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
|
||||||
|
v.findViewById<TextView>(R.id.btn_confirm).setOnClickListener {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
onConfirm?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 宽度更贴合弹窗
|
||||||
|
dialog.window?.setLayout(
|
||||||
|
(resources.displayMetrics.widthPixels * 0.86f).toInt(),
|
||||||
|
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
onConfirm = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.example.myapplication.ui.keyboard
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class DragSortCallback(
|
||||||
|
private val onMove: (from: Int, to: Int) -> Unit
|
||||||
|
) : ItemTouchHelper.Callback() {
|
||||||
|
|
||||||
|
private var didHaptic = false
|
||||||
|
|
||||||
|
override fun isLongPressDragEnabled(): Boolean = true
|
||||||
|
override fun isItemViewSwipeEnabled(): Boolean = false
|
||||||
|
|
||||||
|
override fun getMovementFlags(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder
|
||||||
|
): Int {
|
||||||
|
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or
|
||||||
|
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
|
||||||
|
return makeMovementFlags(dragFlags, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||||
|
super.onSelectedChanged(viewHolder, actionState)
|
||||||
|
|
||||||
|
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
|
||||||
|
// ✅ 触觉反馈(只触发一次)
|
||||||
|
if (!didHaptic) {
|
||||||
|
viewHolder.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
didHaptic = true
|
||||||
|
}
|
||||||
|
// ✅ 视觉反馈:放大 + 半透明
|
||||||
|
viewHolder.itemView.animate()
|
||||||
|
.scaleX(1.03f).scaleY(1.03f)
|
||||||
|
.alpha(0.85f)
|
||||||
|
.setDuration(120)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
super.clearView(recyclerView, viewHolder)
|
||||||
|
didHaptic = false
|
||||||
|
|
||||||
|
// ✅ 结束拖动,恢复视觉
|
||||||
|
viewHolder.itemView.animate()
|
||||||
|
.scaleX(1f).scaleY(1f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(120)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
val from = viewHolder.adapterPosition
|
||||||
|
val to = target.adapterPosition
|
||||||
|
if (from == RecyclerView.NO_POSITION || to == RecyclerView.NO_POSITION) return false
|
||||||
|
onMove(from, to)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.myapplication.ui.keyboard
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.ListByUserWithNot
|
||||||
|
|
||||||
|
class KeyboardAdapter(
|
||||||
|
private val onItemClick: (ListByUserWithNot) -> Unit
|
||||||
|
) : RecyclerView.Adapter<KeyboardAdapter.VH>() {
|
||||||
|
|
||||||
|
private val items = mutableListOf<ListByUserWithNot>()
|
||||||
|
|
||||||
|
fun submitList(newList: List<ListByUserWithNot>) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newList)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentIdsInOrder(): List<Int> = items.map { it.id }
|
||||||
|
|
||||||
|
fun moveItem(from: Int, to: Int) {
|
||||||
|
if (from == to) return
|
||||||
|
val item = items.removeAt(from)
|
||||||
|
items.add(to, item)
|
||||||
|
notifyItemMoved(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_keyboard_character, parent, false)
|
||||||
|
return VH(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val root: View = itemView.findViewById(R.id.item_root)
|
||||||
|
private val tvEmoji: TextView = itemView.findViewById(R.id.tv_emoji)
|
||||||
|
private val tvName: TextView = itemView.findViewById(R.id.tv_name)
|
||||||
|
|
||||||
|
fun bind(item: ListByUserWithNot) {
|
||||||
|
tvEmoji.text = item.emoji ?: "🙂"
|
||||||
|
tvName.text = item.characterName ?: ""
|
||||||
|
|
||||||
|
// ✅ 点击整卡(不直接删,交给外层弹窗确认)
|
||||||
|
root.setOnClickListener { onItemClick(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,144 @@
|
|||||||
package com.example.myapplication.ui.keyboard
|
package com.example.myapplication.ui.keyboard
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.ApiResponse
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
|
import com.example.myapplication.ui.common.LoadingOverlay
|
||||||
|
import com.example.myapplication.network.ListByUserWithNot
|
||||||
|
import com.example.myapplication.network.RetrofitClient
|
||||||
|
import com.example.myapplication.network.updateUserCharacterSortRequest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MyKeyboard : Fragment() {
|
class MyKeyboard : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var rv: RecyclerView
|
||||||
|
private lateinit var btnSave: TextView
|
||||||
|
private lateinit var adapter: KeyboardAdapter
|
||||||
|
private lateinit var loadingOverlay: LoadingOverlay
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View = inflater.inflate(R.layout.my_keyboard, container, false)
|
||||||
return inflater.inflate(R.layout.my_keyboard, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||||
parentFragmentManager.popBackStack()
|
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = view.findViewById(R.id.rv_keyboard)
|
||||||
|
btnSave = view.findViewById(R.id.btn_keyboard)
|
||||||
|
|
||||||
|
adapter = KeyboardAdapter(
|
||||||
|
onItemClick = { item ->
|
||||||
|
// ✅ 点击卡片:弹自定义确认弹窗
|
||||||
|
ConfirmDeleteDialogFragment
|
||||||
|
.newInstance(item.characterName, item.emoji) {
|
||||||
|
// ✅ 用户确认后才删除
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val resp = setdelUserCharacter(item.id)
|
||||||
|
if (resp?.code == 0 && resp.data == true) {
|
||||||
|
Toast.makeText(requireContext(),"Deleted successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
AuthEventBus.emit(AuthEvent.CharacterDeleted(item.id))
|
||||||
|
loadList()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), resp?.message ?: "Delete failed", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show(parentFragmentManager, "confirm_delete")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
rv.layoutManager = GridLayoutManager(requireContext(), 2)
|
||||||
|
rv.adapter = adapter
|
||||||
|
|
||||||
|
// ✅ 长按拖动排序 + 反馈
|
||||||
|
ItemTouchHelper(
|
||||||
|
DragSortCallback { from, to ->
|
||||||
|
adapter.moveItem(from, to)
|
||||||
|
}
|
||||||
|
).attachToRecyclerView(rv)
|
||||||
|
|
||||||
|
loadingOverlay = LoadingOverlay.attach(view as ViewGroup)
|
||||||
|
loadList()
|
||||||
|
|
||||||
|
// ✅ Save:上传当前排序(id数组)
|
||||||
|
btnSave.setOnClickListener {
|
||||||
|
val sortIds = adapter.getCurrentIdsInOrder()
|
||||||
|
Log.d("MyKeyboard-sort", sortIds.toString())
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
val body = updateUserCharacterSortRequest(sort = sortIds)
|
||||||
|
val resp = setupdateUserCharacterSort(body)
|
||||||
|
if (resp?.code == 0 && resp.data == true) {
|
||||||
|
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
|
Toast.makeText(requireContext(), "Sorting has been successfully modified.", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), resp?.message ?: "Save failed", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(requireContext(), "Network error: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadList() {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
val resp = getlistByUser()
|
||||||
|
if (resp?.code == 0 && resp.data != null) {
|
||||||
|
adapter.submitList(resp.data)
|
||||||
|
Log.d("1314520-list", resp.data.toString())
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), resp?.message ?: "Load failed", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(requireContext(), "Load failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户人设列表
|
||||||
|
private suspend fun getlistByUser(): ApiResponse<List<ListByUserWithNot>>? =
|
||||||
|
runCatching { RetrofitClient.apiService.listByUser() }.getOrNull()
|
||||||
|
|
||||||
|
// 更新用户人设排序
|
||||||
|
private suspend fun setupdateUserCharacterSort(body: updateUserCharacterSortRequest): ApiResponse<Boolean>? =
|
||||||
|
runCatching { RetrofitClient.apiService.updateUserCharacterSort(body) }.getOrNull()
|
||||||
|
|
||||||
|
// 删除用户人设
|
||||||
|
private suspend fun setdelUserCharacter(id: Int): ApiResponse<Boolean>? {
|
||||||
|
loadingOverlay.show()
|
||||||
|
return try {
|
||||||
|
runCatching { RetrofitClient.apiService.delUserCharacter(id) }.getOrNull()
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.example.myapplication.ui.mine
|
package com.example.myapplication.ui.mine
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -17,11 +20,15 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import com.example.myapplication.network.AuthEvent
|
import com.example.myapplication.network.AuthEvent
|
||||||
import com.example.myapplication.network.AuthEventBus
|
import com.example.myapplication.network.AuthEventBus
|
||||||
import com.example.myapplication.network.LoginResponse
|
import com.example.myapplication.network.LoginResponse
|
||||||
|
import com.example.myapplication.network.ApiResponse
|
||||||
|
import com.example.myapplication.network.ShareResponse
|
||||||
import com.example.myapplication.network.RetrofitClient
|
import com.example.myapplication.network.RetrofitClient
|
||||||
|
import com.example.myapplication.ui.common.LoadingOverlay
|
||||||
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
import com.example.myapplication.utils.EncryptedSharedPreferencesUtil
|
||||||
import de.hdodenhof.circleimageview.CircleImageView
|
import de.hdodenhof.circleimageview.CircleImageView
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -33,6 +40,9 @@ class MineFragment : Fragment() {
|
|||||||
private lateinit var nickname: TextView
|
private lateinit var nickname: TextView
|
||||||
private lateinit var time: TextView
|
private lateinit var time: TextView
|
||||||
private lateinit var logout: TextView
|
private lateinit var logout: TextView
|
||||||
|
private lateinit var avatar: CircleImageView
|
||||||
|
private lateinit var share: LinearLayout
|
||||||
|
private lateinit var loadingOverlay: LoadingOverlay
|
||||||
|
|
||||||
private var loadUserJob: Job? = null
|
private var loadUserJob: Job? = null
|
||||||
|
|
||||||
@@ -48,6 +58,7 @@ class MineFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
loadUserJob?.cancel()
|
loadUserJob?.cancel()
|
||||||
|
loadingOverlay.remove()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,20 +68,29 @@ class MineFragment : Fragment() {
|
|||||||
nickname = view.findViewById(R.id.nickname)
|
nickname = view.findViewById(R.id.nickname)
|
||||||
time = view.findViewById(R.id.time)
|
time = view.findViewById(R.id.time)
|
||||||
logout = view.findViewById(R.id.logout)
|
logout = view.findViewById(R.id.logout)
|
||||||
|
avatar = view.findViewById(R.id.avatar)
|
||||||
|
share = view.findViewById(R.id.click_Share)
|
||||||
|
loadingOverlay = LoadingOverlay.attach(view.findViewById(R.id.rootCoordinator))
|
||||||
|
|
||||||
// 1) 先用本地缓存秒出首屏
|
// 1) 先用本地缓存秒出首屏
|
||||||
renderFromCache()
|
renderFromCache()
|
||||||
|
|
||||||
// 2) 首次进入不刷新,由onResume处理
|
share.setOnClickListener {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
// // ✅ 手动刷新:不改布局也能用
|
loadingOverlay.show()
|
||||||
// // - 点昵称刷新
|
try {
|
||||||
// nickname.setOnClickListener { refreshUser(force = true, showToast = true) }
|
val response = getinviteCode()
|
||||||
// // - 长按 time 刷新
|
response?.data?.h5Link?.let { link ->
|
||||||
// time.setOnLongClickListener {
|
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
// refreshUser(force = true, showToast = true)
|
val clip = ClipData.newPlainText("h5Link", link)
|
||||||
// true
|
clipboard.setPrimaryClip(clip)
|
||||||
// }
|
Toast.makeText(context, "The sharing link has been copied to the clipboard.", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logout.setOnClickListener {
|
logout.setOnClickListener {
|
||||||
LogoutDialogFragment { doLogout() }
|
LogoutDialogFragment { doLogout() }
|
||||||
@@ -85,17 +105,20 @@ class MineFragment : Fragment() {
|
|||||||
// 使用事件总线打开金币充值页面
|
// 使用事件总线打开金币充值页面
|
||||||
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
|
||||||
}
|
}
|
||||||
view.findViewById<CircleImageView>(R.id.avatar).setOnClickListener {
|
avatar.setOnClickListener {
|
||||||
safeNavigate(R.id.action_mineFragment_to_personalSettings)
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.PersonalSettings))
|
||||||
}
|
}
|
||||||
view.findViewById<LinearLayout>(R.id.keyboard_settings).setOnClickListener {
|
view.findViewById<LinearLayout>(R.id.keyboard_settings).setOnClickListener {
|
||||||
safeNavigate(R.id.action_mineFragment_to_mykeyboard)
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.MyKeyboard))
|
||||||
|
}
|
||||||
|
view.findViewById<LinearLayout>(R.id.click_record).setOnClickListener {
|
||||||
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.consumptionRecordFragment))
|
||||||
}
|
}
|
||||||
view.findViewById<LinearLayout>(R.id.click_Feedback).setOnClickListener {
|
view.findViewById<LinearLayout>(R.id.click_Feedback).setOnClickListener {
|
||||||
safeNavigate(R.id.action_mineFragment_to_feedbackFragment)
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.feedbackFragment))
|
||||||
}
|
}
|
||||||
view.findViewById<LinearLayout>(R.id.click_Notice).setOnClickListener {
|
view.findViewById<LinearLayout>(R.id.click_Notice).setOnClickListener {
|
||||||
safeNavigate(R.id.action_mineFragment_to_notificationFragment)
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.notificationFragment))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 监听登录成功/登出事件(跨 NavHost 可靠)
|
// ✅ 监听登录成功/登出事件(跨 NavHost 可靠)
|
||||||
@@ -108,6 +131,10 @@ class MineFragment : Fragment() {
|
|||||||
renderFromCache()
|
renderFromCache()
|
||||||
refreshUser(force = true, showToast = false)
|
refreshUser(force = true, showToast = false)
|
||||||
}
|
}
|
||||||
|
AuthEvent.UserUpdated -> {
|
||||||
|
renderFromCache()
|
||||||
|
refreshUser(force = true, showToast = false)
|
||||||
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +158,11 @@ class MineFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
nickname.text = cached?.nickName ?: ""
|
nickname.text = cached?.nickName ?: ""
|
||||||
time.text = cached?.vipExpiry?.let { "Due on November $it" } ?: ""
|
time.text = cached?.vipExpiry?.let { "Due on November $it" } ?: ""
|
||||||
|
cached?.avatarUrl?.let { url ->
|
||||||
|
Glide.with(requireContext())
|
||||||
|
.load(url)
|
||||||
|
.into(avatar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,15 +186,22 @@ class MineFragment : Fragment() {
|
|||||||
Log.d(TAG, "getUser ok: nick=${u?.nickName} vip=${u?.vipExpiry}")
|
Log.d(TAG, "getUser ok: nick=${u?.nickName} vip=${u?.vipExpiry}")
|
||||||
|
|
||||||
nickname.text = u?.nickName ?: ""
|
nickname.text = u?.nickName ?: ""
|
||||||
|
|
||||||
time.text = u?.vipExpiry?.let { "Due on November $it" } ?: ""
|
time.text = u?.vipExpiry?.let { "Due on November $it" } ?: ""
|
||||||
|
|
||||||
|
u?.avatarUrl?.let { url ->
|
||||||
|
Glide.with(requireContext())
|
||||||
|
.load(url)
|
||||||
|
.into(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
EncryptedSharedPreferencesUtil.save(requireContext(), "Personal_information", u)
|
EncryptedSharedPreferencesUtil.save(requireContext(), "Personal_information", u)
|
||||||
|
|
||||||
if (showToast) Toast.makeText(requireContext(), "已刷新", Toast.LENGTH_SHORT).show()
|
if (showToast) Toast.makeText(requireContext(), "Refreshed", Toast.LENGTH_SHORT).show()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is kotlinx.coroutines.CancellationException) return@launch
|
if (e is kotlinx.coroutines.CancellationException) return@launch
|
||||||
Log.e(TAG, "getUser failed", e)
|
Log.e(TAG, "getUser failed", e)
|
||||||
if (showToast && isAdded) Toast.makeText(requireContext(), "刷新失败", Toast.LENGTH_SHORT).show()
|
if (showToast && isAdded) Toast.makeText(requireContext(), "Refresh failed", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,6 +219,9 @@ class MineFragment : Fragment() {
|
|||||||
// 清空 UI
|
// 清空 UI
|
||||||
nickname.text = ""
|
nickname.text = ""
|
||||||
time.text = ""
|
time.text = ""
|
||||||
|
Glide.with(requireContext())
|
||||||
|
.load(R.drawable.default_avatar)
|
||||||
|
.into(avatar)
|
||||||
|
|
||||||
// 触发登出事件,让MainActivity打开登录页面
|
// 触发登出事件,让MainActivity打开登录页面
|
||||||
AuthEventBus.emit(AuthEvent.Logout(returnTabTag = "tab_mine"))
|
AuthEventBus.emit(AuthEvent.Logout(returnTabTag = "tab_mine"))
|
||||||
@@ -209,4 +251,7 @@ class MineFragment : Fragment() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "1314520-MineFragment"
|
private const val TAG = "1314520-MineFragment"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getinviteCode(): ApiResponse<ShareResponse>? =
|
||||||
|
runCatching<ApiResponse<ShareResponse>> { RetrofitClient.apiService.inviteCode() }.getOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.example.myapplication.ui.mine.consumptionRecord
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.TransactionRecord
|
||||||
|
|
||||||
|
class TransactionAdapter(
|
||||||
|
private val data: MutableList<TransactionRecord>,
|
||||||
|
private val onCloseClick: () -> Unit,
|
||||||
|
private val onRechargeClick: () -> Unit
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_HEADER = 0
|
||||||
|
private const val TYPE_ITEM = 1
|
||||||
|
private const val TYPE_FOOTER = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header: balance
|
||||||
|
private var headerBalanceText: String = "0.00"
|
||||||
|
|
||||||
|
// Footer state
|
||||||
|
private var showFooter: Boolean = false
|
||||||
|
private var footerNoMore: Boolean = false
|
||||||
|
|
||||||
|
fun updateHeaderBalance(text: Any?) {
|
||||||
|
headerBalanceText = (text ?: "0.00").toString()
|
||||||
|
notifyItemChanged(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFooterLoading() {
|
||||||
|
showFooter = true
|
||||||
|
footerNoMore = false
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFooterNoMore() {
|
||||||
|
showFooter = true
|
||||||
|
footerNoMore = true
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideFooter() {
|
||||||
|
showFooter = false
|
||||||
|
footerNoMore = false
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceAll(list: List<TransactionRecord>) {
|
||||||
|
data.clear()
|
||||||
|
data.addAll(list)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun append(list: List<TransactionRecord>) {
|
||||||
|
if (list.isEmpty()) return
|
||||||
|
val start = 1 + data.size // header占1
|
||||||
|
data.addAll(list)
|
||||||
|
notifyItemRangeInserted(start, list.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
// header + items + optional footer
|
||||||
|
return 1 + data.size + if (showFooter) 1 else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when {
|
||||||
|
position == 0 -> TYPE_HEADER
|
||||||
|
showFooter && position == itemCount - 1 -> TYPE_FOOTER
|
||||||
|
else -> TYPE_ITEM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
return when (viewType) {
|
||||||
|
TYPE_HEADER -> {
|
||||||
|
val v = inflater.inflate(R.layout.layout_consumption_record_header, parent, false)
|
||||||
|
HeaderVH(v, onCloseClick, onRechargeClick)
|
||||||
|
}
|
||||||
|
TYPE_FOOTER -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_loading_footer, parent, false)
|
||||||
|
FooterVH(v)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val v = inflater.inflate(R.layout.item_transaction_record, parent, false)
|
||||||
|
ItemVH(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (holder) {
|
||||||
|
is HeaderVH -> holder.bind(headerBalanceText)
|
||||||
|
is FooterVH -> holder.bind(footerNoMore)
|
||||||
|
is ItemVH -> holder.bind(data[position - 1]) // position-1 because header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderVH(
|
||||||
|
itemView: View,
|
||||||
|
onCloseClick: () -> Unit,
|
||||||
|
onRechargeClick: () -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
private val balance: TextView = itemView.findViewById(R.id.balance)
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener { onCloseClick() }
|
||||||
|
itemView.findViewById<View>(R.id.rechargeButton).setOnClickListener { onRechargeClick() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(balanceText: String) {
|
||||||
|
balance.text = balanceText
|
||||||
|
adjustBalanceTextSize(balance, balanceText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustBalanceTextSize(tv: TextView, text: String) {
|
||||||
|
tv.textSize = when (text.length) {
|
||||||
|
in 0..3 -> 40f
|
||||||
|
4 -> 36f
|
||||||
|
5 -> 32f
|
||||||
|
6 -> 28f
|
||||||
|
7 -> 24f
|
||||||
|
8 -> 22f
|
||||||
|
9 -> 20f
|
||||||
|
else -> 16f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ItemVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val tvTime: TextView = itemView.findViewById(R.id.tvTime)
|
||||||
|
private val tvDesc: TextView = itemView.findViewById(R.id.tvDesc)
|
||||||
|
private val tvAmount: TextView = itemView.findViewById(R.id.tvAmount)
|
||||||
|
|
||||||
|
fun bind(item: TransactionRecord) {
|
||||||
|
tvTime.text = item.createdAt
|
||||||
|
tvDesc.text = item.description
|
||||||
|
tvAmount.text = "${item.amount}"
|
||||||
|
|
||||||
|
// 根据type设置字体颜色
|
||||||
|
val color = when (item.type) {
|
||||||
|
1 -> Color.parseColor("#CD2853") // 收入 - 红色
|
||||||
|
2 -> Color.parseColor("#66CD7C") // 支出 - 绿色
|
||||||
|
else -> tvAmount.currentTextColor // 保持当前颜色
|
||||||
|
}
|
||||||
|
tvAmount.setTextColor(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FooterVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val progress: ProgressBar = itemView.findViewById(R.id.progress)
|
||||||
|
private val tv: TextView = itemView.findViewById(R.id.tvLoading)
|
||||||
|
|
||||||
|
fun bind(noMore: Boolean) {
|
||||||
|
if (noMore) {
|
||||||
|
progress.visibility = View.GONE
|
||||||
|
tv.text = "No more"
|
||||||
|
} else {
|
||||||
|
progress.visibility = View.VISIBLE
|
||||||
|
tv.text = "Loading..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package com.example.myapplication.ui.mine.consumptionRecord
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.ApiResponse
|
||||||
|
import com.example.myapplication.network.RetrofitClient
|
||||||
|
import com.example.myapplication.network.TransactionRecord
|
||||||
|
import com.example.myapplication.network.Wallet
|
||||||
|
import com.example.myapplication.network.transactionsRequest
|
||||||
|
import com.example.myapplication.network.transactionsResponse
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ConsumptionRecordFragment : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||||
|
private lateinit var rv: RecyclerView
|
||||||
|
private lateinit var adapter: TransactionAdapter
|
||||||
|
|
||||||
|
private val listData = arrayListOf<TransactionRecord>()
|
||||||
|
|
||||||
|
private var pageNum = 1
|
||||||
|
private val pageSize = 10
|
||||||
|
private var totalPages = Int.MAX_VALUE
|
||||||
|
private var isLoading = false
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_consumption_record, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
swipeRefresh = view.findViewById(R.id.swipeRefresh)
|
||||||
|
rv = view.findViewById(R.id.rvTransactions)
|
||||||
|
|
||||||
|
setupRecycler()
|
||||||
|
setupRefresh()
|
||||||
|
|
||||||
|
refreshAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ 重点:关闭必须走 NavController popBackStack
|
||||||
|
* 不要 dismiss(),否则 global_nav 栈不会变,底部导航就会一直被隐藏
|
||||||
|
*/
|
||||||
|
private fun closeByNav() {
|
||||||
|
runCatching {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}.onFailure {
|
||||||
|
// 万一不是走 nav 打开的(极少情况),再兜底 dismiss
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(dialog: DialogInterface) {
|
||||||
|
super.onCancel(dialog)
|
||||||
|
// ✅ 用户手势下拉/点外部取消,也要 pop 返回栈
|
||||||
|
closeByNav()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
// ✅ 有些机型/场景只走 onDismiss,不走 onCancel,双保险
|
||||||
|
closeByNav()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecycler() {
|
||||||
|
adapter = TransactionAdapter(
|
||||||
|
data = listData,
|
||||||
|
onCloseClick = { closeByNav() }, // ✅ 改这里:不要 dismiss()
|
||||||
|
onRechargeClick = {
|
||||||
|
AuthEventBus.emit(AuthEvent.OpenGlobalPage(R.id.goldCoinRechargeFragment))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
rv.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
rv.adapter = adapter
|
||||||
|
|
||||||
|
rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
if (newState != RecyclerView.SCROLL_STATE_IDLE) return
|
||||||
|
|
||||||
|
val reachedBottom = !recyclerView.canScrollVertically(1)
|
||||||
|
if (!reachedBottom) return
|
||||||
|
|
||||||
|
if (!isLoading && pageNum < totalPages) {
|
||||||
|
loadMore()
|
||||||
|
} else if (!isLoading && pageNum >= totalPages) {
|
||||||
|
adapter.setFooterNoMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRefresh() {
|
||||||
|
swipeRefresh.setOnRefreshListener { refreshAll() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshAll() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
swipeRefresh.isRefreshing = true
|
||||||
|
|
||||||
|
pageNum = 1
|
||||||
|
totalPages = Int.MAX_VALUE
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
adapter.hideFooter()
|
||||||
|
adapter.replaceAll(emptyList())
|
||||||
|
|
||||||
|
val walletResp = getwalletBalance()
|
||||||
|
val balanceText = walletResp?.data?.balanceDisplay ?: "0.00"
|
||||||
|
adapter.updateHeaderBalance(balanceText)
|
||||||
|
|
||||||
|
loadPage(targetPage = 1, isRefresh = true)
|
||||||
|
|
||||||
|
swipeRefresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMore() {
|
||||||
|
lifecycleScope.launch { loadPage(targetPage = pageNum + 1, isRefresh = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPage(targetPage: Int, isRefresh: Boolean) {
|
||||||
|
if (isLoading) return
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
if (!isRefresh) adapter.setFooterLoading()
|
||||||
|
|
||||||
|
val body = transactionsRequest(pageNum = targetPage, pageSize = pageSize)
|
||||||
|
val resp = gettransactions(body)
|
||||||
|
val data = resp?.data
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
totalPages = data.pages
|
||||||
|
pageNum = data.current
|
||||||
|
|
||||||
|
val records = data.records
|
||||||
|
|
||||||
|
if (isRefresh) adapter.replaceAll(records) else adapter.append(records)
|
||||||
|
|
||||||
|
if (pageNum >= totalPages) adapter.setFooterNoMore() else adapter.hideFooter()
|
||||||
|
|
||||||
|
rv.post {
|
||||||
|
val notScrollableYet = !rv.canScrollVertically(1)
|
||||||
|
if (!isLoading && notScrollableYet && pageNum < totalPages) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
adapter.hideFooter()
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================网络请求===========================================
|
||||||
|
private suspend fun getwalletBalance(): ApiResponse<Wallet>? =
|
||||||
|
runCatching { RetrofitClient.apiService.walletBalance() }.getOrNull()
|
||||||
|
|
||||||
|
private suspend fun gettransactions(body: transactionsRequest): ApiResponse<transactionsResponse>? =
|
||||||
|
runCatching { RetrofitClient.apiService.transactions(body) }.getOrNull()
|
||||||
|
}
|
||||||
@@ -2,16 +2,20 @@ package com.example.myapplication.ui.mine.myotherpages
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.example.myapplication.R
|
import com.example.myapplication.R
|
||||||
import android.widget.FrameLayout
|
import com.example.myapplication.network.ApiResponse
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.example.myapplication.network.RetrofitClient
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.example.myapplication.network.feedbackRequest
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import java.util.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class FeedbackFragment : Fragment() {
|
class FeedbackFragment : Fragment() {
|
||||||
|
|
||||||
@@ -26,9 +30,51 @@ class FeedbackFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// 设置关闭按钮点击事件
|
// 关闭按钮
|
||||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||||
parentFragmentManager.popBackStack()
|
parentFragmentManager.popBackStack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 让多行输入框:不聚焦也能上下滑动内容
|
||||||
|
val etFeedback = view.findViewById<TextInputEditText>(R.id.et_feedback)
|
||||||
|
etFeedback.apply {
|
||||||
|
// 可选:让它本身可滚动(你 XML 已经写了也没问题)
|
||||||
|
isVerticalScrollBarEnabled = true
|
||||||
|
|
||||||
|
setOnTouchListener { v, event ->
|
||||||
|
// 告诉父布局(NestedScrollView)先别抢这个触摸事件
|
||||||
|
v.parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
|
||||||
|
// 手指抬起/取消时,把控制权还给父布局(页面还能继续滚)
|
||||||
|
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||||
|
v.parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// false:不吞事件,让 EditText 自己处理滚动/光标
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交反馈按钮点击事件
|
||||||
|
view.findViewById<View>(R.id.btn_keyboard).setOnClickListener {
|
||||||
|
val feedbackText = etFeedback.text.toString().trim()
|
||||||
|
if (feedbackText.isEmpty()) {
|
||||||
|
Toast.makeText(context, "Please enter your feedback", Toast.LENGTH_SHORT).show()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
val response = submitFeedback(feedbackRequest(content = feedbackText))
|
||||||
|
if (response?.code == 0) {
|
||||||
|
Toast.makeText(context, "Feedback submitted successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
parentFragmentManager.popBackStack()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Failed to submit feedback", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//提交反馈
|
||||||
|
private suspend fun submitFeedback(body:feedbackRequest): ApiResponse<Boolean>? =
|
||||||
|
runCatching<ApiResponse<Boolean>> { RetrofitClient.apiService.feedback(body) }.getOrNull()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package com.example.myapplication.ui.mine.myotherpages
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearSnapHelper
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.myapplication.R
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class GenderSelectSheet : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private val values = listOf("Male", "Female", "The third gender")
|
||||||
|
private var selectedIndex = 0
|
||||||
|
|
||||||
|
private val itemHeightDp = 48f // 每行高度(和 Adapter 里一致)
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val view = inflater.inflate(R.layout.sheet_select_gender, container, false)
|
||||||
|
|
||||||
|
selectedIndex = (arguments?.getInt(ARG_INITIAL) ?: 0).coerceIn(0, values.lastIndex)
|
||||||
|
|
||||||
|
val rv = view.findViewById<RecyclerView>(R.id.gender_wheel)
|
||||||
|
|
||||||
|
val layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
|
||||||
|
rv.layoutManager = layoutManager
|
||||||
|
rv.adapter = WheelAdapter(values, dpToPx(itemHeightDp))
|
||||||
|
rv.overScrollMode = View.OVER_SCROLL_NEVER
|
||||||
|
rv.isNestedScrollingEnabled = true
|
||||||
|
rv.clipToPadding = false
|
||||||
|
|
||||||
|
val snapHelper = LinearSnapHelper()
|
||||||
|
snapHelper.attachToRecyclerView(rv)
|
||||||
|
|
||||||
|
// ✅ 关键:触摸滚轮时不让 BottomSheet 抢手势(否则会拖动弹窗)
|
||||||
|
rv.setOnTouchListener { v, event ->
|
||||||
|
when (event.actionMasked) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
setSheetDraggable(false)
|
||||||
|
v.parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_MOVE -> v.parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
setSheetDraggable(true)
|
||||||
|
v.parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
updateChildColors(recyclerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
|
val snapView = snapHelper.findSnapView(layoutManager) ?: return
|
||||||
|
val pos = layoutManager.getPosition(snapView).coerceIn(0, values.lastIndex)
|
||||||
|
selectedIndex = pos
|
||||||
|
updateChildColors(recyclerView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ 关键:给上下 padding,让 3 条内容也能滚动/居中吸附
|
||||||
|
rv.post {
|
||||||
|
val itemPx = dpToPx(itemHeightDp)
|
||||||
|
val pad = (rv.height / 2 - itemPx / 2).coerceAtLeast(0)
|
||||||
|
rv.setPadding(rv.paddingLeft, pad, rv.paddingRight, pad)
|
||||||
|
|
||||||
|
layoutManager.scrollToPositionWithOffset(selectedIndex, pad)
|
||||||
|
rv.post { updateChildColors(rv) }
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<View>(R.id.btn_close).setOnClickListener { dismiss() }
|
||||||
|
|
||||||
|
view.findViewById<View>(R.id.btn_save).setOnClickListener {
|
||||||
|
parentFragmentManager.setFragmentResult(
|
||||||
|
REQ_KEY,
|
||||||
|
Bundle().apply { putInt(BUNDLE_KEY_GENDER, selectedIndex) }
|
||||||
|
)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSheetDraggable(draggable: Boolean) {
|
||||||
|
val d = dialog as? BottomSheetDialog ?: return
|
||||||
|
val sheet = d.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) ?: return
|
||||||
|
BottomSheetBehavior.from(sheet).isDraggable = draggable
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据距离中心点决定文字颜色 */
|
||||||
|
private fun updateChildColors(rv: RecyclerView) {
|
||||||
|
val centerY = rv.height / 2f
|
||||||
|
val selectedColor = Color.parseColor("#02BEAC")
|
||||||
|
val normalColor = Color.parseColor("#B5B5B5")
|
||||||
|
|
||||||
|
for (i in 0 until rv.childCount) {
|
||||||
|
val child = rv.getChildAt(i)
|
||||||
|
val tv = child as? TextView ?: continue
|
||||||
|
|
||||||
|
val childCenterY = (child.top + child.bottom) / 2f
|
||||||
|
val distance = abs(childCenterY - centerY)
|
||||||
|
|
||||||
|
val isSelected = distance < dpToPx(8f)
|
||||||
|
tv.setTextColor(if (isSelected) selectedColor else normalColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQ_KEY = "req_select_gender"
|
||||||
|
const val BUNDLE_KEY_GENDER = "bundle_gender"
|
||||||
|
private const val ARG_INITIAL = "arg_initial_gender"
|
||||||
|
|
||||||
|
fun newInstance(initialGender: Int) = GenderSelectSheet().apply {
|
||||||
|
arguments = Bundle().apply { putInt(ARG_INITIAL, initialGender) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dpToPx(dp: Float): Int =
|
||||||
|
(dp * resources.displayMetrics.density + 0.5f).toInt()
|
||||||
|
|
||||||
|
private class WheelAdapter(
|
||||||
|
private val items: List<String>,
|
||||||
|
private val itemHeightPx: Int
|
||||||
|
) : RecyclerView.Adapter<WheelAdapter.VH>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val tv = TextView(parent.context).apply {
|
||||||
|
layoutParams = RecyclerView.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
itemHeightPx
|
||||||
|
)
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
textSize = 18f
|
||||||
|
setTextColor(Color.parseColor("#B5B5B5"))
|
||||||
|
}
|
||||||
|
return VH(tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
(holder.itemView as TextView).text = items[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
class VH(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.example.myapplication.ui.mine.myotherpages
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.setPadding
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.example.myapplication.R
|
||||||
|
|
||||||
|
class NicknameEditSheet : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
dialog?.window?.setSoftInputMode(
|
||||||
|
android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or
|
||||||
|
android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val view = inflater.inflate(R.layout.sheet_edit_nickname, container, false)
|
||||||
|
val initial = arguments?.getString(ARG_INITIAL).orEmpty()
|
||||||
|
|
||||||
|
val et = view.findViewById<TextInputEditText>(R.id.et_nickname)
|
||||||
|
et.setText(initial)
|
||||||
|
|
||||||
|
view.findViewById<View>(R.id.btn_close).setOnClickListener { dismiss() }
|
||||||
|
|
||||||
|
view.findViewById<View>(R.id.btn_save).setOnClickListener {
|
||||||
|
val nickname = et.text?.toString()?.trim().orEmpty()
|
||||||
|
if (nickname.isBlank()) return@setOnClickListener
|
||||||
|
|
||||||
|
parentFragmentManager.setFragmentResult(
|
||||||
|
REQ_KEY,
|
||||||
|
Bundle().apply { putString(BUNDLE_KEY_NICKNAME, nickname) }
|
||||||
|
)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQ_KEY = "req_edit_nickname"
|
||||||
|
const val BUNDLE_KEY_NICKNAME = "bundle_nickname"
|
||||||
|
private const val ARG_INITIAL = "arg_initial_nickname"
|
||||||
|
|
||||||
|
fun newInstance(initial: String) = NicknameEditSheet().apply {
|
||||||
|
arguments = Bundle().apply { putString(ARG_INITIAL, initial) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,386 @@
|
|||||||
package com.example.myapplication.ui.mine.myotherpages
|
package com.example.myapplication.ui.mine.myotherpages
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.example.myapplication.R
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.example.myapplication.R
|
||||||
|
import com.example.myapplication.network.ApiResponse
|
||||||
|
import com.example.myapplication.network.AuthEvent
|
||||||
|
import com.example.myapplication.network.AuthEventBus
|
||||||
|
import com.example.myapplication.network.RetrofitClient
|
||||||
|
import com.example.myapplication.network.User
|
||||||
|
import com.example.myapplication.ui.common.LoadingOverlay
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import com.google.android.material.button.MaterialButton
|
import de.hdodenhof.circleimageview.CircleImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import com.example.myapplication.network.updateInfoRequest
|
||||||
|
import android.util.Log
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class PersonalSettings : BottomSheetDialogFragment() {
|
class PersonalSettings : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private var user: User? = null
|
||||||
|
|
||||||
|
private lateinit var avatar: CircleImageView
|
||||||
|
private lateinit var tvNickname: TextView
|
||||||
|
private lateinit var tvGender: TextView
|
||||||
|
private lateinit var tvUserId: TextView
|
||||||
|
private lateinit var loadingOverlay: LoadingOverlay
|
||||||
|
|
||||||
|
// ActivityResultLauncher for image selection - restrict to image types
|
||||||
|
private val galleryLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||||
|
uri?.let { handleImageResult(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cameraLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { success: Boolean ->
|
||||||
|
if (success) {
|
||||||
|
cameraImageUri?.let { handleImageResult(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cameraImageUri: Uri? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View = inflater.inflate(R.layout.personal_settings, container, false)
|
||||||
return inflater.inflate(R.layout.personal_settings, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// 设置关闭按钮点击事件
|
// 初始化loadingOverlay
|
||||||
|
loadingOverlay = LoadingOverlay.attach(requireView() as ViewGroup)
|
||||||
|
|
||||||
|
// 关闭
|
||||||
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
view.findViewById<FrameLayout>(R.id.iv_close).setOnClickListener {
|
||||||
parentFragmentManager.popBackStack()
|
AuthEventBus.emit(AuthEvent.UserUpdated)
|
||||||
|
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind
|
||||||
|
avatar = view.findViewById(R.id.avatar)
|
||||||
|
tvNickname = view.findViewById(R.id.tv_nickname_value)
|
||||||
|
tvGender = view.findViewById(R.id.tv_gender_value)
|
||||||
|
tvUserId = view.findViewById(R.id.tv_userid_value)
|
||||||
|
|
||||||
|
// Avatar click listener
|
||||||
|
avatar.setOnClickListener {
|
||||||
|
showImagePickerDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== FragmentResult listeners =====================
|
||||||
|
|
||||||
|
// 昵称保存回传
|
||||||
|
parentFragmentManager.setFragmentResultListener(
|
||||||
|
NicknameEditSheet.REQ_KEY,
|
||||||
|
viewLifecycleOwner
|
||||||
|
) { _, bundle ->
|
||||||
|
val newName = bundle.getString(NicknameEditSheet.BUNDLE_KEY_NICKNAME).orEmpty()
|
||||||
|
if (newName.isBlank()) return@setFragmentResultListener
|
||||||
|
lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
val ReturnValue = setupdateUserInfo(updateInfoRequest(nickName = newName))
|
||||||
|
Log.d("1314520-PersonalSettings", "setupdateUserInfo: $ReturnValue")
|
||||||
|
if(ReturnValue?.code == 0){
|
||||||
|
tvNickname.text = newName
|
||||||
|
}
|
||||||
|
user = user?.copy(nickName = newName)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("PersonalSettings", "Failed to update nickname", e)
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 性别保存回传
|
||||||
|
parentFragmentManager.setFragmentResultListener(
|
||||||
|
GenderSelectSheet.REQ_KEY,
|
||||||
|
viewLifecycleOwner
|
||||||
|
) { _, bundle ->
|
||||||
|
val newGender = bundle.getInt(GenderSelectSheet.BUNDLE_KEY_GENDER, 0)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
val ReturnValue = setupdateUserInfo(updateInfoRequest(gender = newGender))
|
||||||
|
if(ReturnValue?.code == 0){
|
||||||
|
tvGender.text = genderText(newGender)
|
||||||
|
}
|
||||||
|
user = user?.copy(gender = newGender)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("PersonalSettings", "Failed to update gender", e)
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== row click =====================
|
||||||
|
|
||||||
|
// Nickname:打开编辑 BottomSheet(arguments 传初始值)
|
||||||
|
view.findViewById<View>(R.id.row_nickname).setOnClickListener {
|
||||||
|
NicknameEditSheet.newInstance(user?.nickName.orEmpty())
|
||||||
|
.show(parentFragmentManager, "NicknameEditSheet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gender:打开选择 BottomSheet
|
||||||
|
view.findViewById<View>(R.id.row_gender).setOnClickListener {
|
||||||
|
GenderSelectSheet.newInstance(user?.gender ?: 0)
|
||||||
|
.show(parentFragmentManager, "GenderSelectSheet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserID:点击复制
|
||||||
|
view.findViewById<View>(R.id.row_userid).setOnClickListener {
|
||||||
|
val uid = user?.uid?.toString() ?: tvUserId.text?.toString().orEmpty()
|
||||||
|
if (uid.isBlank()) return@setOnClickListener
|
||||||
|
copyToClipboard(uid)
|
||||||
|
Toast.makeText(requireContext(), "Copy successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== load & render =====================
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
val resp = getUserdata()
|
||||||
|
val u = resp?.data // 如果你的 ApiResponse 字段不是 data,这里改成你的字段名
|
||||||
|
if (u == null) {
|
||||||
|
Toast.makeText(requireContext(), "Load failed", Toast.LENGTH_SHORT).show()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
user = u
|
||||||
|
renderUser(u)
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderUser(u: User) {
|
||||||
|
tvNickname.text = u.nickName
|
||||||
|
tvGender.text = genderText(u.gender)
|
||||||
|
tvUserId.text = u.uid.toString()
|
||||||
|
|
||||||
|
Glide.with(this)
|
||||||
|
.load(u.avatarUrl)
|
||||||
|
.placeholder(R.drawable.default_avatar)
|
||||||
|
.error(R.drawable.default_avatar)
|
||||||
|
.into(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun genderText(gender: Int): String = when (gender) {
|
||||||
|
1 -> "Female"
|
||||||
|
2 -> "The third gender"
|
||||||
|
0 -> "Male"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyToClipboard(text: String) {
|
||||||
|
val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
cm.setPrimaryClip(ClipData.newPlainText("user_id", text))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getUserdata(): ApiResponse<User>? =
|
||||||
|
runCatching { RetrofitClient.apiService.getUser() }.getOrNull()
|
||||||
|
|
||||||
|
private suspend fun setupdateUserInfo(body: updateInfoRequest): ApiResponse<Boolean>? =
|
||||||
|
runCatching { RetrofitClient.apiService.updateUserInfo(body) }.getOrNull()
|
||||||
|
|
||||||
|
private val cameraPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
cameraImageUri = createImageFile()
|
||||||
|
cameraImageUri?.let { cameraLauncher.launch(it) }
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
"Camera permission is required to take photos",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showImagePickerDialog() {
|
||||||
|
val options = arrayOf(
|
||||||
|
getString(R.string.choose_from_gallery),
|
||||||
|
getString(R.string.take_photo)
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.change_avatar)
|
||||||
|
.setItems(options) { _, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> galleryLauncher.launch("image/png,image/jpeg")
|
||||||
|
1 -> {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
android.Manifest.permission.CAMERA
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
cameraImageUri = createImageFile()
|
||||||
|
cameraImageUri?.let { cameraLauncher.launch(it) }
|
||||||
|
} else {
|
||||||
|
cameraPermissionLauncher.launch(android.Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleImageResult(uri: Uri) {
|
||||||
|
Glide.with(this)
|
||||||
|
.load(uri)
|
||||||
|
.into(avatar)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
uploadAvatar(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun uploadAvatar(uri: Uri) {
|
||||||
|
loadingOverlay.show()
|
||||||
|
try {
|
||||||
|
// Get MIME type to determine image format
|
||||||
|
val mimeType = requireContext().contentResolver.getType(uri)
|
||||||
|
val isPng = mimeType?.equals("image/png", ignoreCase = true) == true
|
||||||
|
|
||||||
|
// Determine file extension and compression format based on MIME type
|
||||||
|
val fileExtension = if (isPng) ".png" else ".jpg"
|
||||||
|
val compressFormat = if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
|
||||||
|
val mediaType = if (isPng) "image/png" else "image/jpeg"
|
||||||
|
|
||||||
|
val inputStream = requireContext().contentResolver.openInputStream(uri) ?: return
|
||||||
|
val storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val tempFile = File.createTempFile(
|
||||||
|
"UPLOAD_${timeStamp}_",
|
||||||
|
fileExtension,
|
||||||
|
storageDir
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read and compress image if needed
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
options.inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeStream(inputStream, null, options)
|
||||||
|
inputStream.close()
|
||||||
|
|
||||||
|
// Calculate inSampleSize
|
||||||
|
var inSampleSize = 1
|
||||||
|
val maxSize = 5 * 1024 * 1024 // 5MB
|
||||||
|
if (options.outHeight * options.outWidth * 4 > maxSize) {
|
||||||
|
val halfHeight = options.outHeight / 2
|
||||||
|
val halfWidth = options.outWidth / 2
|
||||||
|
while (halfHeight / inSampleSize >= 1024 && halfWidth / inSampleSize >= 1024) {
|
||||||
|
inSampleSize *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode with inSampleSize
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
options.inSampleSize = inSampleSize
|
||||||
|
val inputStream2 = requireContext().contentResolver.openInputStream(uri) ?: return
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream2, null, options)
|
||||||
|
inputStream2.close()
|
||||||
|
|
||||||
|
// Compress to file
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
bitmap?.let { bmp ->
|
||||||
|
if (isPng) {
|
||||||
|
// PNG compression (quality parameter is ignored for PNG)
|
||||||
|
bmp.compress(Bitmap.CompressFormat.PNG, 100, output)
|
||||||
|
} else {
|
||||||
|
// JPEG compression with quality adjustment
|
||||||
|
var quality = 90
|
||||||
|
do {
|
||||||
|
output.channel.truncate(0)
|
||||||
|
bmp.compress(Bitmap.CompressFormat.JPEG, quality, output)
|
||||||
|
quality -= 10
|
||||||
|
} while (tempFile.length() > maxSize && quality > 10)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
Toast.makeText(requireContext(), "Failed to decode image", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempFile.length() > maxSize) {
|
||||||
|
Toast.makeText(requireContext(), "Image is too large after compression", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestFile = RequestBody.create(mediaType.toMediaTypeOrNull(), tempFile)
|
||||||
|
val body = MultipartBody.Part.createFormData("file", tempFile.name, requestFile)
|
||||||
|
|
||||||
|
val response = RetrofitClient.createFileUploadService()
|
||||||
|
.uploadFile("avatar", body)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
bitmap?.recycle()
|
||||||
|
tempFile.delete()
|
||||||
|
|
||||||
|
if (response?.code == 0) {
|
||||||
|
val ReturnValue = setupdateUserInfo(updateInfoRequest(avatarUrl = response.data))
|
||||||
|
Toast.makeText(requireContext(), R.string.avatar_updated, Toast.LENGTH_SHORT).show()
|
||||||
|
user = user?.copy(avatarUrl = response.data)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), R.string.upload_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(requireContext(), R.string.upload_error, Toast.LENGTH_SHORT).show()
|
||||||
|
Log.e("PersonalSettings", "Upload avatar error", e)
|
||||||
|
} finally {
|
||||||
|
loadingOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createImageFile(): Uri? {
|
||||||
|
val storageDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
|
val imageFile = File.createTempFile(
|
||||||
|
"JPEG_${timeStamp}_",
|
||||||
|
".jpg",
|
||||||
|
storageDir
|
||||||
|
)
|
||||||
|
return FileProvider.getUriForFile(
|
||||||
|
requireContext(),
|
||||||
|
"${requireContext().packageName}.fileprovider",
|
||||||
|
imageFile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
loadingOverlay.remove()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
app/src/main/res/drawable/associate_close.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
9
app/src/main/res/drawable/complete_close_bg.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FFFFFF" /> <!-- 40% 透明白 -->
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="4dp"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="4dp" />
|
||||||
|
</shape>
|
||||||
19
app/src/main/res/drawable/consumption_details_bg.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- 透明背景:保证不会出现“有背景颜色” -->
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@android:color/transparent" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<!-- 底部边框:固定在底部,只画一条线 -->
|
||||||
|
<item android:gravity="bottom">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<size android:height="1dp" />
|
||||||
|
<solid android:color="#F3F3F3" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</layer-list>
|
||||||
BIN
app/src/main/res/drawable/first_add.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
@@ -2,4 +2,5 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
<solid android:color="#FAFAFA"/>
|
<solid android:color="#FAFAFA"/>
|
||||||
<corners android:radius="11dp"/>
|
<corners android:radius="11dp"/>
|
||||||
|
<stroke android:width="2dp" android:color="#FAFAFA"/>
|
||||||
</shape>
|
</shape>
|
||||||
6
app/src/main/res/drawable/gender_background_select.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
|
<solid android:color="#FAFAFA"/>
|
||||||
|
<corners android:radius="11dp"/>
|
||||||
|
<stroke android:width="2dp" android:color="#02BEAC"/>
|
||||||
|
</shape>
|
||||||
7
app/src/main/res/drawable/gold_coin_bg.xml
Normal file
@@ -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="#70C5FFF6"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
<!-- 白色边框 -->
|
||||||
|
<stroke android:width="2dp" android:color="#FFFFFF"/>
|
||||||
|
</shape>
|
||||||
5
app/src/main/res/drawable/list_two_bg_already.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#F1F1F1" /> <!-- 背景色 -->
|
||||||
|
<corners android:radius="5dp" /> <!-- 圆角半径,越大越圆 -->
|
||||||
|
</shape>
|
||||||
5
app/src/main/res/drawable/my_keyboard_cancel.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="#F3F4F6"/> <!-- 灰色背景 -->
|
||||||
|
<corners android:radius="50dp"/> <!-- 大圆角 -->
|
||||||
|
</shape>
|
||||||
BIN
app/src/main/res/drawable/operation_add.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/drawable/pop_collapse.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/drawable/record.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable/round_bg_others_add.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
7
app/src/main/res/drawable/round_bg_others_already.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color=" #F1F1F1" /> <!-- 背景色 -->
|
||||||
|
<corners android:radius="12dp" /> <!-- 圆角半径,越大越圆 -->
|
||||||
|
|
||||||
|
</shape>
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
|
|
||||||
<solid android:color=" #FFEEE6" /> <!-- 背景色 -->
|
<solid android:color=" #FFEEE6" /> <!-- 背景色 -->
|
||||||
<corners android:radius="14dp" /> <!-- 圆角半径,越大越圆 -->
|
<corners android:radius="12dp" /> <!-- 圆角半径,越大越圆 -->
|
||||||
|
|
||||||
</shape>
|
</shape>
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
|
|
||||||
<solid android:color=" #D6E7FF" /> <!-- 背景色 -->
|
<solid android:color=" #D6E7FF" /> <!-- 背景色 -->
|
||||||
<corners android:radius="14dp" /> <!-- 圆角半径,越大越圆 -->
|
<corners android:radius="12dp" /> <!-- 圆角半径,越大越圆 -->
|
||||||
|
|
||||||
</shape>
|
</shape>
|
||||||
|
|||||||
BIN
app/src/main/res/drawable/second_add.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/drawable/third_add.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
@@ -80,9 +80,10 @@
|
|||||||
android:src="@drawable/male" />
|
android:src="@drawable/male" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/male_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="87dp"
|
android:layout_height="87dp"
|
||||||
android:layout_marginTop="-87dp"
|
android:layout_marginTop="-85dp"
|
||||||
android:background="@drawable/gender_background"
|
android:background="@drawable/gender_background"
|
||||||
android:gravity="end"
|
android:gravity="end"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
@@ -117,9 +118,10 @@
|
|||||||
android:src="@drawable/female" />
|
android:src="@drawable/female" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/female_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="87dp"
|
android:layout_height="87dp"
|
||||||
android:layout_marginTop="-87dp"
|
android:layout_marginTop="-95dp"
|
||||||
android:background="@drawable/gender_background"
|
android:background="@drawable/gender_background"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@@ -161,6 +163,7 @@
|
|||||||
android:src="@drawable/question_mark_one" />
|
android:src="@drawable/question_mark_one" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/third_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="87dp"
|
android:layout_height="87dp"
|
||||||
android:layout_marginTop="-87dp"
|
android:layout_marginTop="-87dp"
|
||||||
|
|||||||
@@ -63,17 +63,19 @@
|
|||||||
android:textSize="10sp"
|
android:textSize="10sp"
|
||||||
android:textColor="#1B1F1A" />
|
android:textColor="#1B1F1A" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/btn_add_second"
|
android:id="@+id/btn_add_second"
|
||||||
android:layout_width="60dp"
|
|
||||||
android:layout_height="28dp"
|
|
||||||
android:background="@drawable/round_bg_two"
|
|
||||||
android:layout_marginTop="50dp"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="+"
|
android:layout_width="60dp"
|
||||||
android:textColor="#6EA0EB"
|
android:layout_marginTop="50dp"
|
||||||
android:textSize="20dp"
|
android:layout_height="28dp"
|
||||||
android:textStyle="bold" />
|
android:background="@drawable/round_bg_two">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/add_second_icon"
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="15dp"
|
||||||
|
android:src="@drawable/second_add"/>
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 第一名 -->
|
<!-- 第一名 -->
|
||||||
@@ -117,17 +119,19 @@
|
|||||||
android:textSize="10sp"
|
android:textSize="10sp"
|
||||||
android:textColor="#1B1F1A" />
|
android:textColor="#1B1F1A" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/btn_add_first"
|
android:id="@+id/btn_add_first"
|
||||||
android:layout_width="60dp"
|
|
||||||
android:layout_height="28dp"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:layout_width="60dp"
|
||||||
android:layout_marginTop="50dp"
|
android:layout_marginTop="50dp"
|
||||||
android:text="+"
|
android:layout_height="28dp"
|
||||||
android:textSize="20dp"
|
android:background="@drawable/round_bg_one">
|
||||||
android:textStyle="bold"
|
<ImageView
|
||||||
android:textColor="#F0C729"
|
android:id="@+id/add_first_icon"
|
||||||
android:background="@drawable/round_bg_one" />
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="15dp"
|
||||||
|
android:src="@drawable/first_add"/>
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 第三名 -->
|
<!-- 第三名 -->
|
||||||
@@ -172,17 +176,20 @@
|
|||||||
android:textSize="10sp"
|
android:textSize="10sp"
|
||||||
android:textColor="#1B1F1A" />
|
android:textColor="#1B1F1A" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
|
<LinearLayout
|
||||||
android:id="@+id/btn_add_third"
|
android:id="@+id/btn_add_third"
|
||||||
android:layout_width="60dp"
|
|
||||||
android:layout_height="28dp"
|
|
||||||
android:layout_marginTop="50dp"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="+"
|
android:layout_width="60dp"
|
||||||
android:textSize="20dp"
|
android:layout_marginTop="50dp"
|
||||||
android:textStyle="bold"
|
android:layout_height="28dp"
|
||||||
android:textColor="#FFD9C4"
|
android:background="@drawable/round_bg_three">
|
||||||
android:background="@drawable/round_bg_three" />
|
<ImageView
|
||||||
|
android:id="@+id/add_third_icon"
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="15dp"
|
||||||
|
android:src="@drawable/third_add"/>
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
83
app/src/main/res/layout/dialog_confirm_delete_character.xml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="18dp"
|
||||||
|
android:background="@drawable/dialog_background">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Delete character design?"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:text="Are you sure you want to delete this character profile?"
|
||||||
|
android:textColor="#6B7280"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_emoji"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="🙂"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Prof."
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_cancel"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Cancel"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:background="@drawable/my_keyboard_cancel" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_confirm"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Delete"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:background="@drawable/my_keyboard_delete" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -69,14 +69,17 @@
|
|||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/et_feedback"
|
android:id="@+id/et_feedback"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="259dp"
|
android:layout_height="200dp"
|
||||||
android:gravity="top|start"
|
android:gravity="top|start"
|
||||||
android:hint="Please enter your feedback..."
|
android:hint="Please enter your feedback..."
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine"
|
||||||
android:overScrollMode="never"
|
android:minLines="4"
|
||||||
android:maxLines="6"
|
android:maxLines="10"
|
||||||
android:minLines="4" />
|
android:scrollbars="vertical"
|
||||||
|
android:isScrollContainer="true"
|
||||||
|
android:nestedScrollingEnabled="true" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_circle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Circle 页面"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
</FrameLayout>
|
|
||||||
28
app/src/main/res/layout/fragment_consumption_record.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/rootCoordinator"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/white"
|
||||||
|
tools:context=".ui.mine.consumptionRecord.ConsumptionRecordFragment">
|
||||||
|
|
||||||
|
<!-- 整页下拉刷新:包住唯一可滚动容器 RecyclerView -->
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/login_bg">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvTransactions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="16dp" />
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -8,13 +8,13 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.home.HomeFragment">
|
tools:context=".ui.home.HomeFragment">
|
||||||
|
|
||||||
<!-- 背景-->
|
<!-- 背景-->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:src="@drawable/bg"
|
android:src="@drawable/bg"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.core.widget.NestedScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -154,6 +154,48 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<!-- 消费记录 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/click_record"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:background="@drawable/settings"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:src="@drawable/record" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:text="Consumption Record"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="9dp"
|
||||||
|
android:layout_height="13dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:src="@drawable/more_icons" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 注意 -->
|
<!-- 注意 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/click_Notice"
|
android:id="@+id/click_Notice"
|
||||||
@@ -198,6 +240,7 @@
|
|||||||
|
|
||||||
<!-- 分享应用 -->
|
<!-- 分享应用 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/click_Share"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="20dp"
|
||||||
|
|||||||
30
app/src/main/res/layout/item_keyboard_character.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/item_root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="#FFFFFF"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingEnd="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_emoji"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="😄"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Prof."
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</LinearLayout>
|
||||||
22
app/src/main/res/layout/item_loading_footer.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingTop="14dp"
|
||||||
|
android:paddingBottom="14dp">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:text="Loading..."
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#666666" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -70,20 +70,23 @@
|
|||||||
android:textColor="#02BEAC"
|
android:textColor="#02BEAC"
|
||||||
android:textSize="10sp" />
|
android:textSize="10sp" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/operation"
|
android:id="@+id/operation"
|
||||||
|
android:gravity="center"
|
||||||
android:layout_width="100dp"
|
android:layout_width="100dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:text="+"
|
android:background="@drawable/list_two_bg">
|
||||||
android:gravity="center"
|
<ImageView
|
||||||
android:layout_marginTop="5dp"
|
android:id="@+id/operation_add_icon"
|
||||||
android:background="@drawable/list_two_bg"
|
android:layout_width="15dp"
|
||||||
android:textColor="#FFFFFF"
|
android:layout_height="15dp"
|
||||||
android:textStyle="bold"
|
android:src="@drawable/operation_add"/>
|
||||||
android:textSize="20sp" />
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,16 +57,17 @@
|
|||||||
android:textColor="#9A9A9A" />
|
android:textColor="#9A9A9A" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/btn_add"
|
android:id="@+id/btn_add"
|
||||||
|
android:gravity="center"
|
||||||
android:layout_width="56dp"
|
android:layout_width="56dp"
|
||||||
android:layout_height="38dp"
|
android:layout_height="38dp"
|
||||||
android:gravity="center"
|
android:background="@drawable/round_bg_others">
|
||||||
android:layout_weight="1"
|
<ImageView
|
||||||
android:text="+"
|
android:id="@+id/add_icon"
|
||||||
android:textSize="14sp"
|
android:layout_width="15dp"
|
||||||
android:textStyle="bold"
|
android:layout_height="15dp"
|
||||||
android:textColor="#02BEAC"
|
android:src="@drawable/round_bg_others_add"/>
|
||||||
android:background="@drawable/round_bg_others" />
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<!-- 内容卡片结束 -->
|
<!-- 内容卡片结束 -->
|
||||||
47
app/src/main/res/layout/item_transaction_record.xml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:padding="20dp"
|
||||||
|
android:background="@drawable/consumption_details_bg"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDesc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Date"
|
||||||
|
android:textColor="#020202"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTime"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="0000-00-00 00:00:00"
|
||||||
|
android:textColor="#9E9E9E"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAmount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="00.00"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:id="@+id/background"
|
android:id="@+id/background"
|
||||||
@@ -60,14 +61,22 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- 补全建议区域(可横向滑动) -->
|
<!-- 补全建议区域(可横向滑动) -->
|
||||||
<HorizontalScrollView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/completion_scroll"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_marginTop="3dp"
|
android:layout_marginTop="3dp"
|
||||||
|
android:background="@drawable/complete_bg">
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
android:scrollbars="none"
|
android:scrollbars="none"
|
||||||
android:overScrollMode="never"
|
android:overScrollMode="never"
|
||||||
android:background="@drawable/complete_bg"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:id="@+id/completion_scroll">
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:id="@+id/completion_HorizontalScrollView">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/completion_suggestions"
|
android:id="@+id/completion_suggestions"
|
||||||
@@ -307,10 +316,24 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:background="@drawable/btn_keyboard"
|
android:background="@drawable/btn_keyboard"
|
||||||
android:textColor="#FFFFFF"/>
|
android:textColor="#FFFFFF"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</HorizontalScrollView>
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/associate_close"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@drawable/complete_close_bg"
|
||||||
|
android:gravity="center"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:src="@drawable/associate_close"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
<!-- 第一行字母键 -->
|
<!-- 第一行字母键 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
136
app/src/main/res/layout/layout_consumption_record_header.xml
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- 标题和返回 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/iv_close"
|
||||||
|
android:layout_width="46dp"
|
||||||
|
android:layout_height="46dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="13dp"
|
||||||
|
android:layout_height="13dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/more_icons"
|
||||||
|
android:rotation="180"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="49dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="Consumption record"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 金币显示卡片 -->
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="126dp"
|
||||||
|
android:elevation="10dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="@drawable/gold_coin_bg">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="My points"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:padding="20dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="38dp"
|
||||||
|
android:layout_height="38dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:src="@drawable/gold_coin" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/balance"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:text="0.00"
|
||||||
|
android:textColor="#02BEAC"
|
||||||
|
android:textSize="40sp" />
|
||||||
|
|
||||||
|
<!-- 充值按钮 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rechargeButton"
|
||||||
|
android:layout_width="114dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@drawable/gold_coin_button"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:text="Recharge" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- 列表标题 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:src="@drawable/gold_coin" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Consumption Details"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#1B1F1A"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -56,7 +56,6 @@
|
|||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 内容-->
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -64,62 +63,30 @@
|
|||||||
android:padding="15dp"
|
android:padding="15dp"
|
||||||
android:layout_marginTop="90dp"
|
android:layout_marginTop="90dp"
|
||||||
android:background="@drawable/mykeyboard_bg"
|
android:background="@drawable/mykeyboard_bg"
|
||||||
android:orientation="horizontal">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<!-- 卡片 -->
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
<LinearLayout
|
android:id="@+id/rv_keyboard"
|
||||||
android:layout_width="115dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="42dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:overScrollMode="never"
|
||||||
android:orientation="horizontal">
|
tools:listitem="@layout/item_keyboard_character" />
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="109dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:background="#FFFFFF">
|
|
||||||
<de.hdodenhof.circleimageview.CircleImageView
|
|
||||||
android:id="@+id/avatar"
|
|
||||||
android:layout_width="20dp"
|
|
||||||
android:layout_height="20dp"
|
|
||||||
android:src="@drawable/default_avatar"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginStart="5dp"
|
|
||||||
android:text="Humor"
|
|
||||||
android:textColor="#1B1F1A"
|
|
||||||
android:textSize="13sp"/>
|
|
||||||
</LinearLayout>
|
|
||||||
<TextView
|
|
||||||
android:layout_width="11dp"
|
|
||||||
android:layout_height="11dp"
|
|
||||||
android:layout_marginStart="-5dp"
|
|
||||||
android:layout_marginTop="-15dp"
|
|
||||||
android:text="-"
|
|
||||||
android:textSize="5sp"
|
|
||||||
android:textColor="#ffffff"
|
|
||||||
android:background="@drawable/my_keyboard_delete"
|
|
||||||
android:gravity="center"/>
|
|
||||||
</LinearLayout>
|
|
||||||
<!-- `````````````````` -->
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<!-- 按钮 -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/btn_keyboard"
|
|
||||||
android:layout_width="343dp"
|
|
||||||
android:layout_height="45dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="Save"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:background="@drawable/my_keyboard_delete"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"/>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_keyboard"
|
||||||
|
android:layout_width="343dp"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_gravity="bottom|center_horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Save"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:background="@drawable/my_keyboard_delete"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"/>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -96,6 +96,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
<!-- Nickname -->
|
<!-- Nickname -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/row_nickname"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
@@ -117,6 +118,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tv_nickname_value"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Nickname"
|
android:text="Nickname"
|
||||||
@@ -138,6 +140,7 @@
|
|||||||
|
|
||||||
<!-- Gender -->
|
<!-- Gender -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/row_gender"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
@@ -159,6 +162,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tv_gender_value"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Gender"
|
android:text="Gender"
|
||||||
@@ -180,6 +184,7 @@
|
|||||||
|
|
||||||
<!-- User ID -->
|
<!-- User ID -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/row_userid"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
@@ -201,6 +206,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tv_userid_value"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="88888888"
|
android:text="88888888"
|
||||||
|
|||||||
61
app/src/main/res/layout/sheet_edit_nickname.xml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="#F8F8F8"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Change the nickname"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#1B1F1A"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_close"
|
||||||
|
android:layout_width="26dp"
|
||||||
|
android:layout_height="26dp"
|
||||||
|
android:src="@drawable/pop_collapse"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Input -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:boxBackgroundMode="none">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_nickname"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="#FFFFFF"/>
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="43dp"/>
|
||||||
|
<!-- Save -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_save"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Save"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:background="@drawable/keyboard_ettings"/>
|
||||||
|
</LinearLayout>
|
||||||
76
app/src/main/res/layout/sheet_select_gender.xml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Modify gender"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#1B1F1A"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_close"
|
||||||
|
android:layout_width="26dp"
|
||||||
|
android:layout_height="26dp"
|
||||||
|
android:src="@drawable/pop_collapse"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="180dp"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/gender_wheel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:nestedScrollingEnabled="true"
|
||||||
|
android:clipToPadding="false"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/line_top"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0.5dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:translationY="-24dp"
|
||||||
|
android:background="#D8D8D8"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/line_bottom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0.5dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:translationY="24dp"
|
||||||
|
android:background="#D8D8D8"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"/>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- Save -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_save"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:layout_marginTop="43dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Save"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:background="@drawable/keyboard_ettings"/>
|
||||||
|
</LinearLayout>
|
||||||
@@ -11,13 +11,6 @@
|
|||||||
android:name="com.example.myapplication.ui.EmptyFragment"
|
android:name="com.example.myapplication.ui.EmptyFragment"
|
||||||
android:label="empty" />
|
android:label="empty" />
|
||||||
|
|
||||||
<!-- 圈子 -->
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/circleFragment"
|
|
||||||
android:name="com.example.myapplication.ui.circle.CircleFragment"
|
|
||||||
android:label="Circle"
|
|
||||||
tools:layout="@layout/fragment_circle" />
|
|
||||||
|
|
||||||
<!-- 充值 -->
|
<!-- 充值 -->
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/rechargeFragment"
|
android:id="@+id/rechargeFragment"
|
||||||
@@ -86,6 +79,50 @@
|
|||||||
android:label="Forget Password Reset"
|
android:label="Forget Password Reset"
|
||||||
tools:layout="@layout/fragment_forget_password_reset" />
|
tools:layout="@layout/fragment_forget_password_reset" />
|
||||||
|
|
||||||
|
<!-- 消费记录 -->
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/consumptionRecordFragment"
|
||||||
|
android:name="com.example.myapplication.ui.mine.consumptionRecord.ConsumptionRecordFragment"
|
||||||
|
android:label="Consumption Record"
|
||||||
|
tools:layout="@layout/fragment_consumption_record" />
|
||||||
|
|
||||||
|
<!-- 个人设置 -->
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/PersonalSettings"
|
||||||
|
android:name="com.example.myapplication.ui.mine.myotherpages.PersonalSettings"
|
||||||
|
android:label="Personal Settings"
|
||||||
|
tools:layout="@layout/personal_settings" />
|
||||||
|
|
||||||
|
<!-- 键盘设置 -->
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/MyKeyboard"
|
||||||
|
android:name="com.example.myapplication.ui.keyboard.MyKeyboard"
|
||||||
|
android:label="My Keyboard"
|
||||||
|
tools:layout="@layout/my_keyboard" />
|
||||||
|
|
||||||
|
<!-- 反馈页面 -->
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/feedbackFragment"
|
||||||
|
android:name="com.example.myapplication.ui.mine.myotherpages.FeedbackFragment"
|
||||||
|
android:label="Feedback"
|
||||||
|
tools:layout="@layout/feedback_fragment" />
|
||||||
|
|
||||||
|
<!-- 通知页面 -->
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/notificationFragment"
|
||||||
|
android:name="com.example.myapplication.ui.mine.myotherpages.NotificationFragment"
|
||||||
|
android:label="Notification"
|
||||||
|
tools:layout="@layout/notification_fragment" />
|
||||||
|
|
||||||
|
<!-- 消费记录跳转 -->
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_global_consumptionRecordFragment"
|
||||||
|
app:destination="@id/consumptionRecordFragment"
|
||||||
|
app:enterAnim="@anim/fade_in"
|
||||||
|
app:exitAnim="@anim/fade_out"
|
||||||
|
app:popEnterAnim="@anim/fade_in_fast"
|
||||||
|
app:popExitAnim="@anim/fade_out_fast" />
|
||||||
|
|
||||||
<!-- 充值跳转 -->
|
<!-- 充值跳转 -->
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_rechargeFragment"
|
android:id="@+id/action_global_rechargeFragment"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
app:popExitAnim="@anim/fade_out_fast" />
|
app:popExitAnim="@anim/fade_out_fast" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
<!-- 个人设置页面 -->
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/PersonalSettings"
|
android:id="@+id/PersonalSettings"
|
||||||
android:name="com.example.myapplication.ui.mine.myotherpages.PersonalSettings"
|
android:name="com.example.myapplication.ui.mine.myotherpages.PersonalSettings"
|
||||||
|
|||||||
@@ -17,4 +17,11 @@
|
|||||||
<string name="key_comma">,</string>
|
<string name="key_comma">,</string>
|
||||||
<string name="key_period">.</string>
|
<string name="key_period">.</string>
|
||||||
<string name="key_enter">Enter</string>
|
<string name="key_enter">Enter</string>
|
||||||
|
<string name="choose_from_gallery">从相册选择</string>
|
||||||
|
<string name="take_photo">拍照</string>
|
||||||
|
<string name="change_avatar">更换头像</string>
|
||||||
|
<string name="cancel">取消</string>
|
||||||
|
<string name="avatar_updated">头像更新成功</string>
|
||||||
|
<string name="upload_failed">上传失败</string>
|
||||||
|
<string name="upload_error">上传出错</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
6
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-files-path
|
||||||
|
name="my_images"
|
||||||
|
path="Pictures" />
|
||||||
|
</paths>
|
||||||