feat: 完善页面

1.完善聊天界面,好友详情页
2.抽象网络请求接口,以减少重复代码
3.引入pictureselector、spannable库
This commit is contained in:
糕小菜 2024-11-25 22:26:51 +08:00
parent c45acb5bda
commit e1816f525a
69 changed files with 1592 additions and 1422 deletions

View File

@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-11-19T05:46:34.362744600Z">
<DropdownSelection timestamp="2024-11-25T08:35:02.927264800Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=10.127.185.244:5555;connection=5b2cc3eb" />
<DeviceId pluginId="Default" identifier="serial=10.39.42.8:5555;connection=563cba5e" />
</handle>
</Target>
</DropdownSelection>

View File

@ -80,6 +80,16 @@ dependencies {
implementation(libs.okhttp3.logging.interceptor)
// 图片选择器
implementation(libs.pictureselector)
// 图片压缩
implementation(libs.compress)
// 图片裁剪
implementation(libs.ucrop)
// 自定义spannable
implementation(libs.spannable)
// implementation(libs.therouter)
// ksp(libs.therouter.ksp)
}

View File

@ -69,4 +69,12 @@
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
# With R8 full mode generic signatures are stripped for classes that are not kept.
-keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep class com.luck.picture.lib.** { *; }
-dontwarn com.yalantis.ucrop**
-keep class com.yalantis.ucrop** { *; }
-keep interface com.yalantis.ucrop** { *; }

View File

@ -2,9 +2,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -19,7 +26,7 @@
tools:ignore="ScopedStorage" />
<application
android:name=".KchatApplication"
android:name=".App"
android:allowBackup="true"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -32,6 +39,12 @@
android:theme="@style/Theme.KChatAndroid"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.activity.TestActivity"
android:exported="false" />
<activity
android:name=".ui.activity.ContactPermissionActivity"
android:exported="false" />
<activity
android:name=".ui.activity.SetRemarkAndLabelActivity"
android:exported="false" />
@ -82,9 +95,6 @@
<activity
android:name=".ui.activity.ApproveContactRequestActivity"
android:exported="false" />
<activity
android:name=".ui.activity.MessageActivity"
android:exported="false" />
<activity
android:name=".ui.activity.ApplyAddFriendActivity"
android:exported="false" />

View File

@ -1,24 +1,24 @@
package com.kaixed.kchat
import android.app.Application
import android.util.Log
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.ObjectBox.init
import com.kaixed.kchat.utils.DensityUtil
import com.tencent.mmkv.MMKV
import io.objectbox.android.Admin
/**
* @Author: kaixed
* @Date: 2024/10/24 17:04
*/
class KchatApplication : Application() {
class App : Application() {
override fun onCreate() {
super.onCreate()
MMKV.initialize(this)
init(this)
Admin(get()).start(this)
Admin(getBoxStore()).start(this)
DensityUtil.init(this)
}

View File

@ -17,5 +17,5 @@ object ObjectBox {
.build()
}
fun get(): BoxStore = checkNotNull(store) { "ObjectBox is not initialized." }
fun getBoxStore(): BoxStore = checkNotNull(store) { "ObjectBox is not initialized." }
}

View File

@ -0,0 +1,18 @@
package com.kaixed.kchat.data.objectbox.data
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.data.objectbox.entity.Contact_
/**
* @Author: kaixed
* @Date: 2024/11/24 13:34
*/
object LocalContact {
private val contactBox by lazy { getBoxStore().boxFor(Contact::class.java) }
fun getContactByUsername(contactId: String): Contact? {
return contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
}
}

View File

@ -0,0 +1,25 @@
package com.kaixed.kchat.network
/**
* @Author: kaixed
* @Date: 2024/11/24 9:29
*/
object ApiCall {
suspend fun <T> apiCall(
apiCall: suspend () -> ApiResponse<T>,
errorMessage: String = "操作失败"
): Result<T?> {
return try {
val response = apiCall()
if (response.isSuccess()) {
response.getResponseData()?.let {
Result.success(it)
} ?: Result.failure(Exception(errorMessage))
} else {
Result.failure(Exception(response.getResponseMsg()))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@ -57,4 +57,13 @@ interface ContactService {
@Field("userId") username: String,
@Field("contactId") contactId: String,
): ApiResponse<String?>
// 设置好友备注
@FormUrlEncoded
@POST("friend/remark")
suspend fun setRemark(
@Field("userId") userId: String,
@Field("contactId") contactId: String,
@Field("remark") remark: String,
): ApiResponse<String?>
}

View File

@ -39,7 +39,7 @@ interface UserApiService {
suspend fun loginByUsername(
@Field("username") username: String,
@Field("password") password: String
): ApiResponse<UserInfo>
): ApiResponse<UserInfo?>
// 登录接口(根据电话登录)
@FormUrlEncoded

View File

@ -1,10 +1,11 @@
package com.kaixed.kchat.repository
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.data.objectbox.entity.Contact_
import com.kaixed.kchat.model.friend.FriendRequestItem
import com.kaixed.kchat.model.search.User
import com.kaixed.kchat.network.ApiCall.apiCall
import com.kaixed.kchat.network.RetrofitClient
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.Pinyin4jUtil
@ -16,24 +17,28 @@ class ContactRepository {
private val contactApiService = RetrofitClient.contactApiService
private val contactBox: Box<Contact> by lazy {
get().boxFor(Contact::class.java)
getBoxStore().boxFor(Contact::class.java)
}
// 获取联系人请求列表
suspend fun getContactRequestList(username: String): Result<List<FriendRequestItem>?> {
return try {
val response = contactApiService.getContactRequestList(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)
}
return apiCall(
apiCall = { contactApiService.getContactRequestList(username) },
errorMessage = "获取好友申请列表失败"
)
// return try {
// val response = contactApiService.getContactRequestList(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)
// }
}
// 接受联系人请求
@ -42,19 +47,27 @@ class ContactRepository {
contactId: String,
remark: String
): Result<Contact?> {
return try {
val response = contactApiService.acceptContactRequest(contactId, username, remark)
if (response.isSuccess()) {
val searchUser = response.getResponseData()
searchUser?.let {
ContactUtil.handleContact(searchUser)
Result.success(searchUser)
} ?: Result.failure(Exception("添加好友失败"))
} else {
Result.failure(Exception(response.getResponseMsg()))
return apiCall(
apiCall = { contactApiService.acceptContactRequest(contactId, username, remark) },
errorMessage = "添加好友失败"
).onSuccess {
it?.let {
ContactUtil.handleContact(it)
}
} catch (e: Exception) {
Result.failure(e)
// return try {
// val response = contactApiService.acceptContactRequest(contactId, username, remark)
// if (response.isSuccess()) {
// val searchUser = response.getResponseData()
// searchUser?.let {
// ContactUtil.handleContact(searchUser)
// Result.success(searchUser)
// } ?: Result.failure(Exception("添加好友失败"))
// } else {
// Result.failure(Exception(response.getResponseMsg()))
// }
// } catch (e: Exception) {
// Result.failure(e)
// }
}
}
@ -63,105 +76,105 @@ class ContactRepository {
username: String,
contactId: String,
): Result<String?> {
return try {
val response = contactApiService.deleteContact(username, contactId)
if (response.isSuccess()) {
val searchUser = response.getResponseData()
searchUser?.let {
val con =
contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
contactBox.remove(con!!)
Result.success(searchUser)
} ?: Result.failure(Exception("删除好友失败"))
} else {
Result.failure(Exception(response.getResponseMsg()))
}
} catch (e: Exception) {
Result.failure(e)
return apiCall(
apiCall = { contactApiService.deleteContact(username, contactId) },
errorMessage = "删除好友失败"
).onSuccess {
val con =
contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
contactBox.remove(con!!)
}
// return try {
// val response = contactApiService.deleteContact(username, contactId)
// if (response.isSuccess()) {
// val searchUser = response.getResponseData()
// searchUser?.let {
// val con =
// contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
// contactBox.remove(con!!)
// Result.success(searchUser)
// } ?: Result.failure(Exception("删除好友失败"))
// } else {
// Result.failure(Exception(response.getResponseMsg()))
// }
// } catch (e: Exception) {
// Result.failure(e)
// }
}
// 添加联系人
suspend fun addContact(contactId: String, message: String): Result<String> {
return try {
val response = contactApiService.addContact(
senderId = contactId,
receiverId = getUsername(),
message = message
)
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 addContact(contactId: String, message: String): Result<String?> {
return apiCall(
apiCall = { contactApiService.addContact(contactId, getUsername(), message) },
errorMessage = "添加联系人失败"
)
// return try {
// val response = contactApiService.addContact(
// senderId = contactId,
// receiverId = getUsername(),
// message = message
// )
// 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 searchContact(username: String): Result<User?> {
return try {
val response = contactApiService.searchContact(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)
}
return apiCall(
apiCall = { contactApiService.searchContact(username) },
errorMessage = "搜索用户失败"
)
// return try {
// val response = contactApiService.searchContact(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 getContactList(username: String): Result<List<Contact>?> {
// return safeApiCall(
// apiCall = { contactApiService.getContactList(username) },
// errorMessage = "获取好友列表失败"
// ).onSuccess {
//
// }
return try {
val response = contactApiService.getContactList(username)
if (response.isSuccess()) {
val searchUsers = response.getResponseData()
searchUsers?.let {
val uiFriendList = searchUsers?.map { networkItem ->
val uiFriendList = searchUsers.map { networkItem ->
mapToFriendItem(networkItem) // 数据转换
}?.sortedWith { f1, f2 ->
val str1 = f1.remarkquanpin ?: f1.quanpin ?: f1.nickname
val str2 = f2.remarkquanpin ?: f2.quanpin ?: f2.nickname
val type1 = when {
str1.matches(Regex("[a-zA-Z]+")) -> 1 // 字母
str1.matches(Regex("[0-9]+")) -> 2 // 数字
else -> 3 // 特殊字符
}
val type2 = when {
str2.matches(Regex("[a-zA-Z]+")) -> 1 // 字母
str2.matches(Regex("[0-9]+")) -> 2 // 数字
else -> 3 // 特殊字符
}
// 比较类型,如果相同再按字母升序排序
if (type1 != type2) {
type1 - type2 // 按类型排序
} else {
str1.compareTo(str2) // 如果类型相同,按字母顺序排序
}
}
}.sortedWith(::compareContacts)
// 设置是否显示分组头
uiFriendList?.forEachIndexed { index, item ->
uiFriendList.forEachIndexed { index, item ->
item.showHeader =
(index == 0 || item.quanpin?.get(index) != uiFriendList[index - 1].quanpin?.get(
index - 1
))
}
Result.success(searchUsers)
Result.success(uiFriendList)
} ?: Result.failure(Exception("当前没有好友"))
} else {
Result.failure(Exception(response.getResponseMsg()))
@ -171,6 +184,20 @@ class ContactRepository {
}
}
suspend fun setRemark(userId: String, contactId: String, remark: String): Result<String?> {
return apiCall(
apiCall = { contactApiService.setRemark(userId, contactId, remark) },
errorMessage = "设置备注失败"
).onSuccess {
val con = contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
if (con != null) {
con.remark = remark
con.remarkquanpin = Pinyin4jUtil.toPinyin(remark)
contactBox.put(con)
}
}
}
private fun mapToFriendItem(response: Contact): Contact {
val pinyin = Pinyin4jUtil.toPinyin(response.nickname)
response.remark?.let {
@ -179,4 +206,27 @@ class ContactRepository {
response.quanpin = pinyin
return response
}
private fun compareContacts(c1: Contact, c2: Contact): Int {
val str1 = c1.remarkquanpin ?: c1.quanpin ?: c1.nickname
val str2 = c2.remarkquanpin ?: c2.quanpin ?: c2.nickname
val type1 = when {
str1.matches(Regex("[a-zA-Z]+")) -> 1
str1.matches(Regex("[0-9]+")) -> 2
else -> 3
}
val type2 = when {
str2.matches(Regex("[a-zA-Z]+")) -> 1
str2.matches(Regex("[0-9]+")) -> 2
else -> 3
}
return if (type1 != type2) {
type1 - type2
} else {
str1.compareTo(str2)
}
}
}

View File

@ -1,10 +1,11 @@
package com.kaixed.kchat.repository
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.data.objectbox.entity.UserInfo_
import com.kaixed.kchat.model.request.RegisterRequest
import com.kaixed.kchat.model.response.register.Register
import com.kaixed.kchat.network.ApiCall.apiCall
import com.kaixed.kchat.network.RetrofitClient
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.tencent.mmkv.MMKV
@ -18,54 +19,107 @@ class UserAuthRepository {
private val userApiService = RetrofitClient.userApiService
private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) }
private val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor(UserInfo::class.java) }
private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
// 用户注册
suspend fun register(registerRequest: RegisterRequest): Result<Register> {
return try {
val response = userApiService.register(registerRequest)
if (response.isSuccess()) {
val register = response.getResponseData()
register?.let {
val userInfo = UserInfo(
username = register.username,
nickname = register.nickname,
avatarUrl = "",
signature = "",
telephone = registerRequest.telephone
)
userInfoBox.put(userInfo)
Result.success(register)
} ?: Result.failure(Exception("注册成功,但未返回用户数据"))
} else {
Result.failure(Exception(response.getResponseMsg()))
suspend fun register(registerRequest: RegisterRequest): Result<Register?> {
return apiCall(
apiCall = { userApiService.register(registerRequest) },
errorMessage = "注册成功,但未返回用户数据"
).onSuccess { register ->
register?.let {
insertUserInfo(register, registerRequest.telephone)
}
} catch (e: Exception) {
Result.failure(e)
}
// return try {
// val response = userApiService.register(registerRequest)
// if (response.isSuccess()) {
// val register = response.getResponseData()
// register?.let {
// val userInfo = UserInfo(
// username = register.username,
// nickname = register.nickname,
// avatarUrl = "",
// signature = "",
// telephone = registerRequest.telephone
// )
// userInfoBox.put(userInfo)
//
// Result.success(register)
// } ?: Result.failure(Exception("注册成功,但未返回用户数据"))
// } else {
// Result.failure(Exception(response.getResponseMsg()))
// }
// } catch (e: Exception) {
// Result.failure(e)
// }
}
// 登录方法
suspend fun loginByUsername(username: String, password: String): Result<UserInfo> {
return try {
val response = userApiService.loginByUsername(username, password)
if (response.isSuccess()) {
val userInfo = response.getResponseData()
if (userInfo != null) {
updateDb(userInfo)
Result.success(userInfo)
} else {
Result.failure(Exception("登录成功,但未返回用户数据"))
}
} else {
Result.failure(Exception(response.getResponseMsg()))
suspend fun loginByUsername(username: String, password: String): Result<UserInfo?> {
return apiCall(
apiCall = { userApiService.loginByUsername(username, password) },
errorMessage = "登录成功,但未返回用户数据"
).onSuccess { userInfo ->
userInfo?.let {
updateDb(userInfo)
}
} catch (e: Exception) {
Result.failure(e)
}
// return try {
// val response = userApiService.loginByUsername(username, password)
// if (response.isSuccess()) {
// val userInfo = response.getResponseData()
// if (userInfo != null) {
// updateDb(userInfo)
// Result.success(userInfo)
// } else {
// Result.failure(Exception("登录成功,但未返回用户数据"))
// }
// } else {
// Result.failure(Exception(response.getResponseMsg()))
// }
// } catch (e: Exception) {
// Result.failure(e)
// }
}
suspend fun loginByTelephone(telephone: String, password: String): Result<UserInfo?> {
return apiCall(
apiCall = { userApiService.loginByTelephone(telephone, password) },
errorMessage = "登录成功,但未返回用户数据"
).onSuccess { userInfo ->
userInfo?.let {
updateDb(userInfo)
}
}
// return try {
// val response = userApiService.loginByTelephone(telephone, password)
// if (response.isSuccess()) {
// val userInfo = response.getResponseData()
// userInfo?.let {
// updateDb(userInfo)
// Result.success(userInfo)
// } ?: Result.failure(Exception("登录成功,但未返回用户数据"))
// } else {
// Result.failure(Exception(response.getResponseMsg()))
// }
// } catch (e: Exception) {
// Result.failure(e)
// }
}
private fun insertUserInfo(register: Register, telephone: String) {
val userInfo = UserInfo(
username = register.username,
nickname = register.nickname,
avatarUrl = "",
signature = "",
telephone = telephone
)
userInfoBox.put(userInfo)
}
private fun updateDb(userInfo: UserInfo) {
@ -83,21 +137,4 @@ class UserAuthRepository {
userInfoBox.put(userInfo)
}
}
suspend fun loginByTelephone(telephone: String, password: String): Result<UserInfo> {
return try {
val response = userApiService.loginByTelephone(telephone, password)
if (response.isSuccess()) {
val userInfo = response.getResponseData()
userInfo?.let {
updateDb(userInfo)
Result.success(userInfo)
} ?: Result.failure(Exception("登录成功,但未返回用户数据"))
} else {
Result.failure(Exception(response.getResponseMsg()))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@ -1,6 +1,6 @@
package com.kaixed.kchat.repository
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.data.objectbox.entity.UserInfo_
import com.kaixed.kchat.model.request.UserRequest
@ -42,7 +42,7 @@ class UserProfileRepository {
return try {
val response = userApiService.changeNickname(userRequest)
if (response.isSuccess()) {
updateNickname(response.getResponseData().toString())
updateNickname(userRequest.nickname!!)
Result.success(true)
} else {
Result.failure(Exception(response.getResponseMsg()))
@ -71,7 +71,7 @@ class UserProfileRepository {
}
private fun updateNickname(newNickname: String) {
val userInfoBox = get().boxFor(UserInfo::class.java)
val userInfoBox = getBoxStore().boxFor(UserInfo::class.java)
val user = userInfoBox
.query(UserInfo_.username.equal(getUsername()))
.build()
@ -87,7 +87,7 @@ class UserProfileRepository {
}
private fun updateDb(url: String) {
val userInfoBox = get().boxFor(UserInfo::class.java)
val userInfoBox = getBoxStore().boxFor(UserInfo::class.java)
val userInfo =
userInfoBox.query(UserInfo_.username.equal(getUsername())).build().findFirst()
if (userInfo != null) {

View File

@ -8,7 +8,7 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Conversation
import com.kaixed.kchat.data.objectbox.entity.Conversation_
import com.kaixed.kchat.data.objectbox.entity.Messages
@ -39,7 +39,7 @@ class WebSocketService : Service() {
private lateinit var messagesBox: Box<Messages>
private val conversationBox: Box<Conversation> by lazy { get().boxFor(Conversation::class.java) }
private val conversationBox: Box<Conversation> by lazy { getBoxStore().boxFor(Conversation::class.java) }
private lateinit var username: String
@ -69,7 +69,7 @@ class WebSocketService : Service() {
override fun onCreate() {
super.onCreate()
messagesBox = get().boxFor(Messages::class.java)
messagesBox = getBoxStore().boxFor(Messages::class.java)
username = getUsername()
firstLoad()
}
@ -113,7 +113,7 @@ class WebSocketService : Service() {
if (conversation.talkerId == currentContactId || messages.senderId == getUsername()) 0
else conversation.unreadCount + 1
conversation.lastContent = messages.content
conversation.lastContent = if (messages.type == "4") "[图片]" else messages.content
conversation.timestamp = messages.timestamp
conversationBox.put(conversation)
} ?: run {
@ -157,8 +157,6 @@ class WebSocketService : Service() {
messages.takerId = messages.senderId
updateDbConversation(messages)
serviceScope.launch {
if ("ack" == messages.type) {
val existingMessage = messagesBox.get(messages.msgLocalId)
@ -202,7 +200,7 @@ class WebSocketService : Service() {
return
}
conversationList[index].apply {
lastContent = messages.content
lastContent = if (messages.type == "4") "[图片]" else messages.content
timestamp = messages.timestamp
unreadCount =
if (this.talkerId == getCurrentContactId() || messages.senderId == getUsername()) 0 else unreadCount + 1
@ -212,7 +210,7 @@ class WebSocketService : Service() {
createChatList(
talkerId = messages.takerId,
nickname = messages.takerId,
content = messages.content,
content = if (messages.type == "4") "[图片]" else messages.content,
timestamp = messages.timestamp
)
)

View File

@ -6,19 +6,27 @@ import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import com.kaixed.kchat.databinding.ActivityAddFriendsBinding
import com.kaixed.kchat.ui.base.BaseActivity
class AddFriendsActivity : BaseActivity<ActivityAddFriendsBinding>() {
override fun inflateBinding(): ActivityAddFriendsBinding {
return ActivityAddFriendsBinding.inflate(layoutInflater)
}
class AddFriendsActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddFriendsBinding
private val mContext: Context by lazy { this }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityAddFriendsBinding.inflate(layoutInflater)
setContentView(binding.root)
initView()
}
override fun initData() {
}
private fun initView() {
binding.ivBack.setOnClickListener {
finish()

View File

@ -26,6 +26,10 @@ class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() {
}
}
override fun initData() {
}
private fun sendContactRequest(contactId: String?) {
contactId?.let {
contactViewModel.addContactResult

View File

@ -21,21 +21,22 @@ class ApplyFriendsDetailActivity : BaseActivity<ActivityApplyFriendsDetailBindin
private var user: User? = null
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
user = intent.getParcelableExtra("user", User::class.java)
request = intent.getParcelableExtra("request", FriendRequestItem::class.java)
setContent()
setOnClick()
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun initData() {
user = intent.getParcelableExtra("user", User::class.java)
request = intent.getParcelableExtra("request", FriendRequestItem::class.java)
}
private fun setContent() {
// 使用 Elvis 操作符 ?:,如果 request 不为空则使用 request否则使用 user
val nickname = request?.nickname ?: user?.nickname
val signature = request?.signature ?: user?.signature
val avatarUrl = request?.avatarUrl ?: user?.avatarUrl

View File

@ -9,16 +9,17 @@ import androidx.activity.viewModels
import com.kaixed.kchat.databinding.ActivityApproveContactRequestBinding
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.handle.ContactUtil
import com.kaixed.kchat.viewmodel.ContactViewModel
class ApproveContactRequestActivity : BaseActivity<ActivityApproveContactRequestBinding>() {
private val contactViewModel: ContactViewModel by viewModels()
private var nickname: String? = null
private var message: String? = null
private var contactId: String? = null
private lateinit var nickname: String
private lateinit var message: String
private lateinit var contactId: String
override fun inflateBinding(): ActivityApproveContactRequestBinding {
return ActivityApproveContactRequestBinding.inflate(layoutInflater)
@ -28,17 +29,16 @@ class ApproveContactRequestActivity : BaseActivity<ActivityApproveContactRequest
super.onCreate(savedInstanceState)
enableEdgeToEdge()
handleIntent(intent)
setContent()
setOnClickListener()
}
private fun handleIntent(intent: Intent) {
nickname = intent.getStringExtra("nickname")
message = intent.getStringExtra("message")
contactId = intent.getStringExtra("contactId")
override fun initData() {
nickname = intent.getStringExtra("nickname").toString()
message = intent.getStringExtra("message").toString()
contactId = intent.getStringExtra("contactId").toString()
}
@SuppressLint("SetTextI18n")

View File

@ -30,6 +30,10 @@ class ApproveDetailActivity : BaseActivity<ActivityApproveDetailBinding>() {
setOnClick()
}
override fun initData() {
}
private fun handleIntent(intent: Intent) {
request = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("request", FriendRequestItem::class.java)

View File

@ -24,7 +24,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Messages
import com.kaixed.kchat.data.objectbox.entity.Messages_
import com.kaixed.kchat.databinding.ActivityChatBinding
@ -99,10 +99,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
handleIntent(intent)
initData()
firstLoadData()
initView()
@ -153,7 +149,7 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
},
}
)
}
@ -253,9 +249,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener {
private fun setListener() {
setBackPressListener()
binding.tvContactName.setOnLongClickListener {
false
}
binding.gvFunctionPanel.selector = ColorDrawable(Color.TRANSPARENT)
@ -277,26 +270,24 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener {
// }
}
})
binding.ivBack.setOnClickListener { finish() }
binding.tvContactName.setOnClickListener {
binding.ctb.setOnTitleClickListener {
startActivity(
Intent(
context,
ContactsDetailActivity::class.java
)
Intent(context, ContactsDetailActivity::class.java).apply {
putExtra("contactId", contactId)
}
)
}
binding.ivMore.setOnClickListener {
binding.ctb.setOnSettingClickListener {
startActivity(
Intent(
context,
ChatDetailActivity::class.java
)
Intent(context, ChatDetailActivity::class.java).apply {
putExtra("contactId", contactId)
}
)
}
binding.etInput.addTextChangedListener(
afterTextChanged = { _ ->
val isInputEmpty = binding.etInput.text.toString().isEmpty()
@ -339,12 +330,7 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener {
val functionItems = mutableListOf<FunctionItem>()
for (i in map.keys) {
functionItems.add(
FunctionItem(
i,
map[i]!!
)
)
functionItems.add(FunctionItem(i, map[i]!!))
}
val functionPanelAdapter = FunctionPanelAdapter(functionItems, this)
@ -375,7 +361,7 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener {
setupFunctionPanel()
setRecycleView()
contactNickname?.let {
binding.tvContactName.text = contactNickname
binding.ctb.setTitleName(contactNickname!!)
}
setPanel()
}
@ -386,18 +372,16 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener {
binding.clBottomPanel.layoutParams = layoutParams
}
private fun initData() {
messagesBox = get().boxFor(Messages::class.java)
softKeyboardHeight = getKeyboardHeight()
mmkv.putString(CURRENT_CONTACT_ID, contactId)
}
private fun handleIntent(intent: Intent?) {
override fun initData() {
intent?.let {
contactId = intent.getStringExtra("contactId").toString()
contactNickname = intent.getStringExtra("contactNickname")
binding.tvContactName.text = contactNickname
binding.ctb.setTitleName(contactNickname!!)
}
messagesBox = getBoxStore().boxFor(Messages::class.java)
softKeyboardHeight = getKeyboardHeight()
mmkv.putString(CURRENT_CONTACT_ID, contactId)
}
private fun setRecycleView() {

View File

@ -3,21 +3,44 @@ package com.kaixed.kchat.ui.activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.kaixed.kchat.data.objectbox.data.LocalContact.getContactByUsername
import com.kaixed.kchat.databinding.ActivityChatDetailBinding
import com.kaixed.kchat.ui.base.BaseActivity
class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
private lateinit var contactId: String
private val contact by lazy { getContactByUsername(contactId) }
override fun inflateBinding(): ActivityChatDetailBinding {
return ActivityChatDetailBinding.inflate(layoutInflater)
}
class ChatDetailActivity : AppCompatActivity() {
private lateinit var binding: ActivityChatDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityChatDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.ifvAvatar.setOnClickListener { v ->
initView()
setListener()
}
private fun initView() {
Glide.with(this).load(contact?.avatarUrl).into(binding.ifvAvatar)
binding.tvContactName.text = contact?.remark ?: contact?.nickname
}
override fun initData() {
contactId = intent.getStringExtra("contactId") ?: ""
}
private fun setListener() {
binding.ifvAvatar.setOnClickListener {
val intent =
Intent(this@ChatDetailActivity, ContactsDetailActivity::class.java)
intent.putExtra("friendId", "kaixed")
Intent(this, ContactsDetailActivity::class.java)
intent.putExtra("contactId", "kaixed")
startActivity(intent)
}
}

View File

@ -0,0 +1,28 @@
package com.kaixed.kchat.ui.activity
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import com.kaixed.kchat.databinding.ActivityContactPermissionBinding
import com.kaixed.kchat.ui.base.BaseActivity
class ContactPermissionActivity : BaseActivity<ActivityContactPermissionBinding>() {
override fun inflateBinding(): ActivityContactPermissionBinding {
return ActivityContactPermissionBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
initView()
}
override fun initData() {
TODO("Not yet implemented")
}
private fun initView() {
binding.ciChat.setYesSelected(false)
}
}

View File

@ -38,6 +38,10 @@ class ContactRequestListActivity : BaseActivity<ActivityContactRequestListBindin
binding.recycleFriendRequestList.adapter = contactRequestListAdapter
}
override fun initData() {
}
private fun getItems() {
contactViewModel.contactRequestListResult
.observe(this) { result ->

View File

@ -5,7 +5,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.data.objectbox.entity.Contact_
import com.kaixed.kchat.databinding.ActivityContactsDetailBinding
@ -14,7 +14,7 @@ import io.objectbox.Box
class ContactsDetailActivity : BaseActivity<ActivityContactsDetailBinding>() {
private val contactBox: Box<Contact> by lazy { get().boxFor(Contact::class.java) }
private val contactBox: Box<Contact> by lazy { getBoxStore().boxFor(Contact::class.java) }
private var contactId: String? = null
@ -33,25 +33,36 @@ class ContactsDetailActivity : BaseActivity<ActivityContactsDetailBinding>() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
contactId = intent.getStringExtra("contactId")
val contact = getUserInfo(contactId!!)
if (contact != null) {
binding.tvContactId.text = "kid: ${contact.username}"
binding.tvContactSignature.text = contact.signature
// Log.d(TAG, "onCreate: $contact")
if (contact.remark?.isNotEmpty() == true) {
binding.tvRemark.text = contact.remark
contactNickname = contact.remark
binding.tvContactName.text = "昵称:${contact.nickname}"
} else {
contactNickname = contact.nickname
binding.tvRemark.text = contact.nickname
binding.tvContactName.visibility = View.GONE
}
}
setOnListener()
updateContent()
}
override fun initData() {
contactId = intent.getStringExtra("contactId")
}
@SuppressLint("SetTextI18n")
private fun updateContent() {
val contact: Contact by lazy {
getContactInfo(contactId!!)
}
binding.tvContactId.text = "kid: $contactId"
binding.tvContactSignature.text = contact.signature
if (contact.remark?.isNotEmpty() == true) {
binding.tvRemark.text = contact.remark
contactNickname = contact.remark
binding.tvContactName.text = "昵称:${contact.nickname}"
} else {
contactNickname = contact.nickname
binding.tvRemark.text = contact.nickname
binding.tvContactName.visibility = View.GONE
}
}
override fun onResume() {
super.onResume()
updateContent()
}
private fun setOnListener() {
@ -74,12 +85,18 @@ class ContactsDetailActivity : BaseActivity<ActivityContactsDetailBinding>() {
putExtra("contactId", contactId)
})
}
binding.ciPermission.setOnClickListener {
startActivity(
Intent(this, ContactPermissionActivity::class.java)
)
}
}
private fun getUserInfo(contactId: String): Contact? {
private fun getContactInfo(contactId: String): Contact {
return contactBox
.query(Contact_.username.equal(contactId))
.build()
.findFirst()
.findFirst()!!
}
}

View File

@ -5,23 +5,18 @@ import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.databinding.ActivityDataSettingBinding
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.ui.fragment.DeleteContactDialogFragment
import com.kaixed.kchat.ui.fragment.LoadingDialogFragment
import com.kaixed.kchat.ui.widget.DeleteContactDialogFragment
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.viewmodel.ContactViewModel
import io.objectbox.Box
class DataSettingActivity : BaseActivity<ActivityDataSettingBinding>(),
DeleteContactDialogFragment.OnContactDeletedListener {
private val contactViewModel by viewModels<ContactViewModel>()
private val contactBox: Box<Contact> by lazy { get().boxFor(Contact::class.java) }
private var contactId = ""
private val loadingDialogFragment by lazy { LoadingDialogFragment.newInstance("加载中...") }
@ -38,16 +33,35 @@ class DataSettingActivity : BaseActivity<ActivityDataSettingBinding>(),
handleIntent(intent)
}
override fun initData() {
}
private fun handleIntent(intent: Intent) {
contactId = intent.getStringExtra("contactId") ?: ""
}
private fun setOnClick() {
binding.ciSetRemarkAndLabel.setOnClickListener {
val intent = Intent(this, SetRemarkActivity::class.java)
val intent = Intent(this, SetRemarkAndLabelActivity::class.java).apply {
putExtra("contactId", contactId)
}
startActivity(intent)
}
binding.ciPermission.setOnClickListener {
val intent = Intent(this, ContactPermissionActivity::class.java).apply {
putExtra("contactId", contactId)
}
startActivity(intent)
}
binding.ciRecommendToContact.setOnClickListener { toast() }
binding.ciComplaints.setOnClickListener {
toast()
}
binding.tvDelete.setOnClickListener {
val deleteDialogFragment = DeleteContactDialogFragment.newInstance(contactId)
deleteDialogFragment.show(supportFragmentManager, "DeleteContactDialogFragment")

View File

@ -34,6 +34,10 @@ class LaunchActivity : BaseActivity<ActivityLaunchBinding>() {
setOnClickListener()
}
override fun initData() {
}
@SuppressLint("DiscouragedApi", "InternalInsetResource")
private fun getStatusBarHeight(): Int {

View File

@ -10,30 +10,21 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.data.objectbox.entity.UserInfo_
import com.kaixed.kchat.databinding.ActivityLoginBinding
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.kaixed.kchat.utils.DensityUtil.dpToPx
import com.kaixed.kchat.utils.DrawableUtil.createDrawable
import com.kaixed.kchat.viewmodel.UserViewModel
import com.tencent.mmkv.MMKV
import io.objectbox.Box
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class LoginActivity : BaseActivity<ActivityLoginBinding>() {
private val userViewModel: UserViewModel by viewModels()
private var previousKeyboardHeight = 0
private var loginByUsername = false
override fun onCreate(savedInstanceState: Bundle?) {
@ -56,6 +47,10 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
getKeyboardHeight()
}
override fun initData() {
}
private fun login(username: String, password: String) {
if (!loginByUsername) {
binding.etUsername.text.toString().length != 11

View File

@ -11,7 +11,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.databinding.ActivityMainBinding
import com.kaixed.kchat.ui.base.BaseActivity
@ -19,10 +19,9 @@ import com.kaixed.kchat.ui.fragment.ContactFragment
import com.kaixed.kchat.ui.fragment.DiscoveryFragment
import com.kaixed.kchat.ui.fragment.HomeFragment
import com.kaixed.kchat.ui.fragment.MineFragment
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.ConstantsUtil.isFirstLaunchApp
import com.kaixed.kchat.utils.Pinyin4jUtil
import com.kaixed.kchat.utils.WidgetUtil
import com.kaixed.kchat.viewmodel.ContactViewModel
import io.objectbox.Box
import kotlinx.coroutines.Dispatchers
@ -33,9 +32,11 @@ import kotlinx.coroutines.launch
class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener {
private val colorPrimary: Int by lazy { ContextCompat.getColor(this, R.color.green) }
private val colorBlack: Int by lazy { ContextCompat.getColor(this, R.color.black) }
private val contactBox: Box<Contact> by lazy { get().boxFor(Contact::class.java) }
private val contactBox: Box<Contact> by lazy { getBoxStore().boxFor(Contact::class.java) }
private val contactViewModel: ContactViewModel by viewModels()
companion object {
@ -65,17 +66,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener {
enableEdgeToEdge()
initView()
initViewPager()
updateSelection(KEY_HOME_FRAGMENT)
initData()
}
private fun initData() {
override fun initData() {
if (isFirstLaunchApp()) {
lifecycleScope.launch(Dispatchers.Main) {
val dialog = WidgetUtil.showLoadingDialog(this@MainActivity, "同步数据中")
dialog.show()
val loadingDialogFragment = LoadingDialogFragment.newInstance("同步数据中")
delay(200)
loadingDialogFragment.showLoading(supportFragmentManager)
contactViewModel.contactListResult.observe(this@MainActivity) { result ->
result.onSuccess {
@ -85,7 +88,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener {
contactViewModel.loadFriendList(getUsername())
delay(2000)
dialog.dismiss()
loadingDialogFragment.dismissLoading()
}
}
}
@ -105,7 +108,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), View.OnClickListener {
}
private fun initViewPager() {
// 使用 FragmentStateAdapter 构造函数
viewPagerAdapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int {
return fragments.size

View File

@ -1,7 +1,6 @@
package com.kaixed.kchat.ui.activity
import android.Manifest
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@ -14,15 +13,15 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.data.objectbox.entity.UserInfo_
import com.kaixed.kchat.databinding.ActivityProfileDetailBinding
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
import com.kaixed.kchat.utils.Constants.AVATAR_URL
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.WidgetUtil
import com.kaixed.kchat.viewmodel.UserViewModel
import com.tencent.mmkv.MMKV
import io.objectbox.Box
@ -34,7 +33,7 @@ import java.io.File
class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) }
private val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor(UserInfo::class.java) }
private val userSessionMMKV by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
@ -76,11 +75,17 @@ class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
}
}
override fun initData() {
}
private fun updateAvatar(uri: Uri) {
val filePath = getPathFromUri(uri)
val dialog = WidgetUtil.showLoadingDialog(this, "正在上传")
dialog.show()
val loadingDialogFragment = LoadingDialogFragment.newInstance("正在上传")
loadingDialogFragment.showLoading(supportFragmentManager)
userViewModel.uploadAvatarResult.observe(this) { result ->
result.onSuccess {
userSessionMMKV.putString(AVATAR_URL, it)
@ -93,8 +98,8 @@ class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
}
}
val file = File(filePath!!)
userViewModel.uploadAvatar(username = getUsername(), file = prepareFilePart(file))
loadingDialogFragment.dismissLoading()
}
private fun prepareFilePart(file: File): MultipartBody.Part {
@ -103,8 +108,6 @@ class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
}
private fun getPathFromUri(uri: Uri): String? {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = contentResolver.query(uri, projection, null, null, null)

View File

@ -14,7 +14,7 @@ import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.databinding.ActivityRegisterBinding
import com.kaixed.kchat.model.request.RegisterRequest
@ -30,7 +30,7 @@ class RegisterActivity : BaseActivity<ActivityRegisterBinding>() {
private val userViewModel: UserViewModel by viewModels()
private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) }
private val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor(UserInfo::class.java) }
override fun inflateBinding(): ActivityRegisterBinding {
return ActivityRegisterBinding.inflate(layoutInflater)
@ -45,6 +45,10 @@ class RegisterActivity : BaseActivity<ActivityRegisterBinding>() {
setOnClickListener()
}
override fun initData() {
}
private fun setOnClickListener() {
binding.ivClose.setOnClickListener {
finish()

View File

@ -6,20 +6,20 @@ import android.os.Looper
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.UserInfo
import com.kaixed.kchat.databinding.ActivityRenameBinding
import com.kaixed.kchat.model.request.UserRequest
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
import com.kaixed.kchat.utils.ConstantsUtil.getNickName
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.WidgetUtil
import com.kaixed.kchat.viewmodel.UserViewModel
import io.objectbox.Box
class RenameActivity : BaseActivity<ActivityRenameBinding>() {
private val userInfoBox: Box<UserInfo> by lazy { get().boxFor(UserInfo::class.java) }
private val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor(UserInfo::class.java) }
private val userViewModel: UserViewModel by viewModels()
@ -36,8 +36,6 @@ class RenameActivity : BaseActivity<ActivityRenameBinding>() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
oldNickname = getNickName()
val nickname = oldNickname
var enable = false
@ -58,10 +56,14 @@ class RenameActivity : BaseActivity<ActivityRenameBinding>() {
}
}
override fun initData() {
oldNickname = getNickName()
}
private fun updateNicknamesByCondition(newNickname: String, username: String) {
binding.ctb.setBtnEnable(false)
val dialog = WidgetUtil.showLoadingDialog(this, "正在保存")
dialog.show()
val dialog: LoadingDialogFragment = LoadingDialogFragment.newInstance("正在保存")
dialog.showLoading(supportFragmentManager)
userViewModel.changeNicknameResult.observe(this) { result ->
result.onSuccess {
@ -76,21 +78,20 @@ class RenameActivity : BaseActivity<ActivityRenameBinding>() {
binding.etNickname.setSelection(binding.etNickname.text.length)
}
Handler(Looper.getMainLooper()).postDelayed({
dialog.dismiss()
toast(if (updateSucceed) "更新成功" else "更新失败")
}, 800)
val userRequest = UserRequest(
username = username,
nickname = newNickname
)
userViewModel.changeNickname(userRequest)
Handler(Looper.getMainLooper()).postDelayed({
dialog.dismissLoading()
toast(if (updateSucceed) "更新成功" else "更新失败")
}, 800)
}
override fun onResume() {
super.onResume()
binding.etNickname.setText(getNickName())
val nickname = getNickName()
binding.etNickname.setText(nickname)
}
}

View File

@ -12,21 +12,23 @@ import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory
import com.kaixed.kchat.databinding.ActivitySearchBinding
import com.kaixed.kchat.model.search.SearchItem
import com.kaixed.kchat.ui.adapter.SearchResultAdapter
import com.kaixed.kchat.ui.base.BaseActivity
class SearchActivity : AppCompatActivity() {
private var binding: ActivitySearchBinding? = null
class SearchActivity : BaseActivity<ActivitySearchBinding>() {
override fun inflateBinding(): ActivitySearchBinding {
return ActivitySearchBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.enableEdgeToEdge()
binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding!!.root)
setOnClickListener()
setupRecycleView()
binding!!.etSearch.addTextChangedListener(
binding.etSearch.addTextChangedListener(
afterTextChanged = { s ->
s?.let {
setupViewVisibility(s.length)
@ -35,9 +37,13 @@ class SearchActivity : AppCompatActivity() {
)
}
override fun initData() {
}
private fun setupViewVisibility(length: Int) {
binding!!.rvSearchResult.visibility = if (length > 0) View.VISIBLE else View.INVISIBLE
binding!!.tvPageSetting.visibility = if (length > 0) View.INVISIBLE else View.VISIBLE
binding.rvSearchResult.visibility = if (length > 0) View.VISIBLE else View.INVISIBLE
binding.tvPageSetting.visibility = if (length > 0) View.INVISIBLE else View.VISIBLE
}
private fun setupRecycleView() {
@ -60,13 +66,13 @@ class SearchActivity : AppCompatActivity() {
}
val searchResultAdapter: SearchResultAdapter = getAdapter()
binding!!.rvSearchResult.adapter = searchResultAdapter
binding.rvSearchResult.adapter = searchResultAdapter
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
binding!!.rvSearchResult.layoutManager = layoutManager
binding.rvSearchResult.layoutManager = layoutManager
}
private fun setOnClickListener() {
binding!!.tvCancel.setOnClickListener { v ->
binding.tvCancel.setOnClickListener { v ->
finish()
}
}

View File

@ -24,8 +24,11 @@ import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.viewmodel.ContactViewModel
class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() {
private var isSearching = false
private lateinit var userItem: User
private lateinit var loadingDialog: Dialog
override fun inflateBinding(): ActivitySearchFriendsBinding {
@ -43,6 +46,10 @@ class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() {
setOnClick()
}
override fun initData() {
}
private fun setOnClick() {
binding.clFriends.setOnClickListener {
val intent = Intent(this, ApplyFriendsDetailActivity::class.java)

View File

@ -1,15 +1,47 @@
package com.kaixed.kchat.ui.activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.data.LocalContact
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.data.objectbox.entity.Contact_
import com.kaixed.kchat.databinding.ActivitySetRemarkAndLabelBinding
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.viewmodel.ContactViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SetRemarkAndLabelActivity : BaseActivity<ActivitySetRemarkAndLabelBinding>() {
private val contactBox by lazy {
getBoxStore().boxFor(Contact::class.java)
}
private var contactId: String? = null
private var hasFocus = false
private var remark = ""
private val contact: Contact? by lazy {
LocalContact.getContactByUsername(contactId!!)
}
private val contactViewModel by lazy { ContactViewModel() }
override fun inflateBinding(): ActivitySetRemarkAndLabelBinding {
return ActivitySetRemarkAndLabelBinding.inflate(layoutInflater)
}
@ -18,5 +50,94 @@ class SetRemarkAndLabelActivity : BaseActivity<ActivitySetRemarkAndLabelBinding>
super.onCreate(savedInstanceState)
enableEdgeToEdge()
updateContent()
setOnListener()
}
}
override fun initData() {
contactId = intent?.getStringExtra("contactId") ?: ""
remark = contact?.remark ?: contact?.nickname ?: ""
}
override fun onResume() {
super.onResume()
updateContent()
}
private fun updateContent() {
binding.etRemark.setText(remark)
updateByFocus()
if (hasFocus) {
binding.etRemark.setSelection(remark.length)
}
}
private fun setOnListener() {
binding.etRemark.setOnFocusChangeListener { _, hasFocus ->
this.hasFocus = hasFocus
updateByFocus()
}
binding.ivClean.setOnClickListener {
remark = ""
binding.etRemark.setText(remark)
binding.ivClean.visibility = View.INVISIBLE
}
binding.ctb.setOnBtnClickListener {
updateRemark()
}
binding.etRemark.addTextChangedListener(afterTextChanged = { text ->
remark = text.toString()
})
binding.cvAddLabel.setOnClickListener { toast() }
binding.cvAddTelephone.setOnClickListener { toast() }
}
private fun hideKeyboard(view: View) {
view.clearFocus()
val imm = this.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(view.windowToken, 0)
}
private fun updateRemark() {
hideKeyboard(binding.etRemark)
val dialog = LoadingDialogFragment.newInstance("正在保存")
dialog.showLoading(supportFragmentManager)
contactViewModel.setRemarkResult.observe(this) { result ->
result.onSuccess {
val deleteIntent = Intent("com.kaixed.kchat.broadcast.UPDATE_CONTACT_LIST")
deleteIntent.putExtra("update", true)
LocalBroadcastManager.getInstance(this).sendBroadcast(deleteIntent)
lifecycleScope.launch {
delay(1000)
dialog.dismissLoading()
finish()
}
}
result.onFailure {
toast(it.message.toString())
dialog.dismissLoading()
}
}
contactViewModel.setRemark(getUsername(), contactId!!, remark)
}
private fun updateByFocus() {
if (hasFocus) {
binding.etRemark.setTextColor(ContextCompat.getColor(this, R.color.black))
binding.ivClean.visibility = View.VISIBLE
binding.ctb.setBtnEnable(true)
} else {
binding.etRemark.setTextColor(Color.parseColor("#ACACAC"))
binding.ivClean.visibility = View.GONE
}
}
}

View File

@ -0,0 +1,46 @@
package com.kaixed.kchat.ui.activity
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import com.kaixed.kchat.databinding.ActivityTestBinding
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.ImageEngines
import com.luck.picture.lib.basic.PictureSelector
import com.luck.picture.lib.config.SelectMimeType
import com.luck.picture.lib.entity.LocalMedia
import com.luck.picture.lib.interfaces.OnResultCallbackListener
class TestActivity : BaseActivity<ActivityTestBinding>() {
override fun inflateBinding(): ActivityTestBinding {
return ActivityTestBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
}
override fun initData() {
PictureSelector.create(this)
.openGallery(SelectMimeType.ofImage())
.setImageEngine(ImageEngines())
.forResult(object : OnResultCallbackListener<LocalMedia?> {
override fun onResult(result: ArrayList<LocalMedia?>) {
result.forEach { localMedia ->
localMedia?.let {
val imagePath = it.realPath
toast("图片路径: $imagePath")
}
}
}
override fun onCancel() {
toast("已取消")
}
})
}
}

View File

@ -1,25 +1,23 @@
package com.kaixed.kchat.ui.adapter
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.core.view.updateMargins
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.drake.spannable.replaceSpan
import com.drake.spannable.span.CenterImageSpan
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox
import com.kaixed.kchat.data.objectbox.entity.Messages
import com.kaixed.kchat.databinding.ChatRecycleItemCustomMineBinding
import com.kaixed.kchat.databinding.ChatRecycleItemCustomOtherBinding
import com.kaixed.kchat.databinding.ChatRecycleItemImageMineBinding
import com.kaixed.kchat.databinding.ChatRecycleItemImageOtherBinding
import com.kaixed.kchat.databinding.ChatRecycleItemTipBinding
import com.kaixed.kchat.databinding.PopwindowsBinding
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.DensityUtil.dpToPx
import com.kaixed.kchat.utils.ImageSpanUtil
import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow
import com.kaixed.kchat.utils.TextUtil
import io.objectbox.Box
import java.util.LinkedList
@ -79,6 +77,23 @@ class ChatAdapter(
)
}
ITEM_TYPE_IMAGE_OTHER -> {
ImageViewOtherHolder(
ChatRecycleItemImageOtherBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}
ITEM_TYPE_IMAGE_MINE -> {
ImageViewMineHolder(
ChatRecycleItemImageMineBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}
else -> {
TipViewHolder(
ChatRecycleItemTipBinding.inflate(
@ -93,6 +108,23 @@ class ChatAdapter(
val singleMessage = messages[position]
when (holder) {
is ImageViewOtherHolder -> {
Glide.with(context).load(singleMessage.content).into(holder.binding.image)
if (position == messages.size - 1) {
holder.binding.tvTimer.visibility = View.VISIBLE
holder.binding.tvTimer.text =
TextUtil.getTimestampString(singleMessage.timestamp)
} else {
if (singleMessage.timestamp - messages[position + 1].timestamp < 1L * 60 * 1000) {
holder.binding.tvTimer.visibility = View.GONE
} else {
holder.binding.tvTimer.visibility = View.VISIBLE
holder.binding.tvTimer.text =
TextUtil.getTimestampString(singleMessage.timestamp)
}
}
}
is TipViewHolder -> {
holder.bindData(singleMessage)
}
@ -113,13 +145,16 @@ class ChatAdapter(
}
holder.binding.tvMessageContent.setOnLongClickListener {
showPopupWindow(
holder.binding.tvMessageContent, singleMessage.senderId == getUsername()
context,
holder.binding.tvMessageContent,
singleMessage.senderId == getUsername()
)
true
}
holder.binding.tvMessageContent.text = ImageSpanUtil.setEmojiSpan(
context, singleMessage.content, dpToPx(18)
)
holder.binding.tvMessageContent.text = singleMessage.content.replaceSpan("[委屈]") {
CenterImageSpan(context, R.drawable.emoji).setDrawableSize(55)
}
}
is CustomViewOtherHolder -> {
@ -138,13 +173,17 @@ class ChatAdapter(
}
holder.binding.tvMessageContent.setOnLongClickListener {
showPopupWindow(
holder.binding.tvMessageContent, singleMessage.senderId == getUsername()
context,
holder.binding.tvMessageContent,
singleMessage.senderId == getUsername()
)
true
}
holder.binding.tvMessageContent.text = ImageSpanUtil.setEmojiSpan(
context, singleMessage.content, dpToPx(18)
)
holder.binding.tvMessageContent.text = singleMessage.content.replaceSpan("[委屈]") {
CenterImageSpan(context, R.drawable.emoji).setDrawableSize(55)
}
}
}
}
@ -166,6 +205,12 @@ class ChatAdapter(
class CustomViewOtherHolder(val binding: ChatRecycleItemCustomOtherBinding) :
RecyclerView.ViewHolder(binding.root)
class ImageViewMineHolder(val binding: ChatRecycleItemImageMineBinding) :
RecyclerView.ViewHolder(binding.root)
class ImageViewOtherHolder(val binding: ChatRecycleItemImageOtherBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemViewType(position: Int): Int {
val message = messages[position]
return if (message.senderId == getUsername()) {
@ -176,100 +221,10 @@ class ChatAdapter(
}
private fun updateDb(message: Messages) {
val messagesBox: Box<Messages> = ObjectBox.get().boxFor(Messages::class.java)
val messagesBox: Box<Messages> = ObjectBox.getBoxStore().boxFor(Messages::class.java)
messagesBox.put(message)
}
override fun getItemCount(): Int = messages.size
private fun showPopupWindow(parentView: View, isMine: Boolean) {
val binding: PopwindowsBinding = PopwindowsBinding.inflate(LayoutInflater.from(context))
val popupWindow: PopupWindow = PopupWindow(
binding.root, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
isFocusable = true
isOutsideTouchable = true
setBackgroundDrawable(ColorDrawable())
}
// 获取屏幕的总宽度
val screenWidth = context.resources.displayMetrics.widthPixels
val screenHeight = context.resources.displayMetrics.heightPixels
val location = IntArray(2)
parentView.getLocationOnScreen(location)
binding.root.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
// popupWindow的长度
val popupWidth = binding.root.measuredWidth
val popupHeight = binding.root.measuredHeight
val parentWidth = parentView.width
val parentHeight = parentView.height
val parentX = location[0]
// 父布局中心点横坐标
val distanceToLeftEdge = parentX + parentView.width / 2
// 中心点距离右侧距离
val distanceToRightEdge = screenWidth - distanceToLeftEdge
binding.ivArrowDown.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val arrowWidth = binding.ivArrowDown.measuredWidth
val isYEnoughSpace = (screenHeight - popupHeight) > popupHeight
val xOffset: Int
val yOffset = if (isYEnoughSpace) -popupHeight - parentHeight + arrowWidth / 3 else 0
val marginStart: Int
var rightEnough = false
var leftEnough = false
if (isYEnoughSpace) {
binding.ivArrowUp.visibility = View.GONE
binding.ivArrowDown.visibility = View.VISIBLE
} else {
binding.ivArrowUp.visibility = View.VISIBLE
binding.ivArrowDown.visibility = View.GONE
}
if (isMine) {
rightEnough = (popupWidth / 2) < distanceToRightEdge
xOffset = if (rightEnough)
(parentWidth - popupWidth) / 2
else
parentWidth - popupWidth + dpToPx(45)
marginStart = popupWidth - (dpToPx(55) + parentWidth / 2)
} else {
leftEnough = (popupWidth / 2) < distanceToLeftEdge
xOffset = if (leftEnough) parentWidth - popupWidth
else -dpToPx(45)
marginStart = dpToPx(35) + parentWidth / 2
}
binding.ivArrowDown.layoutParams =
(binding.ivArrowUp.layoutParams as LinearLayout.LayoutParams).apply {
val enoughSpace = if (isMine) rightEnough else leftEnough
if (enoughSpace) {
gravity = Gravity.CENTER_HORIZONTAL
} else {
updateMargins(left = marginStart)
}
}
popupWindow.showAsDropDown(parentView, xOffset, yOffset)
// binding.ivWithdraw.setOnClickListener {
// messages[position].status = "withdraw"
// updateDb(messages[position])
// notifyItemChanged(position)
// popupWindow.dismiss()
// }
}
}

View File

@ -10,6 +10,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.drake.spannable.replaceSpan
import com.drake.spannable.span.CenterImageSpan
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.entity.Conversation
import com.kaixed.kchat.databinding.ChatMainItemBinding
@ -24,6 +26,7 @@ import com.kaixed.kchat.utils.TextUtil
class ConversationAdapter(
private var chatLists: List<Conversation>, private val context: Context
) : RecyclerView.Adapter<ConversationAdapter.MyViewHolder?>() {
private var onChatListItemClickListener: OnChatListItemClickListener? = null
companion object {
@ -35,8 +38,10 @@ class ConversationAdapter(
}
class MyViewHolder(val binding: ChatMainItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bindData(chatList: Conversation) {
binding.tvContent.text = chatList.lastContent
fun bindData(chatList: Conversation, context: Context) {
binding.tvContent.text = chatList.lastContent.replaceSpan("[委屈]") {
CenterImageSpan(context, R.drawable.emoji).setDrawableSize(55)
}
binding.tvNickname.text = chatList.talkerId
binding.tvTimestamp.text = TextUtil.getTimestampString(chatList.timestamp)
}
@ -74,7 +79,7 @@ class ConversationAdapter(
holder.binding.rlUnreadCount.visibility = View.INVISIBLE
}
holder.bindData(chatLists[position])
holder.bindData(chatLists[position], context)
holder.itemView.setOnClickListener {
onChatListItemClickListener!!.onItemClick(chatLists[position].talkerId)

View File

@ -19,9 +19,16 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = inflateBinding()
setContentView(binding.root)
initData()
}
abstract fun initData()
fun toast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
fun toast() {
Toast.makeText(this, "暂未开发", Toast.LENGTH_SHORT).show()
}
}

View File

@ -13,7 +13,6 @@ import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -21,7 +20,7 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE
import androidx.recyclerview.widget.LinearLayoutManager
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Conversation
import com.kaixed.kchat.data.objectbox.entity.Conversation_
import com.kaixed.kchat.data.objectbox.entity.Messages
@ -32,6 +31,7 @@ import com.kaixed.kchat.service.WebSocketService.LocalBinder
import com.kaixed.kchat.ui.activity.AddFriendsActivity
import com.kaixed.kchat.ui.activity.ContactUpdatesActivity
import com.kaixed.kchat.ui.activity.SearchActivity
import com.kaixed.kchat.ui.activity.TestActivity
import com.kaixed.kchat.ui.adapter.ConversationAdapter
import com.kaixed.kchat.ui.adapter.MyGridAdapter
import com.kaixed.kchat.ui.base.BaseFragment
@ -44,15 +44,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnChatListItemClickLis
private var chatLists: MutableList<Conversation> = mutableListOf()
private var oldLists: MutableList<Conversation> = mutableListOf()
private val conversationAdapter: ConversationAdapter by lazy {
ConversationAdapter(chatLists, context)
}
private val conversationBox: Box<Conversation> by lazy { get().boxFor(Conversation::class.java) }
private val conversationBox: Box<Conversation> by lazy { getBoxStore().boxFor(Conversation::class.java) }
private val messagesBox: Box<Messages> by lazy { get().boxFor(Messages::class.java) }
private val messagesBox: Box<Messages> by lazy { getBoxStore().boxFor(Messages::class.java) }
private var talkerId: String? = null
@ -97,6 +95,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnChatListItemClickLis
}
private fun setOnClick() {
binding.ifvAvatar.setOnClickListener {
startActivity(Intent(context, TestActivity::class.java))
}
binding.ivMessage.setOnClickListener {
}
binding.ivSearch.setOnClickListener {
@ -268,9 +269,15 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnChatListItemClickLis
binding.recycleChatList.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.recycleChatList.adapter = conversationAdapter
binding.recycleChatList.setHasFixedSize(true)
conversationAdapter.setItemListener(this)
}
private fun resetContent() {
flipImage(binding.ivMore)
flipped = false
}
private fun setupGridView() {
initMenuData()
val myGridAdapter = MyGridAdapter(context, items)
@ -282,11 +289,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnChatListItemClickLis
when (position) {
0 -> {}
2 -> {
val intent =
Intent(
context,
AddFriendsActivity::class.java
)
val intent = Intent(context, AddFriendsActivity::class.java)
startActivity(intent)
}
@ -329,6 +332,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnChatListItemClickLis
override fun onResume() {
super.onResume()
if (flipped) {
resetContent()
}
// updateChatLists()
}

View File

@ -8,7 +8,7 @@ import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.kaixed.kchat.R
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Conversation
import com.kaixed.kchat.data.objectbox.entity.Messages
import com.kaixed.kchat.databinding.BottomSheetLayoutBinding
@ -75,7 +75,7 @@ class MyBottomSheetFragment : BottomSheetDialogFragment() {
MMKV.defaultMMKV().putBoolean(USER_LOGIN_STATUS, false)
val boxStore: BoxStore = get()
val boxStore: BoxStore = getBoxStore()
boxStore.boxFor(Conversation::class.java).removeAll()
boxStore.boxFor(Messages::class.java).removeAll()

View File

@ -63,13 +63,21 @@ class CustomItem @JvmOverloads constructor(
private fun setupRightContent(type: String) {
when (type) {
"switch" -> {
binding.ssvSwitch.visibility = View.VISIBLE
binding.sbSwitch.visibility = View.VISIBLE
binding.ivArrowRight.visibility = View.GONE
binding.ivYes.visibility = View.GONE
}
"yes" -> {
binding.sbSwitch.visibility = View.GONE
binding.ivArrowRight.visibility = View.GONE
binding.ivYes.visibility = View.VISIBLE
}
else -> {
binding.ssvSwitch.visibility = View.GONE
binding.sbSwitch.visibility = View.GONE
binding.ivArrowRight.visibility = View.VISIBLE
binding.ivYes.visibility = View.GONE
}
}
}
@ -159,6 +167,10 @@ class CustomItem @JvmOverloads constructor(
}
fun setSwitchChecked(checked: Boolean) {
binding.ssvSwitch.setOn(checked)
binding.sbSwitch.setChecked(checked)
}
fun setYesSelected(selected: Boolean) {
binding.ivYes.visibility = if (selected) View.VISIBLE else View.GONE
}
}

View File

@ -24,6 +24,8 @@ class CustomTitleBar @JvmOverloads constructor(
private var binding: ViewTitleBarBinding =
ViewTitleBarBinding.inflate(LayoutInflater.from(context), this, true)
private var isBtnEnabled: Boolean = false
init {
attrs?.let {
val typedArray = context.obtainStyledAttributes(it, R.styleable.CustomTitleBar, 0, 0)
@ -65,7 +67,12 @@ class CustomTitleBar @JvmOverloads constructor(
binding.ivSetting.setOnClickListener(listener)
}
fun setOnTitleClickListener(listener: OnClickListener?) {
binding.tvTitleName.setOnClickListener(listener)
}
fun setBtnEnable(enable: Boolean) {
isBtnEnabled = enable
val colorEnable = ContextCompat.getColor(context, R.color.white)
val color = Color.parseColor("#BFBFBF")
binding.tvSave.setTextColor(if (enable) colorEnable else color)
@ -73,6 +80,14 @@ class CustomTitleBar @JvmOverloads constructor(
}
fun setOnBtnClickListener(listener: OnClickListener?) {
binding.tvSave.setOnClickListener(listener)
binding.tvSave.setOnClickListener {
if (isBtnEnabled) {
listener?.onClick(it)
}
}
}
fun setTitleName(titleName: String) {
binding.tvTitleName.text = titleName
}
}

View File

@ -1,4 +1,4 @@
package com.kaixed.kchat.ui.fragment
package com.kaixed.kchat.ui.widget
import android.app.Dialog
import android.content.Context

View File

@ -1,4 +1,4 @@
package com.kaixed.kchat.ui.fragment
package com.kaixed.kchat.ui.widget
import android.app.Dialog
import android.graphics.Color
@ -12,11 +12,9 @@ import com.kaixed.kchat.utils.DensityUtil.dpToPx
class LoadingDialogFragment : DialogFragment() {
// 设置加载文本
private var loadingText: String = ""
companion object {
// 通过这个方法设置加载文本,并创建实例
fun newInstance(text: String): LoadingDialogFragment {
val fragment = LoadingDialogFragment()
val bundle = Bundle()
@ -53,11 +51,15 @@ class LoadingDialogFragment : DialogFragment() {
// 显示对话框
fun showLoading(fragmentManager: FragmentManager) {
this.show(fragmentManager, "LoadingDialogFragment")
if (!this.isAdded) { // 避免重复添加
this.show(fragmentManager, "LoadingDialogFragment")
}
}
// 关闭对话框
fun dismissLoading() {
this.dismissAllowingStateLoss()
if (isAdded && !isStateSaved) {
dismissAllowingStateLoss()
}
}
}

View File

@ -1,600 +0,0 @@
package com.kaixed.kchat.ui.widget
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.core.content.ContextCompat
import com.kaixed.kchat.R
/**
* @Author: 7heaven
* @Date: 2024/10/23 19:29
*/
class ShSwitchView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) :
View(context, attrs, defStyle) {
private var innerContentAnimator: ValueAnimator? = null
private var knobExpandAnimator: ValueAnimator? = null
private var knobMoveAnimator: ValueAnimator? = null
private val gestureDetector: GestureDetector
private val gestureListener: SimpleOnGestureListener = object : SimpleOnGestureListener() {
override fun onDown(event: MotionEvent): Boolean {
if (!isEnabled) {
return false
}
preIsOn = isOn
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
knobExpandAnimator!!.setFloatValues(knobExpandRate, 1.0f)
knobExpandAnimator!!.start()
return true
}
override fun onShowPress(event: MotionEvent) {
}
override fun onSingleTapUp(event: MotionEvent): Boolean {
isOn = knobState
if (preIsOn == isOn) {
isOn = !isOn
knobState = !knobState
}
if (knobState) {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
} else {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
return true
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (e2.x > centerX) {
if (!knobState) {
knobState = !knobState
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
}
} else {
if (knobState) {
knobState = !knobState
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
}
}
return true
}
}
private var width = 0
private var height = 0
private var centerX = 0
private var centerY = 0
private var cornerRadius = 0f
private val shadowSpace: Int
private val outerStrokeWidth: Int
private val shadowDrawable: Drawable
private val knobBound: RectF
private var knobMaxExpandWidth = 0f
private var intrinsicKnobWidth = 0f
private var knobExpandRate = 0f
private var knobMoveRate = 0f
private var knobState = false
private var isOn = false
private var preIsOn = false
private val innerContentBound: RectF
private var innerContentRate = 1.0f
private var intrinsicInnerWidth = 0f
private var intrinsicInnerHeight = 0f
private var tintColor: Int
private var tempTintColor: Int
private var colorStep = backgroundColor
private val paint: Paint
private val ovalForPath: RectF
private val roundRectPath: Path
private val tempForRoundRect: RectF
private var dirtyAnimation = false
private var isAttachedToWindow = false
interface OnSwitchStateChangeListener {
fun onSwitchStateChange(isOn: Boolean)
}
var onSwitchStateChangeListener: OnSwitchStateChangeListener? = null
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.ShSwitchView)
tintColor = ta.getColor(R.styleable.ShSwitchView_tintColor, -0x6316b7)
tempTintColor = tintColor
val defaultOuterStrokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1.5f,
context.resources.displayMetrics
).toInt()
val defaultShadowSpace = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
5f,
context.resources.displayMetrics
).toInt()
outerStrokeWidth = ta.getDimensionPixelOffset(
R.styleable.ShSwitchView_outerStrokeWidth,
defaultOuterStrokeWidth
)
shadowSpace =
ta.getDimensionPixelOffset(R.styleable.ShSwitchView_shadowSpace, defaultShadowSpace)
ta.recycle()
knobBound = RectF()
innerContentBound = RectF()
ovalForPath = RectF()
tempForRoundRect = RectF()
paint = Paint(Paint.ANTI_ALIAS_FLAG)
roundRectPath = Path()
gestureDetector = GestureDetector(context, gestureListener)
gestureDetector.setIsLongpressEnabled(false)
setLayerType(LAYER_TYPE_SOFTWARE, null)
initAnimators()
shadowDrawable = ContextCompat.getDrawable(context, R.drawable.shadow)!!
}
private fun initAnimators() {
innerContentAnimator = ValueAnimator.ofFloat(innerContentRate, 1.0f).apply {
duration = COMMON_DURATION
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
(animation as? ValueAnimator)?.let {
setInnerContentRate(it.animatedValue as Float)
}
}
}
knobExpandAnimator = ValueAnimator.ofFloat(knobExpandRate, 1.0f).apply {
duration = COMMON_DURATION
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
(animation as? ValueAnimator)?.let {
setKnobExpandRate(it.animatedValue as Float)
}
}
}
knobMoveAnimator = ValueAnimator.ofFloat(knobMoveRate, 1.0f).apply {
duration = COMMON_DURATION
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
(animation as? ValueAnimator)?.let {
setKnobMoveRate(it.animatedValue as Float)
}
}
}
}
fun setInnerContentRate(rate: Float) {
this.innerContentRate = rate
invalidate()
}
fun getInnerContentRate(): Float {
return this.innerContentRate
}
fun setKnobExpandRate(rate: Float) {
this.knobExpandRate = rate
invalidate()
}
fun getKnobExpandRate(): Float {
return this.knobExpandRate
}
fun setKnobMoveRate(rate: Float) {
this.knobMoveRate = rate
invalidate()
}
fun getKnobMoveRate(): Float {
return knobMoveRate
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
isAttachedToWindow = true
if (dirtyAnimation) {
knobState = this.isOn
if (knobState) {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
} else {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
dirtyAnimation = false
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
isAttachedToWindow = false
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthMeasureSpec = widthMeasureSpec
var heightMeasureSpec = heightMeasureSpec
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
width = MeasureSpec.getSize(widthMeasureSpec)
height = MeasureSpec.getSize(heightMeasureSpec)
//make sure widget remain in a good appearance
if (height.toFloat() / width.toFloat() < 0.33333f) {
height = (width.toFloat() * 0.33333f).toInt()
widthMeasureSpec =
MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec))
heightMeasureSpec =
MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
super.setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
centerX = width / 2
centerY = height / 2
cornerRadius = (centerY - shadowSpace).toFloat()
innerContentBound.left = (outerStrokeWidth + shadowSpace).toFloat()
innerContentBound.top = (outerStrokeWidth + shadowSpace).toFloat()
innerContentBound.right = (width - outerStrokeWidth - shadowSpace).toFloat()
innerContentBound.bottom = (height - outerStrokeWidth - shadowSpace).toFloat()
intrinsicInnerWidth = innerContentBound.width()
intrinsicInnerHeight = innerContentBound.height()
knobBound.left = (outerStrokeWidth + shadowSpace).toFloat()
knobBound.top = (outerStrokeWidth + shadowSpace).toFloat()
knobBound.right = (height - outerStrokeWidth - shadowSpace).toFloat()
knobBound.bottom = (height - outerStrokeWidth - shadowSpace).toFloat()
intrinsicKnobWidth = knobBound.height()
knobMaxExpandWidth = width.toFloat() * 0.7f
if (knobMaxExpandWidth > knobBound.width() * 1.25f) {
knobMaxExpandWidth = knobBound.width() * 1.25f
}
}
fun isOn(): Boolean {
return this.isOn
}
fun setOn(on: Boolean) {
setOn(on, false)
}
fun setOn(on: Boolean, animated: Boolean) {
if (this.isOn == on) {
return
}
if (!isAttachedToWindow && animated) {
dirtyAnimation = true
this.isOn = on
return
}
this.isOn = on
knobState = this.isOn
if (!animated) {
if (on) {
setKnobMoveRate(1.0f)
setInnerContentRate(0.0f)
} else {
setKnobMoveRate(0.0f)
setInnerContentRate(1.0f)
}
setKnobExpandRate(0.0f)
} else {
if (knobState) {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 1.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 0.0f)
innerContentAnimator!!.start()
} else {
knobMoveAnimator!!.setFloatValues(knobMoveRate, 0.0f)
knobMoveAnimator!!.start()
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
}
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
}
fun setTintColor(tintColor: Int) {
this.tintColor = tintColor
tempTintColor = this.tintColor
}
fun getTintColor(): Int {
return this.tintColor
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled) {
return false
}
when (event.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (!knobState) {
innerContentAnimator!!.setFloatValues(innerContentRate, 1.0f)
innerContentAnimator!!.start()
}
knobExpandAnimator!!.setFloatValues(knobExpandRate, 0.0f)
knobExpandAnimator!!.start()
isOn = knobState
if (this@ShSwitchView.onSwitchStateChangeListener != null && isOn != preIsOn) {
onSwitchStateChangeListener!!.onSwitchStateChange(isOn)
}
}
}
return gestureDetector.onTouchEvent(event)
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
if (enabled) {
this.tintColor = tempTintColor
} else {
this.tintColor = this.RGBColorTransform(0.5f, tempTintColor, -0x1)
}
}
public override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//innerContentCalculation begin
var w = intrinsicInnerWidth / 2.0f * innerContentRate
val h = intrinsicInnerHeight / 2.0f * innerContentRate
innerContentBound.left = centerX - w
innerContentBound.top = centerY - h
innerContentBound.right = centerX + w
innerContentBound.bottom = centerY + h
//innerContentCalculation end
//knobExpandCalculation begin
w = intrinsicKnobWidth + (knobMaxExpandWidth - intrinsicKnobWidth) * knobExpandRate
val left = knobBound.left + knobBound.width() / 2 > centerX
if (left) {
knobBound.left = knobBound.right - w
} else {
knobBound.right = knobBound.left + w
}
//knobExpandCalculation end
//knobMoveCalculation begin
val kw = knobBound.width()
w = (width - kw - ((shadowSpace + outerStrokeWidth) * 2)) * knobMoveRate
this.colorStep = RGBColorTransform(knobMoveRate, backgroundColor, tintColor)
knobBound.left = shadowSpace + outerStrokeWidth + w
knobBound.right = knobBound.left + kw
//knobMoveCalculation end
//background
paint.color = colorStep
paint.style = Paint.Style.FILL
drawRoundRect(
shadowSpace.toFloat(),
shadowSpace.toFloat(),
(width - shadowSpace).toFloat(),
(height - shadowSpace).toFloat(),
cornerRadius,
canvas,
paint
)
//innerContent
paint.color = Color.WHITE
canvas.drawRoundRect(
innerContentBound,
innerContentBound.height() / 2,
innerContentBound.height() / 2,
paint
)
//knob
// shadowDrawable.setBounds((int) (knobBound.left - shadowSpace), (int) (knobBound.top - shadowSpace), (int) (knobBound.right + shadowSpace), (int) (knobBound.bottom + shadowSpace));
// shadowDrawable.draw(canvas);
paint.setShadowLayer(
2f,
0f,
(shadowSpace / 2).toFloat(),
if (isEnabled) 0x20000000 else 0x10000000
)
// paint.setColor(isEnabled() ? 0x20000000 : 0x10000000);
// drawRoundRect(knobBound.left, knobBound.top + shadowSpace / 2, knobBound.right, knobBound.bottom + shadowSpace / 2, cornerRadius - outerStrokeWidth, canvas, paint);
//
// paint.setColor(foregroundColor);
canvas.drawRoundRect(
knobBound,
cornerRadius - outerStrokeWidth,
cornerRadius - outerStrokeWidth,
paint
)
paint.setShadowLayer(0f, 0f, 0f, 0)
paint.color = backgroundColor
paint.style = Paint.Style.STROKE
paint.strokeWidth = 1f
canvas.drawRoundRect(
knobBound,
cornerRadius - outerStrokeWidth,
cornerRadius - outerStrokeWidth,
paint
)
}
private fun drawRoundRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
radius: Float,
canvas: Canvas,
paint: Paint
) {
tempForRoundRect.left = left
tempForRoundRect.top = top
tempForRoundRect.right = right
tempForRoundRect.bottom = bottom
canvas.drawRoundRect(tempForRoundRect, radius, radius, paint)
}
//seperate RGB channels and calculate new value for each channel
//ignore alpha channel
private fun RGBColorTransform(progress: Float, fromColor: Int, toColor: Int): Int {
val or = (fromColor shr 16) and 0xFF
val og = (fromColor shr 8) and 0xFF
val ob = fromColor and 0xFF
val nr = (toColor shr 16) and 0xFF
val ng = (toColor shr 8) and 0xFF
val nb = toColor and 0xFF
val rGap = ((nr - or).toFloat() * progress).toInt()
val gGap = ((ng - og).toFloat() * progress).toInt()
val bGap = ((nb - ob).toFloat() * progress).toInt()
return -0x1000000 or ((or + rGap) shl 16) or ((og + gGap) shl 8) or (ob + bGap)
}
companion object {
private const val COMMON_DURATION = 300L
private const val intrinsicWidth = 0
private const val intrinsicHeight = 0
private const val backgroundColor = -0x333334
private const val foregroundColor = -0xa0a0b
}
}

View File

@ -0,0 +1,246 @@
package com.kaixed.kchat.ui.widget
import android.animation.AnimatorSet
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.View
import android.view.animation.AccelerateInterpolator
import androidx.annotation.Nullable
import com.kaixed.kchat.R
class SwitchButton @JvmOverloads constructor(
context: Context,
@Nullable attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
companion object {
/**
* 背景动画的延时与持续时间
*/
private const val BG_ANIMATOR_START_DELAY = 0
private const val BG_ANIMATOR_DURATION = 200
/**
* dp px 的工具方法
*/
fun dip2px(context: Context, dipValue: Float): Int {
val scale = context.resources.displayMetrics.density
return (dipValue * scale + 0.5f).toInt()
}
fun getDisplayMetrics(context: Context): DisplayMetrics {
return context.resources.displayMetrics
}
}
// 属性变量
private var mCheckedBg: Int
private var mUncheckedBg: Int
private var mCurrentBoundBg: Int
private var mSmallCircleMargin: Int
private val mBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
private val mSmallCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = Color.WHITE
}
private var mDefaultWidth: Int
private var mDefaultHeight: Int
private var mSmallCircleRadius = 0
private var mWidth = 0
private var mHeight = 0
private var mBoundRadius = 0f
private var mBoundRect: RectF? = null
private var mSmallCircleCenterX = 0
private var mSmallCircleCenterY = 0
private var isChecked = false
private var mCheckedChangeListener: OnCheckedChangeListener? = null
private var mSmallCircleStartX = 0
private var mSmallCircleEndX = 0
private val mArgbEvaluator = ArgbEvaluator()
private var mOffAnimatorSet: AnimatorSet? = null
private var mOpenAnimatorSet: AnimatorSet? = null
init {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.SwitchButton,
defStyleAttr,
defStyleRes
)
mCheckedBg = typedArray.getColor(R.styleable.SwitchButton_sb_checked_bg, Color.BLUE)
mUncheckedBg = typedArray.getColor(R.styleable.SwitchButton_sb_unchecked_bg, Color.GRAY)
isChecked = typedArray.getBoolean(R.styleable.SwitchButton_sb_checked, false)
mSmallCircleMargin = typedArray.getDimension(
R.styleable.SwitchButton_sb_circle_bg_margin,
dip2px(context, 3f).toFloat()
).toInt()
typedArray.recycle()
mCurrentBoundBg = if (isChecked) mCheckedBg else mUncheckedBg
mDefaultWidth = dip2px(context, 50f)
mDefaultHeight = dip2px(context, 30f)
init()
}
private fun init() {
setOnClickListener {
if (isChecked) {
if (mOffAnimatorSet?.isRunning == true) return@setOnClickListener
off()
} else {
if (mOpenAnimatorSet?.isRunning == true) return@setOnClickListener
open()
}
isChecked = !isChecked
mCheckedChangeListener?.onCheckedChanged(this, isChecked)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mWidth = w
mHeight = h
mSmallCircleRadius = (mHeight - mSmallCircleMargin * 2) / 2
mBoundRadius = (mHeight / 2).toFloat()
mSmallCircleCenterY = mHeight / 2
mSmallCircleStartX = mWidth / 4
mSmallCircleEndX = (mWidth / 4) * 3
updateCheckStatus()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val resultWidth: Int
val resultHeight: Int
if (widthMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.EXACTLY) {
resultWidth = widthSize
resultHeight = heightSize
} else {
resultWidth = mDefaultWidth
resultHeight = mDefaultHeight
}
setMeasuredDimension(resultWidth, resultHeight)
}
override fun onDraw(canvas: Canvas) {
drawBg(canvas, mBgPaint)
drawCircle(canvas, mSmallCirclePaint)
}
private fun drawBg(canvas: Canvas, paint: Paint) {
mBgPaint.color = mCurrentBoundBg
if (mBoundRect == null) {
mBoundRect = RectF(0f, 0f, mWidth.toFloat(), mHeight.toFloat())
}
canvas.drawRoundRect(mBoundRect!!, mBoundRadius, mBoundRadius, paint)
}
private fun drawCircle(canvas: Canvas, paint: Paint) {
canvas.drawCircle(
mSmallCircleCenterX.toFloat(),
mSmallCircleCenterY.toFloat(),
mSmallCircleRadius.toFloat(),
paint
)
}
private fun open() {
val startX = mSmallCircleStartX
val endX = mSmallCircleEndX
val openAnimator = ValueAnimator.ofInt(startX, endX).apply {
duration = 300
addUpdateListener { animation ->
mSmallCircleCenterX = animation.animatedValue as Int
invalidate()
}
}
val bgAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
startDelay = BG_ANIMATOR_START_DELAY.toLong()
duration = BG_ANIMATOR_DURATION.toLong()
interpolator = AccelerateInterpolator()
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
mCurrentBoundBg = mArgbEvaluator.evaluate(fraction, mUncheckedBg, mCheckedBg) as Int
invalidate()
}
}
mOpenAnimatorSet = AnimatorSet().apply {
playTogether(openAnimator, bgAnimator)
start()
}
}
private fun off() {
val startX = mSmallCircleEndX
val endX = mSmallCircleStartX
val offAnimator = ValueAnimator.ofInt(startX, endX).apply {
duration = 200
addUpdateListener { animation ->
mSmallCircleCenterX = animation.animatedValue as Int
invalidate()
}
}
val bgAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
startDelay = BG_ANIMATOR_START_DELAY.toLong()
duration = BG_ANIMATOR_DURATION.toLong()
interpolator = AccelerateInterpolator()
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
mCurrentBoundBg = mArgbEvaluator.evaluate(fraction, mCheckedBg, mUncheckedBg) as Int
invalidate()
}
}
mOffAnimatorSet = AnimatorSet().apply {
playTogether(offAnimator, bgAnimator)
start()
}
}
fun setChecked(checked: Boolean) {
isChecked = checked
updateCheckStatus()
postInvalidate()
}
private fun updateCheckStatus() {
if (isChecked) {
mSmallCircleCenterX = mSmallCircleEndX
mCurrentBoundBg = mCheckedBg
} else {
mSmallCircleCenterX = mSmallCircleStartX
mCurrentBoundBg = mUncheckedBg
}
}
fun isChecked(): Boolean = isChecked
fun setOnCheckedChangeListener(listener: OnCheckedChangeListener) {
mCheckedChangeListener = listener
}
interface OnCheckedChangeListener {
fun onCheckedChanged(button: SwitchButton, isChecked: Boolean)
}
}

View File

@ -0,0 +1,42 @@
package com.kaixed.kchat.utils
import android.content.Context
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.luck.picture.lib.engine.ImageEngine
/**
* @Author: kaixed
* @Date: 2024/11/25 13:50
*/
class ImageEngines: ImageEngine {
override fun loadImage(context: Context?, url: String?, imageView: ImageView?) {
Glide.with(context!!).load(url).into(imageView!!)
}
override fun loadImage(
context: Context?,
imageView: ImageView?,
url: String?,
maxWidth: Int,
maxHeight: Int
) {
Glide.with(context!!).load(url).override(maxWidth, maxHeight).into(imageView!!)
}
override fun loadAlbumCover(context: Context?, url: String?, imageView: ImageView?) {
Glide.with(context!!).load(url).into(imageView!!)
}
override fun loadGridImage(context: Context?, url: String?, imageView: ImageView?) {
Glide.with(context!!).load(url).into(imageView!!)
}
override fun pauseRequests(context: Context?) {
Glide.with(context!!).pauseRequests()
}
override fun resumeRequests(context: Context?) {
Glide.with(context!!).resumeRequests()
}
}

View File

@ -0,0 +1,110 @@
package com.kaixed.kchat.utils
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.core.view.updateMargins
import com.kaixed.kchat.databinding.PopwindowsBinding
import com.kaixed.kchat.utils.DensityUtil.dpToPx
/**
* @Author: kaixed
* @Date: 2024/11/25 15:44
*/
object PopWindowUtil {
fun showPopupWindow(context: Context, parentView: View, isMine: Boolean) {
val binding: PopwindowsBinding = PopwindowsBinding.inflate(LayoutInflater.from(context))
val popupWindow: PopupWindow = PopupWindow(
binding.root, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
isFocusable = true
isOutsideTouchable = true
setBackgroundDrawable(ColorDrawable())
}
// 获取屏幕的总宽度
val screenWidth = context.resources.displayMetrics.widthPixels
val screenHeight = context.resources.displayMetrics.heightPixels
val location = IntArray(2)
parentView.getLocationOnScreen(location)
binding.root.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
// popupWindow的长度
val popupWidth = binding.root.measuredWidth
val popupHeight = binding.root.measuredHeight
val parentWidth = parentView.width
val parentHeight = parentView.height
val parentX = location[0]
// 父布局中心点横坐标
val distanceToLeftEdge = parentX + parentView.width / 2
// 中心点距离右侧距离
val distanceToRightEdge = screenWidth - distanceToLeftEdge
binding.ivArrowDown.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val arrowWidth = binding.ivArrowDown.measuredWidth
val isYEnoughSpace = (screenHeight - popupHeight) > popupHeight
val xOffset: Int
val yOffset = if (isYEnoughSpace) -popupHeight - parentHeight + arrowWidth / 3 else 0
val marginStart: Int
var rightEnough = false
var leftEnough = false
if (isYEnoughSpace) {
binding.ivArrowUp.visibility = View.GONE
binding.ivArrowDown.visibility = View.VISIBLE
} else {
binding.ivArrowUp.visibility = View.VISIBLE
binding.ivArrowDown.visibility = View.GONE
}
if (isMine) {
rightEnough = (popupWidth / 2) < distanceToRightEdge
xOffset = if (rightEnough)
(parentWidth - popupWidth) / 2
else
parentWidth - popupWidth + dpToPx(45)
marginStart = popupWidth - (dpToPx(55) + parentWidth / 2)
} else {
leftEnough = (popupWidth / 2) < distanceToLeftEdge
xOffset = if (leftEnough) parentWidth - popupWidth
else -dpToPx(45)
marginStart = dpToPx(35) + parentWidth / 2
}
binding.ivArrowDown.layoutParams =
(binding.ivArrowUp.layoutParams as LinearLayout.LayoutParams).apply {
val enoughSpace = if (isMine) rightEnough else leftEnough
if (enoughSpace) {
gravity = Gravity.CENTER_HORIZONTAL
} else {
updateMargins(left = marginStart)
}
}
popupWindow.showAsDropDown(parentView, xOffset, yOffset)
// binding.ivWithdraw.setOnClickListener {
// messages[position].status = "withdraw"
// updateDb(messages[position])
// notifyItemChanged(position)
// popupWindow.dismiss()
// }
}
}

View File

@ -1,34 +0,0 @@
package com.kaixed.kchat.utils
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Window
import android.view.WindowManager
import com.kaixed.kchat.databinding.DialogDeleteContactBinding
import com.kaixed.kchat.databinding.DialogLoadingBinding
import com.kaixed.kchat.utils.DensityUtil.dpToPx
/**
* @Author: kaixed
* @Date: 2024/11/5 21:26
*/
object WidgetUtil {
fun showLoadingDialog(context: Context, str: String): Dialog {
val binding = DialogLoadingBinding.inflate(LayoutInflater.from(context))
val dialog = Dialog(context)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setCancelable(false)
dialog.setContentView(binding.root)
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
val params = binding.root.layoutParams
params.height = dpToPx(150)
params.width = dpToPx(150)
binding.root.layoutParams = params
binding.tvLoading.text = str
return dialog
}
}

View File

@ -1,6 +1,6 @@
package com.kaixed.kchat.utils.handle
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.utils.Pinyin4jUtil
import io.objectbox.Box
@ -10,7 +10,8 @@ import io.objectbox.Box
* @Date: 2024/11/19 16:11
*/
object ContactUtil {
private val contactBox: Box<Contact> by lazy { get().boxFor(Contact::class.java) }
private val contactBox: Box<Contact> by lazy { getBoxStore().boxFor(Contact::class.java) }
fun handleContact(contact: Contact) {
val contactLists = getDbContactLists()

View File

@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kaixed.kchat.data.objectbox.ObjectBox.get
import com.kaixed.kchat.data.objectbox.ObjectBox.getBoxStore
import com.kaixed.kchat.data.objectbox.entity.Contact
import com.kaixed.kchat.model.friend.FriendRequestItem
import com.kaixed.kchat.model.search.User
@ -27,8 +27,8 @@ class ContactViewModel : ViewModel() {
_acceptContactRequestResult
// 添加联系人
private val _addContactResult = MutableLiveData<Result<String>>()
val addContactResult: LiveData<Result<String>> = _addContactResult
private val _addContactResult = MutableLiveData<Result<String?>>()
val addContactResult: LiveData<Result<String?>> = _addContactResult
// 搜索联系人
private val _searchContactResult = MutableLiveData<Result<User?>>()
@ -42,6 +42,18 @@ class ContactViewModel : ViewModel() {
private val _deleteContactResult = MutableLiveData<Result<String?>>()
val deleteContactResult: LiveData<Result<String?>> = _deleteContactResult
// 删除联系人
private val _setRemarkResult = MutableLiveData<Result<String?>>()
val setRemarkResult: LiveData<Result<String?>> = _setRemarkResult
// 设置备注
fun setRemark(username: String, contactId: String, remark: String) {
viewModelScope.launch {
val result = contactRepository.setRemark(username, contactId, remark)
_setRemarkResult.postValue(result)
}
}
// 删除联系人
fun deleteContact(username: String, contactId: String) {
viewModelScope.launch {
@ -83,7 +95,7 @@ class ContactViewModel : ViewModel() {
}
fun loadFriendListInDb(): List<Contact> {
val contactBox: Box<Contact> = get().boxFor(Contact::class.java)
val contactBox: Box<Contact> = getBoxStore().boxFor(Contact::class.java)
val sortedContacts = contactBox.query()
.sort { contact1, contact2 ->

View File

@ -24,8 +24,8 @@ class UserViewModel : ViewModel() {
private val userSearchRepo = UserSearchRepository()
private val _loginResult = MutableLiveData<Result<UserInfo>>()
val loginResult: LiveData<Result<UserInfo>> = _loginResult
private val _loginResult = MutableLiveData<Result<UserInfo?>>()
val loginResult: LiveData<Result<UserInfo?>> = _loginResult
fun loginByUsername(username: String, password: String) {
viewModelScope.launch {
@ -41,8 +41,8 @@ class UserViewModel : ViewModel() {
}
}
private val _registerResult = MutableLiveData<Result<Register>>()
val registerResult: LiveData<Result<Register>> = _registerResult
private val _registerResult = MutableLiveData<Result<Register?>>()
val registerResult: LiveData<Result<Register?>> = _registerResult
// 注册请求
fun register(registerRequest: RegisterRequest) {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="1dp" />
<size android:width="1.5dp" />
<!-- <solid android:color="#1772F6"/>-->
<solid android:color="@color/green" />
</shape>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="70dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M512,0a512,512 0,0 0,-512 512,512 512,0 0,0 512,512 512,512 0,0 0,512 -512,512 512,0 0,0 -512,-512z"
android:fillColor="#7D7D7D"/>
<path
android:pathData="M717.2,306.2a36,36 0,0 0,-50.8 0.1L512,461 357.7,306.3a36,36 0,0 0,-51 50.8L461.2,512 306.7,666.9a36,36 0,0 0,51 50.8L512,563l154.3,154.8a35.7,35.7 0,0 0,50.8 0.1,36 36,0 0,0 0.1,-50.9L562.8,512l154.3,-154.9a35.9,35.9 0,0 0,0 -50.9z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="70dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="@color/green"
android:pathData="M925.9,268.8c-17.1,-17.1 -42.7,-17.1 -59.7,0L426.7,708.3l-183.5,-183.5c-17.1,-17.1 -42.7,-12.8 -59.7,4.3 -17.1,17.1 -17.1,42.7 0,59.7l213.3,213.3c17.1,17.1 42.7,17.1 59.7,0l469.3,-469.3c17.1,-21.3 17.1,-46.9 0,-64z" />
</vector>

View File

@ -8,57 +8,12 @@
android:fitsSystemWindows="true"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
<com.kaixed.kchat.ui.widget.CustomTitleBar
android:id="@+id/ctb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_left_arrow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_contact_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:text="contact"
android:textColor="@color/black"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/tv_contact_status"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_back" />
<TextView
android:id="@+id/tv_contact_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="在线"
android:textColor="@color/black"
android:textSize="8sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/iv_back"
app:layout_constraintStart_toStartOf="@id/tv_contact_name"
app:layout_constraintTop_toBottomOf="@id/tv_contact_name" />
<ImageView
android:id="@+id/iv_more"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginEnd="5dp"
android:src="@drawable/ic_more"
app:layout_constraintBottom_toBottomOf="@id/iv_back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_back" />
</androidx.constraintlayout.widget.ConstraintLayout>
app:titleIcon="@drawable/ic_more"
app:titleName="contact" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycle_chat_list"

View File

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray"
android:fitsSystemWindows="true"
android:orientation="vertical"
tools:context=".ui.activity.ChatDetailActivity">
<com.kaixed.kchat.ui.widget.CustomTitleBar
@ -15,259 +17,79 @@
app:layout_constraintTop_toTopOf="parent"
app:titleName="聊天信息" />
<ImageView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="centerCrop"
android:src="@drawable/bac_contacts_detail"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/ifv_avatar"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_marginStart="15dp"
android:layout_marginTop="15dp"
android:src="@drawable/ic_avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ctb"
app:round="8dp" />
<TextView
android:id="@+id/tv_contact_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="kaixed"
app:layout_constraintEnd_toEndOf="@id/ifv_avatar"
app:layout_constraintStart_toStartOf="@id/ifv_avatar"
app:layout_constraintTop_toBottomOf="@id/ifv_avatar" />
android:background="@color/white"
android:paddingVertical="10dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/ifv_avatar"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginStart="15dp"
android:src="@drawable/ic_avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:roundPercent="0.2" />
<TextView
android:id="@+id/tv_contact_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="kaixed"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="@id/ifv_avatar"
app:layout_constraintStart_toStartOf="@id/ifv_avatar"
app:layout_constraintTop_toBottomOf="@id/ifv_avatar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/tv_contact_name">
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:itemName="查找聊天记录" />
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="#E5E5E5" />
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:isBottomDividerVisible="true"
app:itemName="消息免打扰"
app:rightType="switch" />
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:isBottomDividerVisible="true"
app:itemName="置顶聊天"
app:rightType="switch" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp">
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemName="提醒"
app:rightType="switch" />
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:itemName="设置当前聊天背景" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="查找聊天记录"
android:textColor="@color/black"
android:textSize="16sp" />
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:itemName="清空聊天记录" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/icon_arrow_right" />
</RelativeLayout>
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:itemName="投诉" />
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="#E5E5E5" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="消息免打扰"
android:textColor="@color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/icon_arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="15dp"
android:background="#E5E5E5" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="置顶聊天"
android:textColor="@color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/icon_arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="15dp"
android:background="#E5E5E5" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="提醒"
android:textColor="@color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/icon_arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="#E5E5E5" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="设置当前聊天背景"
android:textColor="@color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/icon_arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="#E5E5E5" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="清空聊天记录"
android:textColor="@color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/icon_arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="#E5E5E5" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingStart="15dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="投诉"
android:textColor="@color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/icon_arrow_right" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E5E5E5" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,69 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#EDEDED"
android:fitsSystemWindows="true"
android:orientation="vertical"
tools:context=".ui.activity.ContactPermissionActivity">
<com.kaixed.kchat.ui.widget.CustomTitleBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:titleName="朋友权限" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#E3E3E3" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="5dp"
android:paddingVertical="10dp"
android:text="设置朋友权限"
android:textSize="13sp" />
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:isBottomDividerVisible="true"
app:itemName="聊天、朋友圈、运动等"
app:rightType="yes" />
<com.kaixed.kchat.ui.widget.CustomItem
android:id="@+id/ci_chat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemName="仅聊天"
app:rightType="yes" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="5dp"
android:paddingVertical="10dp"
android:text="朋友圈和状态"
android:textSize="13sp" />
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:isBottomDividerVisible="true"
app:itemName="不让他看我"
app:rightType="switch" />
<com.kaixed.kchat.ui.widget.CustomItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemName="不看他"
app:rightType="switch" />
</LinearLayout>

View File

@ -95,6 +95,7 @@
app:itemName="设置备注和标签" />
<com.kaixed.kchat.ui.widget.CustomItem
android:id="@+id/ci_permission"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:isTopDividerVisible="true"

View File

@ -28,11 +28,13 @@
app:itemName="设置备注和标签" />
<com.kaixed.kchat.ui.widget.CustomItem
android:id="@+id/ci_permission"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemName="朋友权限" />
<com.kaixed.kchat.ui.widget.CustomItem
android:id="@+id/ci_recommend_to_contact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin"
@ -61,6 +63,7 @@
app:rightType="switch" />
<com.kaixed.kchat.ui.widget.CustomItem
android:id="@+id/ci_complaints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemName="投诉" />

View File

@ -6,7 +6,7 @@
android:background="@color/white"
android:orientation="vertical">
<!-- <FrameLayout-->
<!-- <FrameLayout-->
<!-- android:id="@+id/fl_main"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="0dp"-->
@ -28,8 +28,8 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/gray"
android:layout_height="@dimen/divider_height"
android:background="#D2D2D2"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout

View File

@ -57,13 +57,33 @@
app:cardElevation="0dp"
app:cardMaxElevation="0dp">
<EditText
android:id="@+id/et_remark"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F7F7F7"
android:paddingHorizontal="16dp"
android:textCursorDrawable="@drawable/cursor" />
android:background="#F7F7F7">
<EditText
android:id="@+id/et_remark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F7F7F7"
android:paddingHorizontal="16dp"
android:textCursorDrawable="@drawable/cursor"
android:textSize="15sp" />
<ImageView
android:id="@+id/iv_clean"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_clean"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<TextView
@ -75,6 +95,7 @@
android:textSize="13sp" />
<androidx.cardview.widget.CardView
android:id="@+id/cv_add_label"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginHorizontal="15dp"
@ -118,6 +139,7 @@
android:textSize="13sp" />
<androidx.cardview.widget.CardView
android:id="@+id/cv_add_telephone"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginHorizontal="15dp"

View File

@ -7,6 +7,7 @@
android:fitsSystemWindows="true"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -37,7 +37,7 @@
android:minHeight="35dp"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:text="你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好你好"
android:text="haha"
android:textColor="@color/white"
android:textColorHighlight="#CCCCCC"
android:textIsSelectable="true"

View File

@ -6,14 +6,32 @@
android:paddingVertical="10dp">
<TextView
android:layout_width="wrap_content"
android:id="@+id/tv_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="这是一个小tip"
android:textColor="@color/black"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:gravity="center_horizontal"
android:paddingTop="20dp"
android:text="昨天"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/ifv_avatar"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginVertical="20dp"
android:layout_marginStart="10dp"
android:src="@drawable/ic_avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_timer"
app:roundPercent="0.3" />
<ImageView
android:id="@+id/image"
android:layout_width="150dp"
android:layout_height="100dp"
android:layout_marginStart="10dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toEndOf="@id/ifv_avatar"
app:layout_constraintTop_toTopOf="@id/ifv_avatar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -33,7 +33,7 @@
android:layout_marginStart="15dp"
android:text="xxx"
android:textColor="@color/black"
android:textSize="17sp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_item_left_icon"
app:layout_constraintTop_toTopOf="parent"
@ -92,13 +92,31 @@
android:layout_centerInParent="true"
android:src="@drawable/icon_arrow_right" />
<com.kaixed.kchat.ui.widget.ShSwitchView
android:id="@+id/ssv_switch"
android:layout_width="57dp"
android:layout_height="38dp"
<com.kaixed.kchat.ui.widget.SwitchButton
android:id="@+id/sb_switch"
android:layout_width="48dp"
android:layout_height="26dp"
android:layout_centerInParent="true"
android:layout_gravity="center_vertical"
app:sb_checked="false"
android:visibility="gone"
app:tintColor="@color/green" />
app:sb_checked_bg="@color/green"
app:sb_circle_bg_margin="3.5dp"
app:sb_unchecked_bg="#EAEAEA" />
<!-- <com.kaixed.kchat.ui.widget.ShSwitchView-->
<!-- android:id="@+id/ssv_switch"-->
<!-- android:layout_width="57dp"-->
<!-- android:layout_height="38dp"-->
<!-- app:tintColor="@color/green" />-->
<ImageView
android:id="@+id/iv_yes"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerInParent="true"
android:src="@drawable/ic_yes"
android:visibility="invisible" />
</RelativeLayout>
<View

View File

@ -36,4 +36,15 @@
<attr name="outerStrokeWidth" format="reference|dimension" />
<attr name="shadowSpace" format="reference|dimension" />
</declare-styleable>
<declare-styleable name="SwitchButton">
<!-- 关闭时的背景颜色 -->
<attr name="sb_unchecked_bg" format="color" />
<!-- 打开时的背景颜色 -->
<attr name="sb_checked_bg" format="color" />
<!-- 开关小圆距离开关背景的距离 -->
<attr name="sb_circle_bg_margin" format="dimension" />
<!-- 是否默认打开 -->
<attr name="sb_checked" format="boolean" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@ -17,11 +17,13 @@ activity = "1.9.3"
constraintlayout = "2.2.0"
mmkv = "1.3.9"
okhttp = "4.12.0"
pictureselector = "v3.11.2"
pinyin4j = "2.5.1"
preference = "1.2.1"
retrofit = "2.11.0"
shapedrawable = "3.2"
shapeview = "9.2"
spannable = "1.2.7"
therouter = "1.2.2"
window = "1.3.0"
#noinspection GradleDependency
@ -31,14 +33,17 @@ objectbox = "4.0.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
compress = { module = "io.github.lucksiege:compress", version.ref = "pictureselector" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesCore" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptorVersion" }
pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref = "pictureselector" }
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" }
shapedrawable = { module = "com.github.getActivity:ShapeDrawable", version.ref = "shapedrawable" }
shapeview = { module = "com.github.getActivity:ShapeView", version.ref = "shapeview" }
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" }
objectbox-kotlin = { group = "io.objectbox", name = "objectbox-kotlin", version.ref = "objectbox" }
@ -59,6 +64,7 @@ mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
preference = { module = "androidx.preference:preference", version.ref = "preference" }
therouter = { module = "cn.therouter:router", version.ref = "therouter" }
ucrop = { module = "io.github.lucksiege:ucrop", version.ref = "pictureselector" }
window = { module = "androidx.window:window", version.ref = "window" }
[plugins]