feat: 增加接口签名验证
- 增加接口签名验证 - 优化登陆界面以适应密码保险箱 - 修复表情面板高度错误
This commit is contained in:
parent
632cd5c4e6
commit
da1fc07599
@ -20,7 +20,7 @@ android {
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.0.01"
|
||||
versionName = "0.0.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@ -91,6 +91,9 @@ dependencies {
|
||||
|
||||
// 自定义spannable
|
||||
implementation(libs.spannable)
|
||||
|
||||
implementation(libs.soft.input.event)
|
||||
|
||||
implementation(libs.scanplus)
|
||||
// implementation(libs.therouter)
|
||||
// ksp(libs.therouter.ksp)
|
||||
|
@ -14,7 +14,7 @@ import io.objectbox.Box
|
||||
|
||||
class ContactRepository {
|
||||
|
||||
private val contactApiService = RetrofitClient.contactApiService
|
||||
private val friendApiService = RetrofitClient.friendApiService
|
||||
|
||||
private val contactBox: Box<Contact> by lazy {
|
||||
getBoxStore().boxFor(Contact::class.java)
|
||||
@ -23,7 +23,7 @@ class ContactRepository {
|
||||
// 获取联系人请求列表
|
||||
suspend fun getContactRequestList(username: String): Result<List<FriendRequestItem>?> {
|
||||
return apiCall(
|
||||
apiCall = { contactApiService.getContactRequestList(username) },
|
||||
apiCall = { friendApiService.getContactRequestList(username) },
|
||||
errorMessage = "获取好友申请列表失败"
|
||||
)
|
||||
}
|
||||
@ -35,7 +35,7 @@ class ContactRepository {
|
||||
remark: String
|
||||
): Result<Contact?> {
|
||||
return apiCall(
|
||||
apiCall = { contactApiService.acceptContactRequest(contactId, username, remark) },
|
||||
apiCall = { friendApiService.acceptContactRequest(contactId, username, remark) },
|
||||
errorMessage = "添加好友失败"
|
||||
).onSuccess {
|
||||
it?.let {
|
||||
@ -50,7 +50,7 @@ class ContactRepository {
|
||||
contactId: String,
|
||||
): Result<String?> {
|
||||
return apiCall(
|
||||
apiCall = { contactApiService.deleteContact(username, contactId) },
|
||||
apiCall = { friendApiService.deleteContact(username, contactId) },
|
||||
errorMessage = "删除好友失败"
|
||||
).onSuccess {
|
||||
val con =
|
||||
@ -62,7 +62,7 @@ class ContactRepository {
|
||||
// 添加联系人
|
||||
suspend fun addContact(contactId: String, message: String): Result<String?> {
|
||||
return apiCall(
|
||||
apiCall = { contactApiService.addContact(getUsername(), contactId, message) },
|
||||
apiCall = { friendApiService.addContact(getUsername(), contactId, message) },
|
||||
errorMessage = "添加联系人失败"
|
||||
)
|
||||
}
|
||||
@ -70,21 +70,16 @@ class ContactRepository {
|
||||
// 搜索联系人
|
||||
suspend fun searchContact(username: String): Result<SearchUser?> {
|
||||
return apiCall(
|
||||
apiCall = { contactApiService.searchContact(username) },
|
||||
apiCall = { friendApiService.searchContact(username) },
|
||||
errorMessage = "搜索用户失败"
|
||||
)
|
||||
}
|
||||
|
||||
// 获取联系人列表
|
||||
suspend fun getContactList(username: String): Result<List<Contact>?> {
|
||||
// return safeApiCall(
|
||||
// apiCall = { contactApiService.getContactList(username) },
|
||||
// errorMessage = "获取好友列表失败"
|
||||
// ).onSuccess {
|
||||
//
|
||||
// }
|
||||
return try {
|
||||
val response = contactApiService.getContactList(username)
|
||||
val maps = mapOf("username" to username)
|
||||
val response = friendApiService.getContactList(maps)
|
||||
if (response.isSuccess()) {
|
||||
val searchUsers = response.getResponseData()
|
||||
searchUsers?.let {
|
||||
@ -113,7 +108,7 @@ class ContactRepository {
|
||||
|
||||
suspend fun setRemark(userId: String, contactId: String, remark: String): Result<String?> {
|
||||
return apiCall(
|
||||
apiCall = { contactApiService.setRemark(userId, contactId, remark) },
|
||||
apiCall = { friendApiService.setRemark(userId, contactId, remark) },
|
||||
errorMessage = "设置备注失败"
|
||||
).onSuccess {
|
||||
val con = contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
|
||||
|
@ -23,7 +23,10 @@ class UserAuthRepository {
|
||||
|
||||
private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
|
||||
|
||||
// 用户注册
|
||||
/**
|
||||
* 注册方法
|
||||
* @param registerRequest 注册请求参数
|
||||
*/
|
||||
suspend fun register(registerRequest: RegisterRequest): Result<Register?> {
|
||||
return apiCall(
|
||||
apiCall = { authApiService.register(registerRequest) },
|
||||
@ -35,10 +38,15 @@ class UserAuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// 登录方法
|
||||
/**
|
||||
* 登录方法
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
*/
|
||||
suspend fun loginByUsername(username: String, password: String): Result<UserInfo?> {
|
||||
val requestParams = mapOf("username" to username, "password" to password)
|
||||
return apiCall(
|
||||
apiCall = { authApiService.loginByUsername(username, password) },
|
||||
apiCall = { authApiService.loginByUsername(requestParams) },
|
||||
errorMessage = "登录成功,但未返回用户数据"
|
||||
).onSuccess { userInfo ->
|
||||
userInfo?.let {
|
||||
@ -47,10 +55,15 @@ class UserAuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 登录方法
|
||||
* @param telephone 手机号
|
||||
* @param password 密码
|
||||
*/
|
||||
suspend fun loginByTelephone(telephone: String, password: String): Result<UserInfo?> {
|
||||
val requestParams = mapOf("telephone" to telephone, "password" to password)
|
||||
return apiCall(
|
||||
apiCall = { authApiService.loginByTelephone(telephone, password) },
|
||||
apiCall = { authApiService.loginByTelephone(requestParams) },
|
||||
errorMessage = "登录成功,但未返回用户数据"
|
||||
).onSuccess { userInfo ->
|
||||
userInfo?.let {
|
||||
@ -58,7 +71,7 @@ class UserAuthRepository {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun insertUserInfo(
|
||||
register: Register,
|
||||
telephone: String
|
||||
|
@ -23,34 +23,21 @@ class UserProfileRepository {
|
||||
private val userApiService = RetrofitClient.userApiService
|
||||
|
||||
// 获取用户信息
|
||||
suspend fun getUserInfo(username: String): Result<SearchUser> {
|
||||
return try {
|
||||
val response = userApiService.getUserInfo(username)
|
||||
if (response.isSuccess()) {
|
||||
val searchUser = response.getResponseData()
|
||||
searchUser?.let {
|
||||
Result.success(searchUser)
|
||||
} ?: Result.failure(Exception("用户不存在"))
|
||||
} else {
|
||||
Result.failure(Exception(response.getResponseMsg()))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
suspend fun getUserInfo(username: String): Result<SearchUser?> {
|
||||
val requestParams = mapOf("username" to username)
|
||||
return apiCall(
|
||||
apiCall = { userApiService.getUserInfo(requestParams) },
|
||||
errorMessage = "获取用户信息失败"
|
||||
)
|
||||
}
|
||||
|
||||
// 修改昵称
|
||||
suspend fun changeNickname(userRequest: UserRequest): Result<Boolean> {
|
||||
return try {
|
||||
val response = userApiService.changeNickname(userRequest)
|
||||
if (response.isSuccess()) {
|
||||
updateNickname(userRequest.nickname!!)
|
||||
Result.success(true)
|
||||
} else {
|
||||
Result.failure(Exception(response.getResponseMsg()))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
suspend fun changeNickname(userRequest: UserRequest): Result<Boolean?> {
|
||||
return apiCall(
|
||||
apiCall = { userApiService.changeNickname(userRequest) },
|
||||
errorMessage = "修改昵称失败"
|
||||
).onSuccess {
|
||||
updateNickname(userRequest.nickname!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,9 @@ class UserSearchRepository {
|
||||
|
||||
// 获取用户列表
|
||||
suspend fun getUserList(username: String): Result<List<User>> {
|
||||
val requestParams = mapOf("username" to username)
|
||||
return try {
|
||||
val response = userApiService.getUserListByNickname(username)
|
||||
val response = userApiService.fetchUserList(requestParams)
|
||||
if (response.isSuccess()) {
|
||||
val userList = response.getResponseData()
|
||||
userList?.let {
|
||||
|
@ -11,7 +11,7 @@ object NetworkInterface {
|
||||
// private const val URL = "49.233.105.103:6000"
|
||||
// const val SERVER_URL = "https://$URL"
|
||||
const val SERVER_URL = "http://$URL"
|
||||
const val WEBSOCKET_SERVER_URL = "wss://$URL"
|
||||
const val WEBSOCKET_SERVER_URL = "ws://$URL"
|
||||
const val WEBSOCKET = "/websocket/single/"
|
||||
const val USER_INFO = "/users/info/"
|
||||
const val USER_LOGIN_BY_USERNAME = "/users/login/username"
|
||||
|
@ -1,9 +1,10 @@
|
||||
package com.kaixed.kchat.network
|
||||
|
||||
import com.kaixed.kchat.network.interceptor.SignInterceptor
|
||||
import com.kaixed.kchat.network.interceptor.TokenRefreshInterceptor
|
||||
import com.kaixed.kchat.network.service.AuthApiService
|
||||
import com.kaixed.kchat.network.service.ContactService
|
||||
import com.kaixed.kchat.network.service.FileApiService
|
||||
import com.kaixed.kchat.network.service.FriendApiService
|
||||
import com.kaixed.kchat.network.service.UserApiService
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@ -48,6 +49,7 @@ object RetrofitClient {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.pingInterval(15, TimeUnit.SECONDS)
|
||||
.addInterceptor(SignInterceptor()) // 签名拦截器
|
||||
.addInterceptor(TokenRefreshInterceptor()) // Token 拦截器
|
||||
.build()
|
||||
|
||||
@ -65,8 +67,8 @@ object RetrofitClient {
|
||||
retrofit.create(UserApiService::class.java)
|
||||
}
|
||||
|
||||
val contactApiService: ContactService by lazy {
|
||||
retrofit.create(ContactService::class.java)
|
||||
val friendApiService: FriendApiService by lazy {
|
||||
retrofit.create(FriendApiService::class.java)
|
||||
}
|
||||
|
||||
val fileApiService: FileApiService by lazy {
|
||||
|
@ -1,37 +1,99 @@
|
||||
package com.kaixed.kchat.network.interceptor
|
||||
|
||||
import com.kaixed.kchat.network.SignUtil.generateSign
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
class SignInterceptor : Interceptor {
|
||||
|
||||
private val secretKey = "YourSecretKey" // 后端秘钥
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val urlPath = originalRequest.url.encodedPath
|
||||
|
||||
val skipSignPaths = listOf(
|
||||
"/users/avatar",
|
||||
)
|
||||
|
||||
// 检查是否需要跳过签名
|
||||
if (skipSignPaths.any { urlPath.endsWith(it) }) {
|
||||
// 直接放行请求,不做签名处理
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
val request = chain.request()
|
||||
|
||||
// 获取当前时间戳
|
||||
val timestamp = System.currentTimeMillis().toString()
|
||||
|
||||
// 生成请求的签名
|
||||
val sign = generateSign(originalRequest, timestamp)
|
||||
// 生成随机 nonce
|
||||
val nonce = UUID.randomUUID().toString()
|
||||
|
||||
// 创建新的请求,加入 sign 和 timestamp
|
||||
val newRequest = originalRequest.newBuilder()
|
||||
// 获取请求参数
|
||||
val params = getRequestParams(request)
|
||||
|
||||
// 生成签名
|
||||
val sign = generateServerSign(params, timestamp, nonce, secretKey)
|
||||
|
||||
// 构造新的请求,添加请求头
|
||||
val newRequest = request.newBuilder()
|
||||
.addHeader("sign", sign)
|
||||
.addHeader("timestamp", timestamp)
|
||||
.addHeader("nonce", nonce)
|
||||
.build()
|
||||
|
||||
// 继续请求
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
// 获取请求的参数,处理 POST 请求的 Map 格式数据,并确保按字典顺序排序
|
||||
private fun getRequestParams(request: Request): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
|
||||
// 处理 GET 请求
|
||||
if (request.method == "GET") {
|
||||
val url = request.url
|
||||
for (i in 0 until url.querySize) {
|
||||
params[url.queryParameterName(i)] = url.queryParameterValue(i) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 POST 请求(Map 格式)
|
||||
else if (request.method == "POST") {
|
||||
val body = request.body
|
||||
if (body is FormBody) {
|
||||
// 获取所有参数,并确保按字典顺序排序
|
||||
for (i in 0 until body.size) {
|
||||
params[body.name(i)] = body.value(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按字典顺序排序参数
|
||||
return params.toSortedMap()
|
||||
}
|
||||
|
||||
// 生成服务端签名
|
||||
private fun generateServerSign(params: Map<String, String>, timestamp: String, nonce: String, secretKey: String): String {
|
||||
val sortedParams = params.toSortedMap() // 确保是按字典顺序排序
|
||||
|
||||
// 拼接参数
|
||||
val sb = StringBuilder()
|
||||
for ((key, value) in sortedParams) {
|
||||
sb.append("$key=$value&")
|
||||
}
|
||||
sb.append("timestamp=$timestamp&")
|
||||
sb.append("nonce=$nonce&")
|
||||
sb.append("secretKey=$secretKey")
|
||||
|
||||
// 使用 SHA-256 进行加密
|
||||
return hashWithSHA256(sb.toString())
|
||||
}
|
||||
|
||||
|
||||
// 使用 SHA-256 进行哈希加密
|
||||
private fun hashWithSHA256(input: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hashBytes = digest.digest(input.toByteArray(Charsets.UTF_8))
|
||||
val hexString = StringBuilder()
|
||||
for (b in hashBytes) {
|
||||
val hex = Integer.toHexString(0xff and b.toInt())
|
||||
if (hex.length == 1) hexString.append('0')
|
||||
hexString.append(hex)
|
||||
}
|
||||
return hexString.toString()
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,6 @@ import com.kaixed.kchat.data.model.request.RegisterRequest
|
||||
import com.kaixed.kchat.data.model.response.register.Register
|
||||
import com.kaixed.kchat.network.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
@ -17,36 +15,32 @@ import retrofit2.http.Query
|
||||
*/
|
||||
interface AuthApiService {
|
||||
|
||||
@POST("users/access-token")
|
||||
@POST("auth/access-token")
|
||||
suspend fun auth(
|
||||
@Header("Authorization") token: String,
|
||||
@Query("username") username: String
|
||||
): ApiResponse<String>
|
||||
|
||||
@POST("users/refresh-token")
|
||||
@POST("auth/refresh-token")
|
||||
suspend fun refresh(
|
||||
@Header("Authorization") token: String,
|
||||
@Query("username") username: String
|
||||
): ApiResponse<String>
|
||||
|
||||
// 登录接口(根据用户名登录)
|
||||
@FormUrlEncoded
|
||||
@POST("users/login/username")
|
||||
@POST("auth/login/username")
|
||||
suspend fun loginByUsername(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String
|
||||
@Body requestParams: Map<String, String>,
|
||||
): ApiResponse<UserInfo?>
|
||||
|
||||
// 登录接口(根据电话登录)
|
||||
@FormUrlEncoded
|
||||
@POST("users/login/telephone")
|
||||
@POST("auth/login/telephone")
|
||||
suspend fun loginByTelephone(
|
||||
@Field("telephone") telephone: String,
|
||||
@Field("password") password: String
|
||||
@Body requestParams: Map<String, String>,
|
||||
): ApiResponse<UserInfo>
|
||||
|
||||
// 注册接口
|
||||
@POST("users/register")
|
||||
@POST("auth/register")
|
||||
suspend fun register(
|
||||
@Body registerRequest: RegisterRequest
|
||||
): ApiResponse<Register>
|
||||
|
@ -4,6 +4,7 @@ import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.model.friend.FriendRequestItem
|
||||
import com.kaixed.kchat.data.model.search.SearchUser
|
||||
import com.kaixed.kchat.network.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
@ -12,10 +13,9 @@ import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/11/15 11:00
|
||||
* @Date: 2025/1/25 16:38
|
||||
*/
|
||||
interface ContactService {
|
||||
|
||||
interface FriendApiService {
|
||||
// 获取联系人请求列表
|
||||
@FormUrlEncoded
|
||||
@POST("friend/request/list")
|
||||
@ -48,15 +48,13 @@ interface ContactService {
|
||||
): ApiResponse<SearchUser?>
|
||||
|
||||
// 获取联系人列表
|
||||
@FormUrlEncoded
|
||||
@POST("friend/list")
|
||||
@POST("friends/list")
|
||||
suspend fun getContactList(
|
||||
@Field("userId") username: String
|
||||
@Body requestParams: Map<String, String>,
|
||||
): ApiResponse<List<Contact>?>
|
||||
|
||||
// 删除联系人
|
||||
@FormUrlEncoded
|
||||
@POST("friend/delete")
|
||||
@POST("friends/delete")
|
||||
suspend fun deleteContact(
|
||||
@Field("userId") username: String,
|
||||
@Field("contactId") contactId: String,
|
||||
@ -70,4 +68,4 @@ interface ContactService {
|
||||
@Field("contactId") contactId: String,
|
||||
@Field("remark") remark: String,
|
||||
): ApiResponse<String?>
|
||||
}
|
||||
}
|
@ -1,22 +1,15 @@
|
||||
package com.kaixed.kchat.network.service
|
||||
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.data.model.request.RegisterRequest
|
||||
import com.kaixed.kchat.data.model.request.UpdatePasswordRequest
|
||||
import com.kaixed.kchat.data.model.request.UserRequest
|
||||
import com.kaixed.kchat.data.model.response.register.Register
|
||||
import com.kaixed.kchat.data.model.response.search.User
|
||||
import com.kaixed.kchat.data.model.search.SearchUser
|
||||
import com.kaixed.kchat.network.ApiResponse
|
||||
import okhttp3.MultipartBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Part
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
@ -25,22 +18,22 @@ import retrofit2.http.Path
|
||||
interface UserApiService {
|
||||
|
||||
// 获取用户列表
|
||||
@GET("userList/{username}")
|
||||
suspend fun getUserListByNickname(
|
||||
@Path("username") username: String
|
||||
@POST("friends/list")
|
||||
suspend fun fetchUserList(
|
||||
@Body requestParams: Map<String, String>
|
||||
): ApiResponse<List<User>>
|
||||
|
||||
// 获取用户信息
|
||||
@GET("users/{username}")
|
||||
@POST("users/info/fetch")
|
||||
suspend fun getUserInfo(
|
||||
@Path("username") username: String
|
||||
@Body requestParams: Map<String, String>
|
||||
): ApiResponse<SearchUser>
|
||||
|
||||
// 更改昵称接口
|
||||
@POST("users/info")
|
||||
suspend fun changeNickname(
|
||||
@Body userRequest: UserRequest
|
||||
): ApiResponse<String>
|
||||
): ApiResponse<Boolean?>
|
||||
|
||||
// 上传头像接口
|
||||
@Multipart
|
||||
@ -50,7 +43,7 @@ interface UserApiService {
|
||||
@Part file: MultipartBody.Part
|
||||
): ApiResponse<String>
|
||||
|
||||
// 更改昵称接口
|
||||
// 更改密码
|
||||
@POST("users/password")
|
||||
suspend fun updatePassword(
|
||||
@Body updatePasswordRequest: UpdatePasswordRequest
|
||||
|
@ -155,6 +155,7 @@ class WebSocketService : Service() {
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
Log.d(TAG, "WebSocket closing: ${t.cause}")
|
||||
Log.d(TAG, "WebSocket closing: $t")
|
||||
establishConnection()
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@ -27,6 +26,9 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drake.softinput.getSoftInputHeight
|
||||
import com.drake.softinput.hasSoftInput
|
||||
import com.drake.softinput.setWindowSoftInput
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.event.UnreadEvent
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
@ -46,12 +48,13 @@ import com.kaixed.kchat.ui.i.IOnItemClickListener
|
||||
import com.kaixed.kchat.ui.i.OnItemClickListener
|
||||
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
|
||||
import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID
|
||||
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
|
||||
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getKeyboardHeight
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.utils.ImageEngines
|
||||
import com.kaixed.kchat.utils.ImageSpanUtil.insertEmoji
|
||||
import com.kaixed.kchat.utils.ScreenUtils
|
||||
import com.kaixed.kchat.viewmodel.FileViewModel
|
||||
import com.luck.picture.lib.basic.PictureSelector
|
||||
import com.luck.picture.lib.config.SelectMimeType
|
||||
@ -66,6 +69,7 @@ import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
IOnItemClickListener {
|
||||
@ -90,8 +94,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
|
||||
private var softKeyboardHeight = 0
|
||||
|
||||
private var keyboardShown = false
|
||||
|
||||
private var bound = false
|
||||
|
||||
private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
|
||||
@ -120,7 +122,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
setListener()
|
||||
bindWebSocketService()
|
||||
setPanelChange()
|
||||
getKeyBoardVisibility()
|
||||
observeStateFlow()
|
||||
if (isSearchHistory) {
|
||||
val size = MessagesManager.queryHistory(msgLocalId)
|
||||
@ -158,18 +159,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKeyBoardVisibility() {
|
||||
val rootView = binding.root
|
||||
|
||||
rootView.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val r = Rect()
|
||||
rootView.getWindowVisibleDisplayFrame(r)
|
||||
val screenHeight = rootView.rootView.height
|
||||
val keypadHeight = screenHeight - r.bottom
|
||||
keyboardShown = keypadHeight > screenHeight * KEYBOARD_HEIGHT_RATIO
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBackPressListener() {
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
@ -220,7 +209,7 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
}
|
||||
|
||||
private fun switchPanel(isSwitchToEmoji: Boolean) {
|
||||
if (keyboardShown) {
|
||||
if (hasSoftInput()) {
|
||||
lockContentViewHeight()
|
||||
handlePanelSwitch(isSwitchToEmoji)
|
||||
unlockContentViewHeight()
|
||||
@ -278,6 +267,12 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
}
|
||||
|
||||
private fun setListener() {
|
||||
binding.etInput.setOnClickListener {
|
||||
if (hasSoftInput()){
|
||||
MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 300))
|
||||
}
|
||||
}
|
||||
|
||||
setBackPressListener()
|
||||
|
||||
binding.gvFunctionPanel.selector = ColorDrawable(Color.TRANSPARENT)
|
||||
|
@ -2,188 +2,215 @@ package com.kaixed.kchat.ui.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.drake.softinput.getSoftInputHeight
|
||||
import com.drake.softinput.hasSoftInput
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.LocalDatabase
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.databinding.ActivityLoginBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.ui.widget.MyBottomSheetFragment
|
||||
import com.kaixed.kchat.utils.Constants.ACCESS_TOKEN
|
||||
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT
|
||||
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
|
||||
import com.kaixed.kchat.utils.Constants.REFRESH_TOKEN
|
||||
import com.kaixed.kchat.utils.DrawableUtil.createDrawable
|
||||
import com.kaixed.kchat.utils.ScreenUtils.dp2px
|
||||
import com.kaixed.kchat.viewmodel.UserViewModel
|
||||
import com.tencent.mmkv.MMKV
|
||||
import kotlin.math.max
|
||||
|
||||
class LoginActivity : BaseActivity<ActivityLoginBinding>() {
|
||||
|
||||
private val userViewModel: UserViewModel by viewModels()
|
||||
|
||||
private var previousKeyboardHeight = 0
|
||||
|
||||
private var loginByUsername = false
|
||||
|
||||
private lateinit var etUsername: EditText
|
||||
|
||||
private lateinit var etPassword: EditText
|
||||
private var isLoginByTelephone = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
setupEdittext()
|
||||
|
||||
etUsername.addTextChangedListener(textWatcher)
|
||||
etPassword.addTextChangedListener(textWatcher)
|
||||
|
||||
initView()
|
||||
setListener()
|
||||
setListeners()
|
||||
setObservers()
|
||||
|
||||
binding.tvLogin.setOnClickListener {
|
||||
val username = etUsername.text.toString().trim()
|
||||
val password = etPassword.text.toString().trim()
|
||||
|
||||
val username =
|
||||
if (isLoginByTelephone) binding.etTelephone.text.toString() else binding.etUsername.text.toString()
|
||||
val password =
|
||||
if (isLoginByTelephone) binding.etTelephonePassword.text.toString() else binding.etPassword.text.toString()
|
||||
login(username, password)
|
||||
}
|
||||
|
||||
getKeyboardHeight()
|
||||
}
|
||||
|
||||
private fun setupEdittext() {
|
||||
etUsername = if (loginByUsername) binding.etUsername else binding.etTelephone
|
||||
etPassword = binding.etPassword
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
|
||||
}
|
||||
|
||||
private fun login(username: String, password: String) {
|
||||
if (!loginByUsername) {
|
||||
if (etUsername.text.toString().length != 11) {
|
||||
toast("请输入正确的手机号码")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
toast("请输入${if (loginByUsername) "用户名" else "手机号"}或密码")
|
||||
}
|
||||
private fun setObservers() {
|
||||
userViewModel.loginResult.observe(this) { loginResult ->
|
||||
loginResult.onSuccess {
|
||||
it?.let {
|
||||
LocalDatabase.saveUserInfo(it)
|
||||
MMKV.defaultMMKV().putString(ACCESS_TOKEN, it.accessToken)
|
||||
MMKV.defaultMMKV().putString(REFRESH_TOKEN, it.refreshToken)
|
||||
}
|
||||
it?.let { saveUserInfo(it) }
|
||||
navigateToMain()
|
||||
}
|
||||
loginResult.onFailure {
|
||||
toast(it.message.toString())
|
||||
}
|
||||
}
|
||||
if (loginByUsername) {
|
||||
userViewModel.loginByUsername(username, password)
|
||||
} else {
|
||||
userViewModel.loginByTelephone(username, password)
|
||||
loginResult.onFailure { toast(it.message.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
setView()
|
||||
setupLoginView()
|
||||
}
|
||||
|
||||
private fun setListener() {
|
||||
binding.ivClose.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun setListeners() {
|
||||
binding.ivClose.setOnClickListener { finish() }
|
||||
binding.tvChangeLoginWay.setOnClickListener {
|
||||
loginByUsername = !loginByUsername
|
||||
setView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setView() {
|
||||
setupEdittext()
|
||||
|
||||
binding.tvTitle.text = if (loginByUsername) "用户名登录" else "手机号登录"
|
||||
binding.etTelephone.visibility = if (loginByUsername) View.INVISIBLE else View.VISIBLE
|
||||
binding.etUsername.visibility = if (loginByUsername) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
binding.tvUsername.text = if (loginByUsername) "用户名" else "手机号"
|
||||
binding.tvChangeLoginWay.text = if (loginByUsername) "手机号登录" else "用户名登录"
|
||||
binding.tvTip.text =
|
||||
if (loginByUsername) "上述账号仅用于登陆验证" else "上述手机号仅用于登陆验证"
|
||||
}
|
||||
|
||||
private fun getKeyboardHeight() {
|
||||
val rootLayout = binding.root
|
||||
|
||||
rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
rootLayout.getWindowVisibleDisplayFrame(rect)
|
||||
|
||||
val screenHeight = rootLayout.rootView.height
|
||||
val keypadHeight = screenHeight - rect.bottom
|
||||
|
||||
// 通过15%的屏幕高度差来判断是否为键盘
|
||||
if (keypadHeight > (screenHeight * KEYBOARD_HEIGHT_RATIO)) {
|
||||
if (keypadHeight != previousKeyboardHeight) {
|
||||
previousKeyboardHeight = keypadHeight
|
||||
|
||||
val kv = MMKV.defaultMMKV()
|
||||
kv.encode(KEYBOARD_HEIGHT, keypadHeight)
|
||||
}
|
||||
isLoginByTelephone = !isLoginByTelephone
|
||||
setupLoginView()
|
||||
if (isLoginByTelephone) {
|
||||
binding.etTelephone.requestFocus()
|
||||
} else {
|
||||
binding.etUsername.requestFocus()
|
||||
}
|
||||
}
|
||||
binding.tvMore.setOnClickListener { showBottomSheet() }
|
||||
}
|
||||
|
||||
private fun setupLoginView() {
|
||||
resetFields()
|
||||
updateViewBasedOnLoginType()
|
||||
}
|
||||
|
||||
private fun resetFields() {
|
||||
if (isLoginByTelephone) {
|
||||
binding.etTelephone.setText("")
|
||||
binding.etTelephonePassword.setText("")
|
||||
} else {
|
||||
binding.etUsername.setText("")
|
||||
binding.etPassword.setText("")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewBasedOnLoginType() {
|
||||
if (isLoginByTelephone) {
|
||||
configureForPhoneLogin()
|
||||
} else {
|
||||
configureForUsernameLogin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureForPhoneLogin() {
|
||||
binding.etTelephone.setOnClickListener {
|
||||
if (hasSoftInput()) {
|
||||
MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 400))
|
||||
}
|
||||
}
|
||||
binding.etTelephonePassword.addTextChangedListener(textWatcher)
|
||||
binding.etTelephone.addTextChangedListener(textWatcher)
|
||||
binding.clUsername.visibility = View.GONE
|
||||
binding.clTelephone.visibility = View.VISIBLE
|
||||
binding.tvTitle.text = "手机号登录"
|
||||
binding.tvUsername.text = "手机号"
|
||||
binding.tvChangeLoginWay.text = "用户名登录"
|
||||
binding.tvTip.text = "上述手机号仅用于登陆验证"
|
||||
}
|
||||
|
||||
private fun configureForUsernameLogin() {
|
||||
binding.etUsername.setOnClickListener {
|
||||
if (hasSoftInput()) {
|
||||
MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 400))
|
||||
}
|
||||
}
|
||||
binding.etPassword.addTextChangedListener(textWatcher)
|
||||
binding.etUsername.addTextChangedListener(textWatcher)
|
||||
binding.clUsername.visibility = View.VISIBLE
|
||||
binding.clTelephone.visibility = View.GONE
|
||||
binding.tvTitle.text = "用户名登录"
|
||||
binding.tvUsername.text = "用户名"
|
||||
binding.tvChangeLoginWay.text = "手机号登录"
|
||||
binding.tvTip.text = "上述账号仅用于登陆验证"
|
||||
}
|
||||
|
||||
private fun login(username: String, password: String) {
|
||||
if (isInputValid(username, password)) {
|
||||
if (isLoginByTelephone) {
|
||||
userViewModel.loginByTelephone(username, password)
|
||||
} else {
|
||||
userViewModel.loginByUsername(username, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInputValid(username: String, password: String): Boolean {
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
toast("请输入${if (!isLoginByTelephone) "用户名" else "手机号"}或密码")
|
||||
return false
|
||||
}
|
||||
if (isLoginByTelephone && username.length != 11) {
|
||||
toast("请输入正确的手机号码")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveUserInfo(user: UserInfo) {
|
||||
LocalDatabase.saveUserInfo(user)
|
||||
MMKV.defaultMMKV().putString(ACCESS_TOKEN, user.accessToken)
|
||||
MMKV.defaultMMKV().putString(REFRESH_TOKEN, user.refreshToken)
|
||||
}
|
||||
|
||||
private fun showBottomSheet() {
|
||||
val bottomSheetFragment = MyBottomSheetFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putStringArrayList("list", arrayListOf("紧急冻结", "安全中心", "反馈问题", "取消"))
|
||||
}
|
||||
}
|
||||
bottomSheetFragment.show(supportFragmentManager, bottomSheetFragment.tag)
|
||||
}
|
||||
|
||||
private val textWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
|
||||
val isInputValid = etUsername.text.isNotEmpty() && etPassword.text.isNotEmpty()
|
||||
|
||||
binding.tvLogin.apply {
|
||||
setTextColor(
|
||||
if (isInputValid) {
|
||||
ContextCompat.getColor(
|
||||
this@LoginActivity, R.color.white
|
||||
)
|
||||
} else {
|
||||
Color.parseColor("#B4B4B4")
|
||||
}
|
||||
)
|
||||
background = createDrawable(
|
||||
if (isInputValid) {
|
||||
ContextCompat.getColor(
|
||||
this@LoginActivity, R.color.green
|
||||
)
|
||||
} else {
|
||||
Color.parseColor("#E1E1E1")
|
||||
}, dp2px(8)
|
||||
)
|
||||
val isInputValid = when {
|
||||
isLoginByTelephone -> binding.etTelephonePassword.text.isNotEmpty() && binding.etTelephone.text.length == 11
|
||||
else -> binding.etUsername.text.isNotEmpty() && binding.etPassword.text.isNotEmpty()
|
||||
}
|
||||
updateLoginButtonState(isInputValid)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoginButtonState(isInputValid: Boolean) {
|
||||
binding.tvLogin.apply {
|
||||
isEnabled = isInputValid
|
||||
setTextColor(
|
||||
if (isInputValid) ContextCompat.getColor(
|
||||
this@LoginActivity,
|
||||
R.color.white
|
||||
) else Color.parseColor("#B4B4B4")
|
||||
)
|
||||
background = createDrawable(
|
||||
if (isInputValid) ContextCompat.getColor(
|
||||
this@LoginActivity,
|
||||
R.color.green
|
||||
) else Color.parseColor("#E1E1E1"), dp2px(8)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMain() {
|
||||
toast("登录成功")
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding.root.viewTreeObserver.removeOnGlobalLayoutListener { }
|
||||
}
|
||||
|
||||
override fun initData() {}
|
||||
|
||||
override fun inflateBinding(): ActivityLoginBinding {
|
||||
return ActivityLoginBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
@ -3,9 +3,14 @@ package com.kaixed.kchat.ui.widget
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
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 androidx.core.content.ContextCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
@ -13,6 +18,7 @@ import com.kaixed.kchat.databinding.BottomSheetLayoutBinding
|
||||
import com.kaixed.kchat.ui.activity.LoginActivity
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
|
||||
import com.kaixed.kchat.utils.Constants.USER_LOGIN_STATUS
|
||||
import com.kaixed.kchat.utils.ScreenUtils
|
||||
import com.tencent.mmkv.MMKV
|
||||
|
||||
/**
|
||||
@ -39,32 +45,56 @@ class MyBottomSheetFragment : BottomSheetDialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.tvQuitAccount.setOnClickListener {
|
||||
deleteAllUserData()
|
||||
activity?.finish()
|
||||
val list = arguments?.getStringArrayList("list")
|
||||
if (list != null) {
|
||||
initList(list)
|
||||
}
|
||||
}
|
||||
|
||||
activity?.startActivity(Intent(
|
||||
activity, LoginActivity::class.java
|
||||
private fun initList(list: ArrayList<String>) {
|
||||
list.forEach {
|
||||
addTextToContainer(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTextToContainer(item: String) {
|
||||
val textView = TextView(requireContext()).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
ScreenUtils.dp2px(50)
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
})
|
||||
}
|
||||
topMargin = if (item == "取消") ScreenUtils.dp2px(8) else ScreenUtils.dp2px(1)
|
||||
}
|
||||
setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.white))
|
||||
text = item
|
||||
setTextColor(ContextCompat.getColor(requireContext(), R.color.black))
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||
gravity = Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL
|
||||
|
||||
binding.tvCancel.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
setOnClickListener {
|
||||
when (item) {
|
||||
"退出账号" -> {
|
||||
deleteAllUserData()
|
||||
activity?.finish()
|
||||
activity?.startActivity(Intent(
|
||||
activity, LoginActivity::class.java
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
})
|
||||
}
|
||||
|
||||
binding.tvQuitApp.setOnClickListener {
|
||||
activity?.finishAffinity()
|
||||
"取消" -> dismiss()
|
||||
"退出应用" -> activity?.finishAffinity()
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.llBottomSheet.addView(textView)
|
||||
}
|
||||
|
||||
private fun deleteAllUserData() {
|
||||
val mmkv = MMKV.mmkvWithID(MMKV_USER_SESSION)
|
||||
mmkv.clearAll()
|
||||
|
||||
MMKV.defaultMMKV().putBoolean(USER_LOGIN_STATUS, false)
|
||||
|
||||
DataBase.cleanAllData()
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
package com.kaixed.kchat.utils
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2025/1/24 20:41
|
||||
*/
|
||||
class AccountUtils {
|
||||
|
||||
}
|
@ -26,6 +26,6 @@ object Constants {
|
||||
const val REFRESH_TOKEN = "refreshToken"
|
||||
|
||||
const val KEYBOARD_HEIGHT_RATIO = 0.15F
|
||||
const val KEYBOARD_DEFAULT_HEIGHT = 200
|
||||
const val KEYBOARD_DEFAULT_HEIGHT = 300
|
||||
const val STATUS_BAR_DEFAULT_HEIGHT: Int = 50
|
||||
}
|
||||
|
145
app/src/main/kotlin/com/kaixed/kchat/utils/KeyboardUtils.kt
Normal file
145
app/src/main/kotlin/com/kaixed/kchat/utils/KeyboardUtils.kt
Normal file
@ -0,0 +1,145 @@
|
||||
package com.kaixed.kchat.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsAnimation
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
object KeyboardUtils {
|
||||
|
||||
private var sDecorViewInvisibleHeightPre: Int = 0
|
||||
private var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
||||
private var mNavHeight: Int = 0
|
||||
private var sDecorViewDelta: Int = 0
|
||||
|
||||
private fun getDecorViewInvisibleHeight(activity: Activity): Int {
|
||||
val decorView = activity.window.decorView ?: return sDecorViewInvisibleHeightPre
|
||||
val outRect = Rect()
|
||||
decorView.getWindowVisibleDisplayFrame(outRect)
|
||||
|
||||
val delta = Math.abs(decorView.bottom - outRect.bottom)
|
||||
if (delta <= mNavHeight) {
|
||||
sDecorViewDelta = delta
|
||||
return 0
|
||||
}
|
||||
return delta - sDecorViewDelta
|
||||
}
|
||||
|
||||
fun registerKeyboardHeightListener(activity: Activity, listener: KeyboardHeightListener) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
invokeAbove31(activity, listener)
|
||||
} else {
|
||||
invokeBelow31(activity, listener)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun invokeAbove31(activity: Activity, listener: KeyboardHeightListener) {
|
||||
activity.window.decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
|
||||
override fun onProgress(windowInsets: WindowInsets, animations: List<WindowInsetsAnimation>): WindowInsets {
|
||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
val navHeight = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||
val hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
|
||||
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0
|
||||
|
||||
listener.onKeyboardHeightChanged(if (hasNavigationBar) Math.max(imeHeight - navHeight, 0) else imeHeight)
|
||||
return windowInsets
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun invokeBelow31(activity: Activity, listener: KeyboardHeightListener) {
|
||||
val flags = activity.window.attributes.flags
|
||||
|
||||
if (flags and WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS != 0) {
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||
}
|
||||
|
||||
val contentView = activity.findViewById<FrameLayout>(android.R.id.content)
|
||||
sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity)
|
||||
|
||||
onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
val height = getDecorViewInvisibleHeight(activity)
|
||||
if (sDecorViewInvisibleHeightPre != height) {
|
||||
listener.onKeyboardHeightChanged(height)
|
||||
sDecorViewInvisibleHeightPre = height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getNavigationBarHeight(activity) { height, hasNav ->
|
||||
mNavHeight = height
|
||||
contentView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun unregisterKeyboardHeightListener(activity: Activity) {
|
||||
onGlobalLayoutListener = null
|
||||
val contentView = activity.window.decorView.findViewById<View>(android.R.id.content) ?: return
|
||||
contentView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
|
||||
}
|
||||
|
||||
private fun getNavBarHeight(): Int {
|
||||
val res = Resources.getSystem()
|
||||
val resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return if (resourceId != 0) {
|
||||
res.getDimensionPixelSize(resourceId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNavigationBarHeight(activity: Activity, callback: (height: Int, hasNav: Boolean) -> Unit) {
|
||||
val view = activity.window.decorView
|
||||
val attachedToWindow = view.isAttachedToWindow
|
||||
|
||||
if (attachedToWindow) {
|
||||
val windowInsets = ViewCompat.getRootWindowInsets(view)
|
||||
val height = windowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||
val hasNavigationBar = windowInsets?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true &&
|
||||
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0
|
||||
|
||||
if (height > 0) {
|
||||
callback(height, hasNavigationBar)
|
||||
} else {
|
||||
callback(getNavBarHeight(), hasNavigationBar)
|
||||
}
|
||||
} else {
|
||||
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
val windowInsets = ViewCompat.getRootWindowInsets(v)
|
||||
val height = windowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||
val hasNavigationBar = windowInsets?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true &&
|
||||
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0
|
||||
|
||||
if (height > 0) {
|
||||
callback(height, hasNavigationBar)
|
||||
} else {
|
||||
callback(getNavBarHeight(), hasNavigationBar)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface KeyboardHeightListener {
|
||||
fun onKeyboardHeightChanged(height: Int)
|
||||
}
|
||||
|
||||
interface NavigationBarCallback {
|
||||
fun onHeight(height: Int, hasNav: Boolean)
|
||||
}
|
||||
}
|
@ -76,8 +76,8 @@ class UserViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private val _userInfoResult = MutableLiveData<Result<SearchUser>>()
|
||||
val userInfoResult: LiveData<Result<SearchUser>> = _userInfoResult
|
||||
private val _userInfoResult = MutableLiveData<Result<SearchUser?>>()
|
||||
val userInfoResult: LiveData<Result<SearchUser?>> = _userInfoResult
|
||||
|
||||
// 获取用户信息
|
||||
fun getUserInfo(username: String) {
|
||||
@ -87,8 +87,8 @@ class UserViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private val _changeNicknameResult = MutableLiveData<Result<Boolean>>()
|
||||
val changeNicknameResult: LiveData<Result<Boolean>> = _changeNicknameResult
|
||||
private val _changeNicknameResult = MutableLiveData<Result<Boolean?>>()
|
||||
val changeNicknameResult: LiveData<Result<Boolean?>> = _changeNicknameResult
|
||||
|
||||
// 修改昵称
|
||||
fun changeNickname(userRequest: UserRequest) {
|
||||
|
@ -38,106 +38,178 @@
|
||||
android:layout_marginTop="50dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:text="手机号"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/view" />
|
||||
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_username"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:maxLength="16"
|
||||
android:layout_marginStart="35dp"
|
||||
android:background="@null"
|
||||
android:hint="请填写用户名"
|
||||
android:textCursorDrawable="@drawable/cursor"
|
||||
android:textSize="17sp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@id/view1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_username"
|
||||
app:layout_constraintTop_toBottomOf="@id/view" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_telephone"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="35dp"
|
||||
android:background="@null"
|
||||
android:hint="请填写手机号"
|
||||
android:maxLength="11"
|
||||
android:textCursorDrawable="@drawable/cursor"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/view1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_username"
|
||||
app:layout_constraintTop_toBottomOf="@id/view" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view1"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_telephone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0.2dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:background="#D8D8D8"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_username" />
|
||||
app:layout_constraintTop_toBottomOf="@id/view">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_password"
|
||||
<TextView
|
||||
android:id="@+id/tv_telephone"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_marginStart="15dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="手机号"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_telephone"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@null"
|
||||
android:hint="请填写手机号"
|
||||
android:maxLength="16"
|
||||
android:textCursorDrawable="@drawable/cursor"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_telephone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_telephone"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_telephone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0.2dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="#D8D8D8"
|
||||
app:layout_constraintBottom_toTopOf="@id/tv_telephone_password"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_telephone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_telephone_password"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="45dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="密码"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_telephone"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_telephone" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_telephone_password"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@null"
|
||||
android:hint="请填写密码"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="16"
|
||||
android:maxLines="1"
|
||||
android:textColor="#A5A5A5"
|
||||
android:textCursorDrawable="@drawable/cursor"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_telephone_password"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_telephone_password"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_telephone_password" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintTop_toBottomOf="@id/view">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_username"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_marginStart="15dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="账号"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_username"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@null"
|
||||
android:hint="请填写账号"
|
||||
android:maxLength="16"
|
||||
android:textCursorDrawable="@drawable/cursor"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_username"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_username"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_username" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0.2dp"
|
||||
android:background="#D8D8D8"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/tv_password"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_username" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_password"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="45dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="密码"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_username"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_username" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_password"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@null"
|
||||
android:hint="请填写密码"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="16"
|
||||
android:maxLines="1"
|
||||
android:textColor="#A5A5A5"
|
||||
android:textCursorDrawable="@drawable/cursor"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_password"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_password"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_password" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:text="密码"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/view1" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_password"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@null"
|
||||
android:hint="请填写密码"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="16"
|
||||
android:maxLines="1"
|
||||
android:textCursorDrawable="@drawable/cursor"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/view2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/et_username"
|
||||
app:layout_constraintTop_toBottomOf="@id/view1" />
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="cl_username, cl_telephone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0.2dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:background="#D8D8D8"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_password" />
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_change_login_way"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:text="用户名登录"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_username"
|
||||
app:layout_constraintTop_toBottomOf="@id/view2" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_tip"
|
||||
@ -166,62 +238,42 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/view2"
|
||||
app:layout_constraintVertical_bias="0.7" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_urgent_freeze"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="紧急冻结"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_login"
|
||||
app:layout_constraintVertical_bias="0.8" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_forgot_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="找回密码"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_urgent_freeze"
|
||||
app:layout_constraintHorizontal_bias="0.7"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/view_2"
|
||||
app:layout_constraintEnd_toStartOf="@id/view_2"
|
||||
app:layout_constraintHorizontal_bias="0.9"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" />
|
||||
app:layout_constraintTop_toTopOf="@id/view_2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_security_center"
|
||||
android:id="@+id/tv_more"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="安全中心"
|
||||
android:text="更多选项"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/view_2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.3"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_urgent_freeze"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" />
|
||||
app:layout_constraintHorizontal_bias="0.1"
|
||||
app:layout_constraintStart_toEndOf="@id/view_2"
|
||||
app:layout_constraintTop_toTopOf="@id/view_2" />
|
||||
|
||||
|
||||
<View
|
||||
android:id="@+id/view_2"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="20dp"
|
||||
android:background="#e5e5e5"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_urgent_freeze"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_forgot_password"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" />
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="#e5e5e5"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_security_center"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_urgent_freeze"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" />
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_login"
|
||||
app:layout_constraintVertical_bias="0.7" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -8,47 +8,11 @@
|
||||
app:cardMaxElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#F7F7F7"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_quit_account"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="55dp"
|
||||
android:background="@color/white"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="退出登录"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_quit_app"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="55dp"
|
||||
android:layout_marginTop="1dp"
|
||||
android:background="@color/white"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="关闭应用"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/divider_height"
|
||||
android:background="#E5E5E5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cancel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="55dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@color/white"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="取消"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
@ -25,6 +25,7 @@ pinyin4j = "2.5.1"
|
||||
preference = "1.2.1"
|
||||
retrofit = "2.11.0"
|
||||
scanplus = "2.12.0.301"
|
||||
softInputEvent = "1.0.9"
|
||||
spannable = "1.2.7"
|
||||
therouter = "1.2.2"
|
||||
window = "1.3.0"
|
||||
@ -49,6 +50,7 @@ pinyin4j = { module = "com.belerweb:pinyin4j", version.ref = "pinyin4j" }
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
|
||||
scanplus = { module = "com.huawei.hms:scanplus", version.ref = "scanplus" }
|
||||
soft-input-event = { module = "com.github.liangjingkanji:soft-input-event", version.ref = "softInputEvent" }
|
||||
spannable = { module = "com.github.liangjingkanji:spannable", version.ref = "spannable" }
|
||||
therouter-ksp = { module = "cn.therouter:apt", version.ref = "therouter" }
|
||||
objectbox-android-objectbrowser = { group = "io.objectbox", name = "objectbox-android-objectbrowser", version.ref = "objectbox" }
|
||||
|
Loading…
Reference in New Issue
Block a user