diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 267e9e7..d9f63b2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,9 +72,6 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.android) - implementation(libs.shapeview) - implementation(libs.shapedrawable) - implementation(libs.pinyin4j) implementation(libs.retrofit) @@ -92,6 +89,11 @@ dependencies { // 自定义spannable implementation(libs.spannable) + // Navigation Component 依赖 + implementation("androidx.navigation:navigation-fragment-ktx:2.8.4") + implementation("androidx.navigation:navigation-ui-ktx:2.8.4") + + // implementation(libs.therouter) // ksp(libs.therouter.ksp) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 83b441a..7dbfa8b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,12 @@ android:theme="@style/Theme.KChatAndroid" android:usesCleartextTraffic="true" tools:targetApi="31"> + + diff --git a/app/src/main/java/com/kaixed/kchat/data/LocalDatabase.kt b/app/src/main/java/com/kaixed/kchat/data/LocalDatabase.kt index 11b6060..0bfa817 100644 --- a/app/src/main/java/com/kaixed/kchat/data/LocalDatabase.kt +++ b/app/src/main/java/com/kaixed/kchat/data/LocalDatabase.kt @@ -15,6 +15,10 @@ import io.objectbox.query.QueryBuilder object LocalDatabase { private val contactBox by lazy { getBox(Contact::class.java) } + fun isMyFriend(contactId: String): Boolean { + return getContactByUsername(contactId) != null + } + fun getContactByUsername(contactId: String): Contact? { return contactBox.query(Contact_.username.equal(contactId)).build().findFirst() } diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/ApplyFriendsDetailActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/ApplyFriendsDetailActivity.kt index 141532e..de521d6 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/activity/ApplyFriendsDetailActivity.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/ApplyFriendsDetailActivity.kt @@ -6,8 +6,7 @@ import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import com.bumptech.glide.Glide -import com.kaixed.kchat.data.model.friend.FriendRequestItem -import com.kaixed.kchat.data.model.search.User +import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.databinding.ActivityApplyFriendsDetailBinding import com.kaixed.kchat.ui.base.BaseActivity @@ -17,9 +16,7 @@ class ApplyFriendsDetailActivity : BaseActivity(), OnItemClickListener, enableEdgeToEdge() firstLoadData() - initView() - setListener() - bindWebSocketService() - setPanelChange() - getKeyBoardVisibility() } @@ -302,7 +297,6 @@ class ChatActivity : BaseActivity(), OnItemClickListener, ) } - binding.etInput.addTextChangedListener( afterTextChanged = { _ -> val isInputEmpty = binding.etInput.text.toString().isEmpty() @@ -362,11 +356,7 @@ class ChatActivity : BaseActivity(), OnItemClickListener, val emoji = strings!![position] // 使用 ImageSpanUtil 插入表情符号 insertEmoji( - context, - editable, - binding.etInput.textSize.toInt(), - emoji, - index + context, editable, binding.etInput.textSize.toInt(), emoji, index ) // 设置新的光标位置 binding.etInput.setSelection(index + emoji.length) @@ -585,7 +575,6 @@ class ChatActivity : BaseActivity(), OnItemClickListener, when (position) { 0 -> { selectPicture() - // 相册 } } } diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt index ba0f155..c3b6eda 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt @@ -22,6 +22,7 @@ import com.kaixed.kchat.utils.ConstantsUtil import com.kaixed.kchat.utils.ConstantsUtil.getAvatarUrl import com.kaixed.kchat.utils.ConstantsUtil.getNickName import com.kaixed.kchat.utils.DensityUtil.dp2Px +import com.kaixed.kchat.utils.TextUtil import kotlin.math.max import kotlin.math.min import kotlin.random.Random @@ -129,7 +130,8 @@ class FriendCircleActivity : BaseActivity() { private fun setContent() { binding.tvNickname.text = getNickName() - Glide.with(this).load(getAvatarUrl()).into(binding.ifvAvatar) + val avatarUrl = TextUtil.extractDimensionsAndPrefix(getAvatarUrl())?.first + Glide.with(this).load(avatarUrl).into(binding.ifvAvatar) } private fun setupRecyclerView() { diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/LoginActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/LoginActivity.kt index 593e7af..b10fd71 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/activity/LoginActivity.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/LoginActivity.kt @@ -6,6 +6,8 @@ import android.graphics.Rect import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.view.View +import android.widget.EditText import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.core.content.ContextCompat @@ -27,19 +29,25 @@ class LoginActivity : BaseActivity() { private var loginByUsername = false + private lateinit var etUsername: EditText + + private lateinit var etPassword: EditText + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - binding.etUsername.addTextChangedListener(textWatcher) - binding.etPassword.addTextChangedListener(textWatcher) + setupEdittext() + + etUsername.addTextChangedListener(textWatcher) + etPassword.addTextChangedListener(textWatcher) initView() setListener() binding.tvLogin.setOnClickListener { - val username = binding.etUsername.text.toString().trim() - val password = binding.etPassword.text.toString().trim() + val username = etUsername.text.toString().trim() + val password = etPassword.text.toString().trim() login(username, password) } @@ -47,13 +55,18 @@ class LoginActivity : BaseActivity() { getKeyboardHeight() } + private fun setupEdittext() { + etUsername = if (loginByUsername) binding.etUsername else binding.etTelephone + etPassword = binding.etPassword + } + override fun initData() { } private fun login(username: String, password: String) { if (!loginByUsername) { - binding.etUsername.text.toString().length != 11 + etUsername.text.toString().length != 11 toast("请输入正确的手机号码") return } @@ -91,8 +104,12 @@ class LoginActivity : BaseActivity() { } private fun setView() { + setupEdittext() + binding.tvTitle.text = if (loginByUsername) "用户名登录" else "手机号登录" - binding.etUsername.hint = if (loginByUsername) "请填写用户名" else "请填写手机号" + binding.etTelephone.visibility = if (loginByUsername) View.INVISIBLE else View.VISIBLE + binding.etUsername.visibility = if (loginByUsername) View.VISIBLE else View.INVISIBLE + binding.tvUsername.text = if (loginByUsername) "用户名" else "手机号" binding.tvChangeLoginWay.text = if (loginByUsername) "手机号登录" else "用户名登录" binding.tvTip.text = @@ -127,7 +144,7 @@ class LoginActivity : BaseActivity() { override fun afterTextChanged(s: Editable) { val isInputValid = - binding.etUsername.text.isNotEmpty() && binding.etPassword.text.isNotEmpty() + etUsername.text.isNotEmpty() && etPassword.text.isNotEmpty() binding.tvLogin.apply { setTextColor( diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/MainActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/MainActivity.kt index 3ae42a3..9547feb 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/activity/MainActivity.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/MainActivity.kt @@ -2,6 +2,8 @@ package com.kaixed.kchat.ui.activity import android.os.Bundle import android.view.View +import android.widget.ImageView +import android.widget.TextView import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.core.content.ContextCompat @@ -10,9 +12,8 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide import com.kaixed.kchat.R -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.kchat.data.local.box.ObjectBox.getBox import com.kaixed.kchat.data.local.entity.Contact import com.kaixed.kchat.databinding.ActivityMainBinding import com.kaixed.kchat.ui.base.BaseActivity @@ -30,22 +31,25 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch -class MainActivity : BaseActivity(), View.OnClickListener { +class MainActivity : BaseActivity() { 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 by lazy { getBoxStore().boxFor(Contact::class.java) } + private val contactBox: Box by lazy { getBox(Contact::class.java) } private val contactViewModel: ContactViewModel by viewModels() - companion object { - private const val KEY_HOME_FRAGMENT = 0 - private const val KEY_CONTACT_FRAGMENT = 1 - private const val KEY_DISCOVERY_FRAGMENT = 2 - private const val KEY_MINE_FRAGMENT = 3 - private const val TAG = "MainActivity" + private val navBinding by lazy { binding.bottomNavContainer } + + private val navItems by lazy { + listOf( + NavItem(navBinding.ivHome, navBinding.tvHome), + NavItem(navBinding.ivContact, navBinding.tvContact), + NavItem(navBinding.ivDiscovery, navBinding.tvDiscovery), + NavItem(navBinding.ivMine, navBinding.tvMine) + ) } private val fragments = listOf( @@ -69,9 +73,6 @@ class MainActivity : BaseActivity(), View.OnClickListener { initView() initViewPager() - - updateSelection(KEY_HOME_FRAGMENT) - } override fun initData() { @@ -94,18 +95,17 @@ class MainActivity : BaseActivity(), View.OnClickListener { } } + private fun initView() { - binding.clHome.setOnClickListener(this) - binding.clHome.tag = KEY_HOME_FRAGMENT + navItems.forEachIndexed { index, navItem -> + navItem.container.setOnClickListener { + updateSelection(index) + } + } + } - binding.clContact.setOnClickListener(this) - binding.clContact.tag = KEY_CONTACT_FRAGMENT - - binding.clDiscovery.setOnClickListener(this) - binding.clDiscovery.tag = KEY_DISCOVERY_FRAGMENT - - binding.clMine.setOnClickListener(this) - binding.clMine.tag = KEY_MINE_FRAGMENT + fun updatePosition(position: Int) { + updateSelection(position) } private fun initViewPager() { @@ -128,46 +128,17 @@ class MainActivity : BaseActivity(), View.OnClickListener { }) } - override fun onClick(view: View) { - val key = view.tag as Int - binding.viewPager.currentItem = key - updateSelection(key) - } - - private fun updateSelection(selectedKey: Int) { - clearSelection() - when (selectedKey) { - KEY_HOME_FRAGMENT -> { - binding.ivHome.isSelected = true - binding.tvHome.setTextColor(colorPrimary) - } - - KEY_CONTACT_FRAGMENT -> { - binding.ivContact.isSelected = true - binding.tvContact.setTextColor(colorPrimary) - } - - KEY_DISCOVERY_FRAGMENT -> { - binding.ivDiscovery.isSelected = true - binding.tvDiscovery.setTextColor(colorPrimary) - } - - KEY_MINE_FRAGMENT -> { - binding.ivMine.isSelected = true - binding.tvMine.setTextColor(colorPrimary) - } + private fun updateSelection(position: Int) { + binding.viewPager.currentItem = position + navItems.forEachIndexed { index, navItem -> + navItem.imageView.isSelected = index == position + navItem.textView.setTextColor(if (index == position) colorPrimary else colorBlack) } } - private fun clearSelection() { - binding.ivHome.isSelected = false - binding.ivContact.isSelected = false - binding.ivDiscovery.isSelected = false - binding.ivMine.isSelected = false - - binding.tvHome.setTextColor(colorBlack) - binding.tvContact.setTextColor(colorBlack) - binding.tvDiscovery.setTextColor(colorBlack) - binding.tvMine.setTextColor(colorBlack) - } + data class NavItem( + val imageView: ImageView, + val textView: TextView, + val container: View = imageView.parent as View + ) } diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt index 76358f8..49c177f 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt @@ -23,6 +23,7 @@ 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.TextUtil.extractDimensionsAndPrefix import com.kaixed.kchat.viewmodel.UserViewModel import com.tencent.mmkv.MMKV import io.objectbox.Box @@ -89,7 +90,8 @@ class ProfileDetailActivity : BaseActivity() { userViewModel.uploadAvatarResult.observe(this) { result -> result.onSuccess { - Glide.with(this).downloadOnly().load(it).submit() + val a = extractDimensionsAndPrefix(it) + Glide.with(this).downloadOnly().load(a!!.first).submit() userSessionMMKV.putString(AVATAR_URL, it) toast("上传成功") binding.ciAvatar.setItemIcon(uri) @@ -98,10 +100,10 @@ class ProfileDetailActivity : BaseActivity() { result.onFailure { toast("上传失败") } + loadingDialogFragment.dismissLoading() } val file = File(filePath!!) userViewModel.uploadAvatar(username = getUsername(), file = prepareFilePart(file)) - loadingDialogFragment.dismissLoading() } private fun prepareFilePart(file: File): MultipartBody.Part { @@ -174,7 +176,9 @@ class ProfileDetailActivity : BaseActivity() { val userInfo = userInfoBox.query(UserInfo_.username.equal(username)).build().findFirst() if (userInfo != null) { binding.ciNickname.setItemDesc(userInfo.nickname) - binding.ciAvatar.setItemIcon(userInfo.avatarUrl) + val avatarUrl = + extractDimensionsAndPrefix(userInfo.avatarUrl)?.first ?: userInfo.avatarUrl + binding.ciAvatar.setItemIcon(avatarUrl) } } } diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/RegisterActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/RegisterActivity.kt index fa1f3e4..86a9a1a 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/activity/RegisterActivity.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/RegisterActivity.kt @@ -2,6 +2,7 @@ package com.kaixed.kchat.ui.activity import android.content.Intent import android.graphics.Color +import android.graphics.Typeface import android.os.Bundle import android.text.Editable import android.text.SpannableString @@ -13,16 +14,15 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.core.content.ContextCompat import androidx.core.widget.addTextChangedListener +import com.drake.spannable.replaceSpan +import com.drake.spannable.span.HighlightSpan import com.kaixed.kchat.R -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore -import com.kaixed.kchat.data.local.entity.UserInfo -import com.kaixed.kchat.databinding.ActivityRegisterBinding import com.kaixed.kchat.data.model.request.RegisterRequest +import com.kaixed.kchat.databinding.ActivityRegisterBinding import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.utils.DensityUtil.dpToPx import com.kaixed.kchat.utils.DrawableUtil.createDrawable import com.kaixed.kchat.viewmodel.UserViewModel -import io.objectbox.Box class RegisterActivity : BaseActivity() { @@ -30,8 +30,6 @@ class RegisterActivity : BaseActivity() { private val userViewModel: UserViewModel by viewModels() - private val userInfoBox: Box by lazy { getBoxStore().boxFor(UserInfo::class.java) } - override fun inflateBinding(): ActivityRegisterBinding { return ActivityRegisterBinding.inflate(layoutInflater) } @@ -56,7 +54,6 @@ class RegisterActivity : BaseActivity() { binding.tvContinue.setOnClickListener { onContinueClick() - } } @@ -122,7 +119,7 @@ class RegisterActivity : BaseActivity() { binding.etUsername.text.toString().isNotEmpty() && binding.etPassword.text.toString().isNotEmpty() - tvContinueEnable = allFieldsFilled + tvContinueEnable = allFieldsFilled && binding.smoothCheckBox.isChecked if (allFieldsFilled) { setContinueButtonState( @@ -157,15 +154,13 @@ class RegisterActivity : BaseActivity() { } } - val spannableString = - SpannableString("我已阅读并同意《软件许可及服务协议》\n本页面收集的信息仅用于注册账户") - spannableString.setSpan( - clickableSpan, - 7, - 18, - SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE - ) - binding.tvTip.text = spannableString + binding.tvTip.text = + "我已阅读并同意《软件许可及服务协议》\n本页面收集的信息仅用于注册账户".replaceSpan("《软件许可及服务协议》") { + HighlightSpan("#576B95", Typeface.defaultFromStyle(Typeface.NORMAL)) { + toast() + } + } + binding.tvTip.highlightColor = Color.TRANSPARENT binding.tvTip.movementMethod = LinkMovementMethod.getInstance() } diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt index 44138a2..1df2493 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt @@ -1,26 +1,22 @@ package com.kaixed.kchat.ui.activity -import android.app.Dialog -import android.content.Context import android.content.Intent import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.SpannableString import android.text.Spanned import android.text.style.ForegroundColorSpan -import android.view.LayoutInflater import android.view.View -import android.view.Window import android.view.inputmethod.InputMethodManager import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.core.widget.addTextChangedListener import com.bumptech.glide.Glide +import com.kaixed.kchat.data.LocalDatabase import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.databinding.ActivitySearchFriendsBinding -import com.kaixed.kchat.databinding.DialogLoadingBinding import com.kaixed.kchat.ui.base.BaseActivity +import com.kaixed.kchat.ui.widget.LoadingDialogFragment import com.kaixed.kchat.viewmodel.ContactViewModel class SearchFriendsActivity : BaseActivity() { @@ -29,7 +25,7 @@ class SearchFriendsActivity : BaseActivity() { private lateinit var userItem: SearchUser - private lateinit var loadingDialog: Dialog + private lateinit var loadingDialog: LoadingDialogFragment override fun inflateBinding(): ActivitySearchFriendsBinding { return ActivitySearchFriendsBinding.inflate(layoutInflater) @@ -52,26 +48,27 @@ class SearchFriendsActivity : BaseActivity() { private fun setOnClick() { binding.clFriends.setOnClickListener { - val intent = Intent(this, ApplyFriendsDetailActivity::class.java) - intent.putExtra("user", userItem) + Intent(this, ApplyFriendsDetailActivity::class.java).apply { + putExtra("user", userItem) + } + startActivity(intent) } } private fun initView() { - binding.clFriends.visibility = View.GONE - binding.tvTitle.visibility = View.GONE + binding.clFriends.visibility = View.INVISIBLE + binding.tvTitle.visibility = View.INVISIBLE binding.etSearch.requestFocus() binding.etSearch.addTextChangedListener(afterTextChanged = { it?.let { + binding.tvNothing.visibility = View.INVISIBLE if (it.isEmpty()) { - binding.clFriends.visibility = View.GONE - binding.tvTitle.visibility = View.GONE - binding.clSearchUser.visibility = View.GONE + binding.clFriends.visibility = View.INVISIBLE + binding.clSearchUser.visibility = View.INVISIBLE } else { binding.clSearchUser.visibility = View.VISIBLE - binding.tvNothing.visibility = View.INVISIBLE val spannableString = SpannableString("搜索:$it") val colorSpan = ForegroundColorSpan(Color.parseColor("#2BA245")) @@ -94,27 +91,11 @@ class SearchFriendsActivity : BaseActivity() { }, 200) binding.clSearchUser.setOnClickListener { - loadingDialog = showLoadingDialog(this) + loadingDialog = LoadingDialogFragment.newInstance("正在搜索中...") + loadingDialog.showLoading(supportFragmentManager) val username = binding.etSearch.text.toString() - contactViewModel.searchContactResult.observe(this) { result -> - loadingDialog.dismiss() - result.onSuccess { - it?.let { - userItem = it - if (::userItem.isInitialized) { - setVisibility(true) - setContent() - } - } - } - - result.onFailure { - setVisibility(false) - binding.tvNothing.visibility = View.VISIBLE - } - } - contactViewModel.searchContact(username) + searchUser(username) } binding.tvCancel.setOnClickListener { @@ -122,26 +103,43 @@ class SearchFriendsActivity : BaseActivity() { } } - private fun showLoadingDialog(context: Context): 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)) - dialog.show() - return dialog + private fun searchUser(username: String) { + contactViewModel.searchContactResult.observe(this) { result -> + result.onSuccess { + it?.let { + if (LocalDatabase.isMyFriend(it.username)) { + val intent = + Intent(this, ContactsDetailActivity::class.java).apply { + putExtra("contactId", it.username) + } + startActivity(intent) + } else { + userItem = it + updateView(true) + setContent(it) + } + } + } + + result.onFailure { + updateView(false) + } + + loadingDialog.dismissLoading() + } + contactViewModel.searchContact(username) } - private fun setContent() { + private fun setContent(userItem: SearchUser) { binding.tvNickname.text = userItem.nickname binding.tvSignature.text = userItem.signature Glide.with(this).load(userItem.avatarUrl).into(binding.ivAvatar) } - private fun setVisibility(isSearchClick: Boolean) { - binding.clFriends.visibility = if (isSearchClick) View.VISIBLE else View.GONE - binding.tvTitle.visibility = if (isSearchClick) View.VISIBLE else View.GONE - binding.clSearchUser.visibility = if (isSearchClick) View.GONE else View.VISIBLE + private fun updateView(isSearchedUser: Boolean) { + binding.tvNothing.visibility = if (isSearchedUser) View.GONE else View.VISIBLE + binding.clFriends.visibility = if (isSearchedUser) View.VISIBLE else View.INVISIBLE + binding.tvTitle.visibility = if (isSearchedUser) View.VISIBLE else View.INVISIBLE + binding.clSearchUser.visibility = if (isSearchedUser) View.INVISIBLE else View.VISIBLE } } diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/SettingActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/SettingActivity.kt new file mode 100644 index 0000000..9849530 --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/SettingActivity.kt @@ -0,0 +1,22 @@ +package com.kaixed.kchat.ui.activity + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import com.kaixed.kchat.databinding.ActivitySettingBinding +import com.kaixed.kchat.ui.base.BaseActivity + +class SettingActivity : BaseActivity() { + + override fun inflateBinding(): ActivitySettingBinding { + return ActivitySettingBinding.inflate(layoutInflater) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + } + + override fun initData() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaixed/kchat/ui/activity/setting/AboutActivity.kt b/app/src/main/java/com/kaixed/kchat/ui/activity/setting/AboutActivity.kt new file mode 100644 index 0000000..33fd3f3 --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/activity/setting/AboutActivity.kt @@ -0,0 +1,26 @@ +package com.kaixed.kchat.ui.activity.setting + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import com.kaixed.kchat.databinding.ActivityAboutBinding +import com.kaixed.kchat.ui.base.BaseActivity +import com.kaixed.kchat.ui.widget.LoadingDialogFragment + +class AboutActivity : BaseActivity() { + + override fun inflateBinding(): ActivityAboutBinding { + return ActivityAboutBinding.inflate(layoutInflater) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + } + + override fun initData() { + binding.ciCheckUpdate.setOnClickListener { + val loadingDialog = LoadingDialogFragment.newInstance("检查中...") + loadingDialog.showLoading(supportFragmentManager) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaixed/kchat/ui/adapter/ChatAdapter.kt b/app/src/main/java/com/kaixed/kchat/ui/adapter/ChatAdapter.kt index 3791f18..218018b 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/adapter/ChatAdapter.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/adapter/ChatAdapter.kt @@ -1,21 +1,27 @@ package com.kaixed.kchat.ui.adapter import android.content.Context +import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.updateLayoutParams 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.local.box.ObjectBox +import com.kaixed.kchat.data.local.box.ObjectBox.getBox import com.kaixed.kchat.data.local.entity.Messages import com.kaixed.kchat.databinding.ChatRecycleItemCustomNormalBinding import com.kaixed.kchat.databinding.ChatRecycleItemImageNormalBinding import com.kaixed.kchat.databinding.ChatRecycleItemTipBinding import com.kaixed.kchat.utils.ConstantsUtil.getUsername +import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow +import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility -import com.kaixed.kchat.utils.ViewUtil.changeView +import com.kaixed.kchat.utils.ViewUtil.setViewVisibility import io.objectbox.Box import java.util.LinkedList @@ -78,23 +84,62 @@ class ChatAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val singleMessage = messages[position] - when (holder) { is CustomViewHolder -> { holder.bindData(singleMessage) - changeTimerVisibility(position, messages, holder.binding.tvTimer, singleMessage) } is ImageViewHolder -> { holder.bindData(singleMessage) - changeTimerVisibility(position, messages, holder.binding.tvTimer, singleMessage) } is TipViewHolder -> { holder.bindData(singleMessage) } - } + (holder as? HasTimer)?.let { + changeTimerVisibility(position, messages, it.getTimerView(), singleMessage) + } + handleLongClick(holder, position, singleMessage) + } + + private fun handleLongClick(holder: RecyclerView.ViewHolder, position: Int, message: Messages) { + val isMine = message.senderId == getUsername() + val contentView = when (holder) { + is CustomViewHolder -> if (isMine) holder.binding.tvMsgContentMine else holder.binding.tvMsgContent + is ImageViewHolder -> if (isMine) holder.binding.imageMine else holder.binding.image + else -> null + } + + contentView?.setOnLongClickListener { + Log.d("haha", "长按了:${message.content} position: $position") + val popupWindow = showPopupWindow(context, it, message.senderId == getUsername()) + val deleteButton = popupWindow.contentView.findViewById(R.id.tv_delete) + deleteButton.setOnClickListener { + deleteMessage(position, message) + Log.d("haha", "长按并删除了了:${message.content} position: $position") + popupWindow.dismiss() + } + true + } + } + + private fun deleteMessage(position: Int, message: Messages) { + // 从数据库删除 + val messagesBox: Box = getBox(Messages::class.java) + messagesBox.remove(message.msgLocalId) + + // 更新数据源并通知 RecyclerView + messages.removeAt(position) + notifyItemRemoved(position) + + if (position != messages.size) { // 如果移除的是最后一个,忽略 + notifyItemRangeChanged(position, messages.size - position); + } + } + + interface HasTimer { + fun getTimerView(): TextView } class TipViewHolder(val binding: ChatRecycleItemTipBinding) : @@ -108,38 +153,65 @@ class ChatAdapter( } } - class CustomViewHolder(val binding: ChatRecycleItemCustomNormalBinding) : - RecyclerView.ViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root), HasTimer { fun bindData(message: Messages) { val sender = message.senderId == getUsername() - binding.tvMessageContent.text = message.content.replaceSpan("[委屈]") { + + setViewVisibility( + parentView = binding.root, + sender = sender, + avatarId = binding.ifvAvatar.id, + avatarMineId = binding.ifvAvatarMine.id, + contentId = binding.tvMsgContent.id, + contentMineId = binding.tvMsgContentMine.id + ) + + val contentView = if (sender) binding.tvMsgContentMine else binding.tvMsgContent + + contentView.text = message.content.replaceSpan("[委屈]") { CenterImageSpan(binding.root.context, R.drawable.emoji).setDrawableSize(55) } - - changeView( - parentView = binding.root, - sender = sender, - avatarId = binding.ifvAvatar.id, - contentId = binding.tvMessageContent.id - ) } + + override fun getTimerView(): TextView = binding.tvTimer } - class ImageViewHolder(val binding: ChatRecycleItemImageNormalBinding) : - RecyclerView.ViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root), HasTimer { fun bindData(message: Messages) { - Glide.with(binding.root.context).load(message.content) - .placeholder(R.drawable.bac_contacts_detail).into(binding.image) val sender = message.senderId == getUsername() - changeView( + setViewVisibility( parentView = binding.root, sender = sender, avatarId = binding.ifvAvatar.id, - contentId = binding.image.id + avatarMineId = binding.ifvAvatarMine.id, + contentId = binding.image.id, + contentMineId = binding.imageMine.id ) + val imageView = if (sender) binding.imageMine else binding.image + loadImage(message, imageView) } + + private fun loadImage(message: Messages, imageView: ImageView) { + val result = extractDimensionsAndPrefix(message.content) + result?.let { + val (url, width, height) = it + imageView.apply { + updateLayoutParams { + this.height = height + this.width = width + } + } + Glide.with(binding.root.context).load(url) + .placeholder(R.drawable.image_loading).into(imageView) + } ?: run { + Glide.with(binding.root.context).load(message.content) + .placeholder(R.drawable.image_loading).into(imageView) + } + } + + override fun getTimerView(): TextView = binding.tvTimer } override fun getItemViewType(position: Int): Int { @@ -148,7 +220,7 @@ class ChatAdapter( } private fun updateDb(message: Messages) { - val messagesBox: Box = ObjectBox.getBoxStore().boxFor(Messages::class.java) + val messagesBox: Box = getBox(Messages::class.java) messagesBox.put(message) } diff --git a/app/src/main/java/com/kaixed/kchat/ui/base/BaseFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/base/BaseFragment.kt index daf6c58..28087c5 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/base/BaseFragment.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/base/BaseFragment.kt @@ -29,6 +29,8 @@ abstract class BaseFragment : Fragment() { abstract fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?): VB +// abstract fun isSetStatusBarPadding(): Boolean + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupStatusBarPadding(binding.root) diff --git a/app/src/main/java/com/kaixed/kchat/ui/fragment/HomeFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/fragment/HomeFragment.kt index 7b10a46..394ec74 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/fragment/HomeFragment.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/fragment/HomeFragment.kt @@ -22,7 +22,6 @@ import android.widget.Toast import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE import androidx.core.view.updateLayoutParams -import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import com.kaixed.kchat.R import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore @@ -43,7 +42,6 @@ import com.kaixed.kchat.ui.base.BaseFragment import com.kaixed.kchat.ui.i.OnDialogFragmentClickListener import com.kaixed.kchat.ui.i.OnItemListener import com.kaixed.kchat.ui.widget.HomeDialogFragment -import com.kaixed.kchat.viewmodel.HomeViewModel import io.objectbox.Box class HomeFragment : BaseFragment(), OnItemListener, @@ -59,8 +57,6 @@ class HomeFragment : BaseFragment(), OnItemListener, private var talkerId: String? = null - private val homeViewModel: HomeViewModel by viewModels() - private val items: List by lazy { getHomeItems() } private var webSocketService: WebSocketService? = null diff --git a/app/src/main/java/com/kaixed/kchat/ui/fragment/MineFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/fragment/MineFragment.kt index a5163b1..cf72207 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/fragment/MineFragment.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/fragment/MineFragment.kt @@ -9,10 +9,12 @@ import com.bumptech.glide.Glide import com.kaixed.kchat.R import com.kaixed.kchat.databinding.FragmentMineBinding import com.kaixed.kchat.ui.activity.ProfileDetailActivity +import com.kaixed.kchat.ui.activity.SettingActivity import com.kaixed.kchat.ui.base.BaseFragment -import com.kaixed.kchat.ui.widget.MyBottomSheetFragment import com.kaixed.kchat.utils.ConstantsUtil import com.kaixed.kchat.utils.ConstantsUtil.getAvatarUrl +import com.kaixed.kchat.utils.ConstantsUtil.getUsername +import com.kaixed.kchat.utils.TextUtil class MineFragment : BaseFragment() { @@ -32,11 +34,7 @@ class MineFragment : BaseFragment() { } binding.ciSetting.setOnClickListener { - val bottomSheetFragment = MyBottomSheetFragment() - bottomSheetFragment.show( - requireActivity().supportFragmentManager, - bottomSheetFragment.tag - ) + startActivity(Intent(requireContext(), SettingActivity::class.java)) } binding.ciSetting.setRedTipVisibility(true) @@ -50,7 +48,10 @@ class MineFragment : BaseFragment() { private fun updateContent() { val nickname = ConstantsUtil.getNickName() binding.tvNickname.text = nickname - Glide.with(requireContext()).load(getAvatarUrl()) + val kid = "kid: ${getUsername()}" + binding.tvId.text = kid + val avatarUrl = TextUtil.extractDimensionsAndPrefix(getAvatarUrl())?.first + Glide.with(requireContext()).load(avatarUrl) .placeholder(R.drawable.ic_default_avatar) .error(R.drawable.ic_default_avatar) .into(binding.ifvAvatar) diff --git a/app/src/main/java/com/kaixed/kchat/ui/fragment/SettingsFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/fragment/SettingsFragment.kt new file mode 100644 index 0000000..2db9f2c --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/fragment/SettingsFragment.kt @@ -0,0 +1,148 @@ +package com.kaixed.kchat.ui.fragment + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.navigation.NavOptions +import androidx.navigation.fragment.findNavController +import com.kaixed.kchat.R +import com.kaixed.kchat.databinding.FragmentSettingsBinding +import com.kaixed.kchat.ui.activity.setting.AboutActivity +import com.kaixed.kchat.ui.base.BaseFragment +import com.kaixed.kchat.ui.widget.MyBottomSheetFragment + + +class SettingsFragment : BaseFragment(), View.OnClickListener { + override fun inflateBinding( + inflater: LayoutInflater, + container: ViewGroup? + ): FragmentSettingsBinding { + return FragmentSettingsBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setListener() + } + + private fun setListener() { + binding.itemAccountSecurity.setOnClickListener(this) + binding.itemTeenMode.setOnClickListener(this) + binding.itemCareMode.setOnClickListener(this) + binding.itemNewMessageNotification.setOnClickListener(this) + binding.itemChat.setOnClickListener(this) + binding.itemDevice.setOnClickListener(this) + binding.itemGeneral.setOnClickListener(this) + binding.textPrivacy.setOnClickListener(this) + binding.itemFriendPermission.setOnClickListener(this) + binding.itemPersonalInfoPermission.setOnClickListener(this) + binding.itemPersonalInfoCollection.setOnClickListener(this) + binding.itemThirdPartyInfoSharing.setOnClickListener(this) + binding.itemPlugin.setOnClickListener(this) + binding.itemAboutKchat.setOnClickListener(this) + binding.itemHelpFeedback.setOnClickListener(this) + binding.tvSwitchAccount.setOnClickListener(this) + binding.tvLogout.setOnClickListener(this) + } + + override fun onClick(v: View?) { + when (v?.id) { + R.id.item_account_security -> { + + val navOptions = NavOptions.Builder() + .setEnterAnim(R.anim.push_in_from_right) // 从右侧推入 + .setExitAnim(R.anim.push_out_to_left) // 从左侧推出 + .setPopEnterAnim(R.anim.push_in_from_left) // 从左侧推入 + .setPopExitAnim(R.anim.push_out_to_right) // 从右侧推出 + .build() + + findNavController().navigate(R.id.action_settings_to_security, null, navOptions) + } + + R.id.item_teen_mode -> { + // 青少年模式点击 + Toast.makeText(context, "青少年模式点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_care_mode -> { + // 关怀模式点击 + Toast.makeText(context, "关怀模式点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_new_message_notification -> { + // 新消息通知点击 + Toast.makeText(context, "新消息通知点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_chat -> { + // 聊天点击 + Toast.makeText(context, "聊天点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_device -> { + // 设备点击 + Toast.makeText(context, "设备点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_general -> { + // 通用点击 + Toast.makeText(context, "通用点击", Toast.LENGTH_SHORT).show() + } + + R.id.text_privacy -> { + // 隐私点击 + Toast.makeText(context, "隐私点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_friend_permission -> { + // 朋友权限点击 + Toast.makeText(context, "朋友权限点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_personal_info_permission -> { + // 个人信息与权限点击 + Toast.makeText(context, "个人信息与权限点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_personal_info_collection -> { + // 个人信息收集清单点击 + Toast.makeText(context, "个人信息收集清单点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_third_party_info_sharing -> { + // 第三方信息共享清单点击 + Toast.makeText(context, "第三方信息共享清单点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_plugin -> { + // 插件点击 + Toast.makeText(context, "插件点击", Toast.LENGTH_SHORT).show() + } + + R.id.item_about_kchat -> { + startActivity(Intent(context, AboutActivity::class.java)) + } + + R.id.item_help_feedback -> { + // 帮助与反馈点击 + Toast.makeText(context, "帮助与反馈点击", Toast.LENGTH_SHORT).show() + } + + R.id.tv_switch_account -> { + // 切换账号点击 + Toast.makeText(context, "切换账号点击", Toast.LENGTH_SHORT).show() + } + + R.id.tv_logout -> { + val bottomSheetFragment = MyBottomSheetFragment() + bottomSheetFragment.show(parentFragmentManager, bottomSheetFragment.tag) + } + + else -> { + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/AccountSecurityFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/AccountSecurityFragment.kt new file mode 100644 index 0000000..97757a9 --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/AccountSecurityFragment.kt @@ -0,0 +1,27 @@ +package com.kaixed.kchat.ui.fragment.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.kaixed.kchat.databinding.FragmentAccountSecurityBinding +import com.kaixed.kchat.ui.base.BaseFragment + + +class AccountSecurityFragment : BaseFragment() { + override fun inflateBinding( + inflater: LayoutInflater, + container: ViewGroup? + ): FragmentAccountSecurityBinding { + return FragmentAccountSecurityBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.ctbTitleBar.setOnBackClickListener { + findNavController().navigateUp() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/ChatFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/ChatFragment.kt new file mode 100644 index 0000000..6019572 --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/ChatFragment.kt @@ -0,0 +1,60 @@ +package com.kaixed.kchat.ui.fragment.settings + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.kaixed.kchat.R + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * Use the [ChatFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class ChatFragment : Fragment() { + // TODO: Rename and change types of parameters + private var param1: String? = null + private var param2: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + param1 = it.getString(ARG_PARAM1) + param2 = it.getString(ARG_PARAM2) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_chat, container, false) + } + + companion object { + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param param1 Parameter 1. + * @param param2 Parameter 2. + * @return A new instance of fragment ChatFragment. + */ + // TODO: Rename and change types and number of parameters + @JvmStatic + fun newInstance(param1: String, param2: String) = + ChatFragment().apply { + arguments = Bundle().apply { + putString(ARG_PARAM1, param1) + putString(ARG_PARAM2, param2) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/NewMessageFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/NewMessageFragment.kt new file mode 100644 index 0000000..2b14f3d --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/fragment/settings/NewMessageFragment.kt @@ -0,0 +1,60 @@ +package com.kaixed.kchat.ui.fragment.settings + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.kaixed.kchat.R + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * Use the [NewMessageFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class NewMessageFragment : Fragment() { + // TODO: Rename and change types of parameters + private var param1: String? = null + private var param2: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + param1 = it.getString(ARG_PARAM1) + param2 = it.getString(ARG_PARAM2) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_new_message, container, false) + } + + companion object { + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param param1 Parameter 1. + * @param param2 Parameter 2. + * @return A new instance of fragment NewMessageFragment. + */ + // TODO: Rename and change types and number of parameters + @JvmStatic + fun newInstance(param1: String, param2: String) = + NewMessageFragment().apply { + arguments = Bundle().apply { + putString(ARG_PARAM1, param1) + putString(ARG_PARAM2, param2) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaixed/kchat/ui/widget/CustomTitleBar.kt b/app/src/main/java/com/kaixed/kchat/ui/widget/CustomTitleBar.kt index a1b9edb..12649e9 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/widget/CustomTitleBar.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/widget/CustomTitleBar.kt @@ -67,6 +67,10 @@ class CustomTitleBar @JvmOverloads constructor( } } + fun setOnBackClickListener(listener: OnClickListener?) { + binding.ivBack.setOnClickListener(listener) + } + fun setOnSettingClickListener(listener: OnClickListener?) { binding.ivSetting.setOnClickListener(listener) } diff --git a/app/src/main/java/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt b/app/src/main/java/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt index c405bae..ca5b19e 100644 --- a/app/src/main/java/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt +++ b/app/src/main/java/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt @@ -1,9 +1,10 @@ package com.kaixed.kchat.ui.widget +import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Bundle -import android.text.util.Linkify import android.view.LayoutInflater import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/com/kaixed/kchat/ui/widget/ReBoundScrollView.kt b/app/src/main/java/com/kaixed/kchat/ui/widget/ReBoundScrollView.kt new file mode 100644 index 0000000..6752a85 --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/widget/ReBoundScrollView.kt @@ -0,0 +1,129 @@ +package com.kaixed.kchat.ui.widget + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.animation.Animation +import android.view.animation.TranslateAnimation +import android.widget.ScrollView + +class ReBoundScrollView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ScrollView(context, attrs, defStyleAttr) { + + private var mEnableTopRebound = true + private var mEnableBottomRebound = true + private var mOnReboundEndListener: OnReboundEndListener? = null + private var mContentView: View? = null + private val mRect = Rect() + + init { + overScrollMode = View.OVER_SCROLL_NEVER + } + + override fun onFinishInflate() { + super.onFinishInflate() + mContentView = getChildAt(0) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + mContentView?.let { + mRect.set(it.left, it.top, it.right, it.bottom) + } + } + + fun setOnReboundEndListener(onReboundEndListener: OnReboundEndListener): ReBoundScrollView { + this.mOnReboundEndListener = onReboundEndListener + return this + } + + fun setEnableTopRebound(enableTopRebound: Boolean): ReBoundScrollView { + this.mEnableTopRebound = enableTopRebound + return this + } + + fun setEnableBottomRebound(enableBottomRebound: Boolean): ReBoundScrollView { + this.mEnableBottomRebound = enableBottomRebound + return this + } + + private var lastY = 0 + private var rebound = false + private var reboundDirection = 0 // <0: bottom rebound, >0: top rebound, 0: no rebound + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + mContentView?.let { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + lastY = ev.y.toInt() + } + + MotionEvent.ACTION_MOVE -> { + if (!isScrollToTop() && !isScrollToBottom()) { + lastY = ev.y.toInt() + return super.dispatchTouchEvent(ev) + } + val deltaY = (ev.y - lastY).toInt() + + if ((!mEnableTopRebound && deltaY > 0) || (!mEnableBottomRebound && deltaY < 0)) { + return super.dispatchTouchEvent(ev) + } + + val offset = (deltaY * 0.48).toInt() + it.layout(mRect.left, mRect.top + offset, mRect.right, mRect.bottom + offset) + rebound = true + } + + MotionEvent.ACTION_UP -> { + if (!rebound) return super.dispatchTouchEvent(ev) + reboundDirection = it.top - mRect.top + val animation = + TranslateAnimation(0f, 0f, it.top.toFloat(), mRect.top.toFloat()) + animation.duration = 300 + animation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation?) {} + + override fun onAnimationEnd(animation: Animation?) { + mOnReboundEndListener?.let { + when { + reboundDirection > 0 -> it.onReboundTopComplete() + reboundDirection < 0 -> it.onReboundBottomComplete() + } + } + reboundDirection = 0 + } + + override fun onAnimationRepeat(animation: Animation?) {} + }) + it.startAnimation(animation) + it.layout(mRect.left, mRect.top, mRect.right, mRect.bottom) + rebound = false + } + } + } + return super.dispatchTouchEvent(ev) + } + + override fun setFillViewport(fillViewport: Boolean) { + super.setFillViewport(true) // 默认填充 ScrollView 或者在 XML 布局中设置 fillViewport 属性 + } + + private fun isScrollToTop(): Boolean { + return scrollY == 0 + } + + private fun isScrollToBottom(): Boolean { + mContentView?.let { + return it.height <= height + scrollY + } + return false + } + + interface OnReboundEndListener { + fun onReboundTopComplete() + fun onReboundBottomComplete() + } +} diff --git a/app/src/main/java/com/kaixed/kchat/ui/widget/SmoothCheckBox.kt b/app/src/main/java/com/kaixed/kchat/ui/widget/SmoothCheckBox.kt new file mode 100644 index 0000000..a8e7ef6 --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/ui/widget/SmoothCheckBox.kt @@ -0,0 +1,342 @@ +package com.kaixed.kchat.ui.widget + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Point +import android.os.Bundle +import android.os.Parcelable +import android.util.AttributeSet +import android.view.View +import android.view.animation.LinearInterpolator +import android.widget.Checkable +import com.kaixed.kchat.R + +/** + * Author : andy + * Date : 16/1/21 11:28 + * Email : andyxialm@gmail.com + * Github : github.com/andyxialm + * Description : A custom CheckBox with animation for Android + */ +class SmoothCheckBox @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr), Checkable { + + private val KEY_INSTANCE_STATE = "InstanceState" + + private val COLOR_TICK = Color.WHITE + private val COLOR_UNCHECKED = Color.WHITE + private val COLOR_CHECKED = Color.parseColor("#07C160") + private val COLOR_FLOOR_UNCHECKED = Color.parseColor("#DFDFDF") + + private val DEF_DRAW_SIZE = 25 + private val DEF_ANIM_DURATION = 300 + + private lateinit var mPaint: Paint + private lateinit var mTickPaint: Paint + private lateinit var mFloorPaint: Paint + private lateinit var mTickPath: Path + + private lateinit var mTickPoints: Array + private lateinit var mCenterPoint: Point + + private var mLeftLineDistance = 0f + private var mRightLineDistance = 0f + private var mDrewDistance = 0f + private var mScaleVal = 1.0f + private var mFloorScale = 1.0f + + private var mWidth = 0 + private var mAnimDuration = DEF_ANIM_DURATION + private var mStrokeWidth = 0 + private var mCheckedColor = COLOR_CHECKED + private var mUnCheckedColor = COLOR_UNCHECKED + private var mFloorColor = COLOR_FLOOR_UNCHECKED + private var mFloorUnCheckedColor = COLOR_FLOOR_UNCHECKED + + private var mChecked = false + private var mTickDrawing = false + private var mListener: OnCheckedChangeListener? = null + + init { + init(attrs) + } + + private fun init(attrs: AttributeSet?) { + val ta = context.obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox) + val tickColor = ta.getColor(R.styleable.SmoothCheckBox_color_tick, COLOR_TICK) + mAnimDuration = ta.getInt(R.styleable.SmoothCheckBox_duration, DEF_ANIM_DURATION) + mFloorColor = + ta.getColor(R.styleable.SmoothCheckBox_color_unchecked_stroke, COLOR_FLOOR_UNCHECKED) + mCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_checked, COLOR_CHECKED) + mUnCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked, COLOR_UNCHECKED) + mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SmoothCheckBox_stroke_width, dpToPx(0)) + ta.recycle() + + mFloorUnCheckedColor = mFloorColor + + mTickPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeCap = Paint.Cap.ROUND + color = tickColor + } + + mFloorPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = mFloorColor + } + + mPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = mCheckedColor + } + + mTickPath = Path() + mCenterPoint = Point() + mTickPoints = Array(3) { Point() } + + setOnClickListener { + toggle() + mTickDrawing = false + mDrewDistance = 0f + if (isChecked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + } + } + + private fun dpToPx(dp: Int): Int = (dp * resources.displayMetrics.density).toInt() + + override fun onSaveInstanceState(): Parcelable { + val bundle = Bundle() + bundle.putParcelable(KEY_INSTANCE_STATE, super.onSaveInstanceState()) + bundle.putBoolean(KEY_INSTANCE_STATE, isChecked) + return bundle + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is Bundle) { + val bundle = state + val isChecked = bundle.getBoolean(KEY_INSTANCE_STATE) + setChecked(isChecked) + super.onRestoreInstanceState(bundle.getParcelable(KEY_INSTANCE_STATE)) + return + } + super.onRestoreInstanceState(state) + } + + override fun isChecked(): Boolean = mChecked + + override fun toggle() { + setChecked(!isChecked()) + } + + override fun setChecked(checked: Boolean) { + mChecked = checked + reset() + invalidate() + mListener?.onCheckedChanged(this, mChecked) + } + + /** + * checked with animation + * @param checked checked + * @param animate change with animation + */ + fun setChecked(checked: Boolean, animate: Boolean) { + if (animate) { + mTickDrawing = false + mChecked = checked + mDrewDistance = 0f + if (checked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + mListener?.onCheckedChanged(this, mChecked) + } else { + setChecked(checked) + } + } + + private fun reset() { + mTickDrawing = true + mFloorScale = 1.0f + mScaleVal = if (isChecked) 0f else 1.0f + mFloorColor = if (isChecked) mCheckedColor else mFloorUnCheckedColor + mDrewDistance = if (isChecked) (mLeftLineDistance + mRightLineDistance) else 0f + } + + private fun measureSize(measureSpec: Int): Int { + val defSize = dpToPx(DEF_DRAW_SIZE) + val specSize = MeasureSpec.getSize(measureSpec) + val specMode = MeasureSpec.getMode(measureSpec) + + return when (specMode) { + MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> minOf(defSize, specSize) + MeasureSpec.EXACTLY -> specSize + else -> defSize + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec)) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + mWidth = measuredWidth + mStrokeWidth = (if (mStrokeWidth == 0) measuredWidth / 10 else mStrokeWidth).coerceIn(3, measuredWidth / 5) + mCenterPoint.x = mWidth / 2 + mCenterPoint.y = measuredHeight / 2 + + // 调整 tick 的坐标,放置在左边中间偏下位置 + mTickPoints[0].x = (mCenterPoint.x / 2) // 水平左边中间 + mTickPoints[0].y = (mCenterPoint.y ) // 垂直偏下 + + mTickPoints[1].x = mTickPoints[0].x + measuredWidth / 4 // 水平方向向右偏移 + mTickPoints[1].y = (mCenterPoint.y + measuredHeight / 6) // 垂直偏下 + + mTickPoints[2].x = mTickPoints[1].x + measuredWidth / 4 // 水平方向继续向右 + mTickPoints[2].y = (mCenterPoint.y - measuredHeight / 6) // 稍微向上偏移 + + // 计算线段的距离 + mLeftLineDistance = Math.sqrt( + Math.pow((mTickPoints[1].x - mTickPoints[0].x).toDouble(), 2.0) + + Math.pow((mTickPoints[1].y - mTickPoints[0].y).toDouble(), 2.0) + ).toFloat() + + mRightLineDistance = Math.sqrt( + Math.pow((mTickPoints[2].x - mTickPoints[1].x).toDouble(), 2.0) + + Math.pow((mTickPoints[2].y - mTickPoints[1].y).toDouble(), 2.0) + ).toFloat() + + mTickPaint.strokeWidth = mStrokeWidth.toFloat() + } + + + override fun onDraw(canvas: Canvas) { + drawBorder(canvas) + drawCenter(canvas) + drawTick(canvas) + } + + private fun drawCenter(canvas: Canvas) { + mPaint.color = mUnCheckedColor + val radius = (mCenterPoint.x - mStrokeWidth) * mScaleVal + canvas.drawCircle(mCenterPoint.x.toFloat(), mCenterPoint.y.toFloat(), radius, mPaint) + } + + private fun drawBorder(canvas: Canvas) { + mFloorPaint.color = mFloorColor + val radius = mCenterPoint.x + canvas.drawCircle( + mCenterPoint.x.toFloat(), + mCenterPoint.y.toFloat(), + radius * mFloorScale, + mFloorPaint + ) + } + + private fun drawTick(canvas: Canvas) { + if (mTickDrawing && isChecked) { + drawTickPath(canvas) + } + } + + private fun drawTickPath(canvas: Canvas) { + mTickPath.reset() + mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) + mTickPath.lineTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + mTickPath.lineTo(mTickPoints[2].x.toFloat(), mTickPoints[2].y.toFloat()) + canvas.drawPath(mTickPath, mTickPaint) + } + + private fun startCheckedAnimation() { + ValueAnimator.ofFloat(1.0f, 0f).apply { + duration = (mAnimDuration / 3 * 2).toLong() + interpolator = LinearInterpolator() + addUpdateListener { + mScaleVal = it.animatedValue as Float + mFloorColor = getGradientColor(mUnCheckedColor, mCheckedColor, 1 - mScaleVal) + postInvalidate() + } + start() + } + + ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f).apply { + duration = mAnimDuration.toLong() + interpolator = LinearInterpolator() + addUpdateListener { + mFloorScale = it.animatedValue as Float + postInvalidate() + } + start() + } + + drawTickDelayed() + } + + private fun startUnCheckedAnimation() { + ValueAnimator.ofFloat(0f, 1.0f).apply { + duration = mAnimDuration.toLong() + interpolator = LinearInterpolator() + addUpdateListener { + mScaleVal = it.animatedValue as Float + mFloorColor = getGradientColor(mCheckedColor, mFloorUnCheckedColor, mScaleVal) + postInvalidate() + } + start() + } + + ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f).apply { + duration = mAnimDuration.toLong() + interpolator = LinearInterpolator() + addUpdateListener { + mFloorScale = it.animatedValue as Float + postInvalidate() + } + start() + } + } + + private fun drawTickDelayed() { + postDelayed({ + mTickDrawing = true + postInvalidate() + }, mAnimDuration.toLong()) + } + + private fun getGradientColor(startColor: Int, endColor: Int, percent: Float): Int { + val startA = Color.alpha(startColor) + val startR = Color.red(startColor) + val startG = Color.green(startColor) + val startB = Color.blue(startColor) + + val endA = Color.alpha(endColor) + val endR = Color.red(endColor) + val endG = Color.green(endColor) + val endB = Color.blue(endColor) + + val currentA = (startA * (1 - percent) + endA * percent).toInt() + val currentR = (startR * (1 - percent) + endR * percent).toInt() + val currentG = (startG * (1 - percent) + endG * percent).toInt() + val currentB = (startB * (1 - percent) + endB * percent).toInt() + + return Color.argb(currentA, currentR, currentG, currentB) + } + + fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) { + mListener = listener + } + + interface OnCheckedChangeListener { + fun onCheckedChanged(checkBox: SmoothCheckBox, isChecked: Boolean) + } +} diff --git a/app/src/main/java/com/kaixed/kchat/utils/CenteredImageSpan.kt b/app/src/main/java/com/kaixed/kchat/utils/CenteredImageSpan.kt deleted file mode 100644 index 7a4c09d..0000000 --- a/app/src/main/java/com/kaixed/kchat/utils/CenteredImageSpan.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.kaixed.kchat.utils - -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.drawable.Drawable -import android.text.style.ImageSpan - -/** - * @Author: kaixed - * @Date: 2024/10/23 18:07 - */ -internal class CenteredImageSpan(drawable: Drawable?) : - ImageSpan(drawable!!, ALIGN_BASELINE) { - override fun draw( - canvas: Canvas, - text: CharSequence?, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint - ) { - val drawable = drawable - val fm = paint.fontMetricsInt - val transY = (y + fm.descent + y + fm.ascent) / 2 - drawable.bounds.bottom / 2 - canvas.save() - canvas.translate(x, transY.toFloat()) - drawable.draw(canvas) - canvas.restore() - } -} diff --git a/app/src/main/java/com/kaixed/kchat/utils/DensityUtil.kt b/app/src/main/java/com/kaixed/kchat/utils/DensityUtil.kt index 5315056..fe76e71 100644 --- a/app/src/main/java/com/kaixed/kchat/utils/DensityUtil.kt +++ b/app/src/main/java/com/kaixed/kchat/utils/DensityUtil.kt @@ -33,6 +33,13 @@ object DensityUtil { appContext.resources.displayMetrics ).toInt() + fun dp2px(dp: Int): Int = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + appContext.resources.displayMetrics + ).toInt() + fun pxToDp(px: Int): Int = (px / appContext.resources.displayMetrics.density).toInt() } diff --git a/app/src/main/java/com/kaixed/kchat/utils/ImageSpanUtil.kt b/app/src/main/java/com/kaixed/kchat/utils/ImageSpanUtil.kt index e8ece41..978506a 100644 --- a/app/src/main/java/com/kaixed/kchat/utils/ImageSpanUtil.kt +++ b/app/src/main/java/com/kaixed/kchat/utils/ImageSpanUtil.kt @@ -2,11 +2,9 @@ package com.kaixed.kchat.utils import android.content.Context import android.text.Editable -import android.text.SpannableString import android.text.Spanned -import androidx.core.content.res.ResourcesCompat +import com.drake.spannable.span.CenterImageSpan import com.kaixed.kchat.R -import java.util.regex.Pattern /** * @Author: kaixed @@ -17,36 +15,6 @@ object ImageSpanUtil { private val emojiMap: MutableMap = hashMapOf("[委屈]" to R.drawable.emoji) - fun setEmojiSpan(context: Context, text: String, textSize: Int): SpannableString { - val spannableString = SpannableString(text) - val regexPattern = "\\[.*?\\]" - - val pattern = Pattern.compile(regexPattern) - val matcher = pattern.matcher(text) - while (matcher.find()) { - // 获取匹配的起始和结束位置 - val start = matcher.start() - val end = matcher.end() - val emojiResId = emojiMap[matcher.group()] - if (emojiResId != null) { - val drawable = ResourcesCompat.getDrawable(context.resources, emojiResId, null) - drawable?.let { - val newSize = (textSize * 1.2).toInt() - it.setBounds(0, 0, newSize, newSize) - val centeredImageSpan = CenteredImageSpan(it) - val verticalAlignImageSpan = VerticalAlignImageSpan(it) - spannableString.setSpan( - verticalAlignImageSpan, - start, - end, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - } - } - } - return spannableString - } - fun insertEmoji( context: Context, editable: Editable, @@ -59,19 +27,16 @@ object ImageSpanUtil { // 创建表情符号的ImageSpan val emojiResId = emojiMap[emojiStr] + if (emojiResId != null) { - val drawable = ResourcesCompat.getDrawable(context.resources, emojiResId, null) - drawable?.let { - val newSize = (textSize * 1.2).toInt() - it.setBounds(0, 0, newSize, newSize) - val centeredImageSpan = CenteredImageSpan(it) - editable.setSpan( - centeredImageSpan, - index, - index + emojiStr.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } + val span = CenterImageSpan(context, emojiResId).setDrawableSize(55) + + editable.setSpan( + span, + index, + index + emojiStr.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } } } diff --git a/app/src/main/java/com/kaixed/kchat/utils/PopWindowUtil.kt b/app/src/main/java/com/kaixed/kchat/utils/PopWindowUtil.kt index 2c724ff..4966da8 100644 --- a/app/src/main/java/com/kaixed/kchat/utils/PopWindowUtil.kt +++ b/app/src/main/java/com/kaixed/kchat/utils/PopWindowUtil.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.PopupWindow import androidx.core.view.updateMargins +import androidx.viewbinding.ViewBinding import com.kaixed.kchat.databinding.PopwindowsBinding import com.kaixed.kchat.utils.DensityUtil.dpToPx @@ -18,7 +19,7 @@ import com.kaixed.kchat.utils.DensityUtil.dpToPx */ object PopWindowUtil { - fun showPopupWindow(context: Context, parentView: View, isMine: Boolean) { + fun showPopupWindow(context: Context, parentView: View, isMine: Boolean): PopupWindow { val binding: PopwindowsBinding = PopwindowsBinding.inflate(LayoutInflater.from(context)) val popupWindow: PopupWindow = PopupWindow( binding.root, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT @@ -70,7 +71,6 @@ object PopWindowUtil { binding.ivArrowDown.visibility = View.GONE } - if (isMine) { rightEnough = (popupWidth / 2) < distanceToRightEdge xOffset = if (rightEnough) @@ -101,6 +101,8 @@ object PopWindowUtil { popupWindow.showAsDropDown(parentView, xOffset, yOffset) + return popupWindow + // binding.ivWithdraw.setOnClickListener { // messages[position].status = "withdraw" // updateDb(messages[position]) diff --git a/app/src/main/java/com/kaixed/kchat/utils/SingleLiveEvent.kt b/app/src/main/java/com/kaixed/kchat/utils/SingleLiveEvent.kt new file mode 100644 index 0000000..23c2c9e --- /dev/null +++ b/app/src/main/java/com/kaixed/kchat/utils/SingleLiveEvent.kt @@ -0,0 +1,34 @@ +package com.kaixed.kchat.utils + +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +class SingleLiveEvent : MutableLiveData() { + private val mPending = AtomicBoolean(false) + + override fun observe(owner: LifecycleOwner, observer: Observer) { + super.observe(owner) { t -> + // 只有在 mPending 为 true 时才会触发回调 + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + } + } + + @MainThread + override fun setValue(value: T?) { + mPending.set(true) + super.setValue(value) + } + + /** + * 用于 T 为 Void 类型的情况,使调用更简洁 + */ + @MainThread + fun call() { + setValue(null) + } +} diff --git a/app/src/main/java/com/kaixed/kchat/utils/TextUtil.kt b/app/src/main/java/com/kaixed/kchat/utils/TextUtil.kt index e18cac1..c0db5f6 100644 --- a/app/src/main/java/com/kaixed/kchat/utils/TextUtil.kt +++ b/app/src/main/java/com/kaixed/kchat/utils/TextUtil.kt @@ -11,6 +11,36 @@ import java.util.Locale */ object TextUtil { + /** + * 从 URL 中提取图片的宽度和高度 + * + * @param mainUrl 图片的 URL + * @return 三元组,包含提取后的 URL、宽度和高度 + */ + fun extractDimensionsAndPrefix(mainUrl: String): Triple? { + // 定义正则表达式,提取 ! 前的部分,w 和 h 参数 + val regex = """(.*)!w=(\d+)&h=(\d+)""".toRegex() + + // 使用正则表达式进行匹配 + val matchResult = regex.find(mainUrl) + + return matchResult?.let { + val prefix = it.groupValues[1] // 提取 ! 前的部分 + val width = it.groupValues[2].toInt() // 提取 w 的值 + val height = it.groupValues[3].toInt() // 提取 h 的值 + val screenWidth = DensityUtil.getScreenWidth() / 2 + + // 计算宽高比 + val ratio = width.toFloat() / screenWidth.toFloat() + + // 根据比率调整宽高,保证图片不会超过屏幕宽度 + val resultWidth = if (ratio > 1) (width / ratio).toInt() else width + val resultHeight = if (ratio > 1) (height / ratio).toInt() else height + + Triple(prefix, resultWidth, resultHeight) // 返回三元组 + } + } + fun getFriendCircleTime(timestamp: Long): String { val currentTime = System.currentTimeMillis() val difference = currentTime - timestamp diff --git a/app/src/main/java/com/kaixed/kchat/utils/ViewUtil.kt b/app/src/main/java/com/kaixed/kchat/utils/ViewUtil.kt index 0127f34..a0de572 100644 --- a/app/src/main/java/com/kaixed/kchat/utils/ViewUtil.kt +++ b/app/src/main/java/com/kaixed/kchat/utils/ViewUtil.kt @@ -1,5 +1,6 @@ package com.kaixed.kchat.utils +import android.util.Log import android.view.View import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout @@ -12,41 +13,93 @@ import com.kaixed.kchat.utils.DensityUtil.dp2Px * @Date: 2024/11/26 12:12 */ object ViewUtil { - fun changeView( + + fun setViewVisibility( parentView: ConstraintLayout, sender: Boolean, avatarId: Int, - contentId: Int + avatarMineId: Int, + contentId: Int, + contentMineId: Int ) { - ConstraintSet().apply { + val constraintSet = ConstraintSet() + constraintSet.clone(parentView) + + if (sender) { + constraintSet.setVisibility(avatarMineId, View.VISIBLE) + constraintSet.setVisibility(avatarId, View.GONE) + constraintSet.setVisibility(contentMineId, View.VISIBLE) + constraintSet.setVisibility(contentId, View.GONE) + } else { + constraintSet.setVisibility(avatarId, View.VISIBLE) + constraintSet.setVisibility(avatarMineId, View.GONE) + constraintSet.setVisibility(contentId, View.VISIBLE) + constraintSet.setVisibility(contentMineId, View.GONE) + } + + constraintSet.applyTo(parentView) + } + + + fun changeView( + parentView: ConstraintLayout, + sender: Boolean, + avatarView: View, + contentView: View + ) { + // 获取 dp 到 px 的转换值 + val margin = dp2Px(10) + Log.d("dp2Px", "Converted value: $margin") + + // 创建 ConstraintSet + val constraintSet = ConstraintSet().apply { clone(parentView) + if (sender) { + // 发送者布局调整:头像右对齐,内容左对齐 connect( - avatarId, ConstraintSet.END, + avatarView.id, ConstraintSet.END, parentView.id, ConstraintSet.END, - dp2Px(10) + margin ) connect( - contentId, ConstraintSet.END, - avatarId, ConstraintSet.START, - dp2Px(10) + contentView.id, ConstraintSet.END, + avatarView.id, ConstraintSet.START, + margin ) + connect(contentView.id, ConstraintSet.TOP, avatarView.id, ConstraintSet.TOP) + connect(contentView.id, ConstraintSet.BOTTOM, avatarView.id, ConstraintSet.BOTTOM) } else { + // 接收者布局调整:头像左对齐,内容右对齐 connect( - avatarId, ConstraintSet.START, + avatarView.id, ConstraintSet.START, parentView.id, ConstraintSet.START, - dp2Px(10) + margin ) connect( - contentId, ConstraintSet.START, - avatarId, ConstraintSet.END, - dp2Px(10) + contentView.id, ConstraintSet.START, + avatarView.id, ConstraintSet.END, + margin ) + connect(contentView.id, ConstraintSet.TOP, avatarView.id, ConstraintSet.TOP) + connect(contentView.id, ConstraintSet.BOTTOM, avatarView.id, ConstraintSet.BOTTOM) } + + // 确保更新到父布局 applyTo(parentView) } + + // 调试:打印控件的可见性 + Log.d("ConstraintDebug", "avatarView visibility: ${avatarView.visibility}") + Log.d("ConstraintDebug", "contentView visibility: ${contentView.visibility}") + + // 强制父布局重新绘制,确保布局生效 + parentView.post { + constraintSet.applyTo(parentView) + } } + fun changeTimerVisibility( position: Int, messages: List, diff --git a/app/src/main/java/com/kaixed/kchat/viewmodel/ContactViewModel.kt b/app/src/main/java/com/kaixed/kchat/viewmodel/ContactViewModel.kt index 0df631f..10defba 100644 --- a/app/src/main/java/com/kaixed/kchat/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/com/kaixed/kchat/viewmodel/ContactViewModel.kt @@ -9,6 +9,7 @@ import com.kaixed.kchat.data.local.entity.Contact import com.kaixed.kchat.data.model.friend.FriendRequestItem import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.data.repository.ContactRepository +import com.kaixed.kchat.utils.SingleLiveEvent import io.objectbox.Box import kotlinx.coroutines.launch @@ -32,7 +33,7 @@ class ContactViewModel : ViewModel() { val addContactResult: LiveData> = _addContactResult // 搜索联系人 - private val _searchContactResult = MutableLiveData>() + private val _searchContactResult = SingleLiveEvent>() val searchContactResult: LiveData> = _searchContactResult // 获取联系人列表 diff --git a/app/src/main/res/anim/push_in_from_left.xml b/app/src/main/res/anim/push_in_from_left.xml new file mode 100644 index 0000000..aa7f8a7 --- /dev/null +++ b/app/src/main/res/anim/push_in_from_left.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/anim/push_in_from_right.xml b/app/src/main/res/anim/push_in_from_right.xml new file mode 100644 index 0000000..255fe16 --- /dev/null +++ b/app/src/main/res/anim/push_in_from_right.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/anim/push_out_to_left.xml b/app/src/main/res/anim/push_out_to_left.xml new file mode 100644 index 0000000..f8f3b99 --- /dev/null +++ b/app/src/main/res/anim/push_out_to_left.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/push_out_to_right.xml b/app/src/main/res/anim/push_out_to_right.xml new file mode 100644 index 0000000..da2f6c3 --- /dev/null +++ b/app/src/main/res/anim/push_out_to_right.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable-hdpi/image_loading.jpg b/app/src/main/res/drawable-hdpi/image_loading.jpg new file mode 100644 index 0000000..1223502 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/image_loading.jpg differ diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..cfbe822 --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 01023c1..8673790 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -57,7 +57,22 @@ android:layout_height="0dp" android:layout_marginStart="35dp" android:background="@null" - android:hint="请填写手机号或用户名" + android:hint="请填写用户名" + android:visibility="invisible" + android:textCursorDrawable="@drawable/cursor" + android:textSize="17sp" + app:layout_constraintBottom_toTopOf="@id/view1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/tv_username" + app:layout_constraintTop_toBottomOf="@id/view" /> + + @@ -134,12 +150,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + layout="@layout/layout_bottom_navigation" /> diff --git a/app/src/main/res/layout/activity_profile_detail.xml b/app/src/main/res/layout/activity_profile_detail.xml index d4e3b85..c88baeb 100644 --- a/app/src/main/res/layout/activity_profile_detail.xml +++ b/app/src/main/res/layout/activity_profile_detail.xml @@ -15,7 +15,7 @@ android:layout_height="wrap_content" app:titleName="个人信息" /> - @@ -89,7 +89,7 @@ app:itemName="我的地址" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_register.xml b/app/src/main/res/layout/activity_register.xml index ecb8327..cfe338d 100644 --- a/app/src/main/res/layout/activity_register.xml +++ b/app/src/main/res/layout/activity_register.xml @@ -26,7 +26,7 @@ android:layout_marginTop="50dp" android:text="手机号注册" android:textColor="@color/black" - android:textSize="24sp" + android:textSize="19sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -46,7 +46,7 @@ android:layout_marginTop="15dp" android:text="昵称" android:textColor="@color/black" - android:textSize="19sp" + android:textSize="17sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/view" /> @@ -58,7 +58,11 @@ android:layout_marginStart="35dp" android:background="@null" android:hint="例如:喜乐" + android:inputType="text" + android:maxLength="6" + android:maxLines="1" android:textCursorDrawable="@drawable/cursor" + android:textSize="17sp" app:layout_constraintBottom_toTopOf="@id/view0" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/tv_username" @@ -67,7 +71,7 @@ @@ -93,7 +97,10 @@ android:layout_marginStart="35dp" android:background="@null" android:hint="请填写手机号" + android:maxLength="11" + android:maxLines="1" android:textCursorDrawable="@drawable/cursor" + android:textSize="17sp" app:layout_constraintBottom_toTopOf="@id/view1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/tv_username" @@ -102,7 +109,7 @@ @@ -127,7 +134,10 @@ android:background="@null" android:hint="请填写密码" android:inputType="textPassword" + android:maxLength="16" + android:maxLines="1" android:textCursorDrawable="@drawable/cursor" + android:textSize="17sp" app:layout_constraintBottom_toTopOf="@id/view2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@id/et_username" @@ -137,12 +147,22 @@ + + + app:layout_constraintVertical_bias="0.95" /> + app:layout_constraintVertical_bias="0.8" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_friends.xml b/app/src/main/res/layout/activity_search_friends.xml index 00747b5..3918625 100644 --- a/app/src/main/res/layout/activity_search_friends.xml +++ b/app/src/main/res/layout/activity_search_friends.xml @@ -43,6 +43,7 @@ android:background="@null" android:hint="账号/手机号" android:maxLines="1" + android:inputType="text" android:textColor="@color/black" android:textSize="15sp" /> @@ -80,7 +81,7 @@ android:orientation="horizontal" android:paddingHorizontal="15dp" android:paddingVertical="10dp" - android:visibility="gone" + android:visibility="invisible" app:layout_constraintTop_toBottomOf="@id/view"> - - + android:visibility="invisible" + app:layout_constraintTop_toBottomOf="@id/view"> + + + app:layout_constraintTop_toBottomOf="@id/tv_title" /> - - - - - - - - + android:layout_height="match_parent" + app:defaultNavHost="true" + app:navGraph="@navigation/nav_graph" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/bottom_sheet_layout.xml b/app/src/main/res/layout/bottom_sheet_layout.xml index 5328a10..c06e3d2 100644 --- a/app/src/main/res/layout/bottom_sheet_layout.xml +++ b/app/src/main/res/layout/bottom_sheet_layout.xml @@ -1,42 +1,54 @@ - + + app:cardCornerRadius="13dp" + app:cardElevation="0dp" + app:cardMaxElevation="0dp"> - + android:layout_height="wrap_content" + android:background="#F7F7F7" + android:orientation="vertical"> - + - - + + + + + + + + diff --git a/app/src/main/res/layout/chat_recycle_item_custom_normal.xml b/app/src/main/res/layout/chat_recycle_item_custom_normal.xml index d5d4068..dfec6b1 100644 --- a/app/src/main/res/layout/chat_recycle_item_custom_normal.xml +++ b/app/src/main/res/layout/chat_recycle_item_custom_normal.xml @@ -21,14 +21,17 @@ 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" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_recycle_item_image_normal.xml b/app/src/main/res/layout/chat_recycle_item_image_normal.xml index de61e60..ad079bc 100644 --- a/app/src/main/res/layout/chat_recycle_item_image_normal.xml +++ b/app/src/main/res/layout/chat_recycle_item_image_normal.xml @@ -9,10 +9,10 @@ @@ -28,8 +30,31 @@ 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" app:roundPercent="0.15" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_account_security.xml b/app/src/main/res/layout/fragment_account_security.xml new file mode 100644 index 0000000..63c4716 --- /dev/null +++ b/app/src/main/res/layout/fragment_account_security.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml new file mode 100644 index 0000000..6d82233 --- /dev/null +++ b/app/src/main/res/layout/fragment_chat.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_discovery.xml b/app/src/main/res/layout/fragment_discovery.xml index 6477875..5063446 100644 --- a/app/src/main/res/layout/fragment_discovery.xml +++ b/app/src/main/res/layout/fragment_discovery.xml @@ -18,25 +18,36 @@ android:textColor="@color/black" android:textSize="16sp" /> - + android:layout_height="match_parent"> - + - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_mine.xml b/app/src/main/res/layout/fragment_mine.xml index 6916eb9..31d9434 100644 --- a/app/src/main/res/layout/fragment_mine.xml +++ b/app/src/main/res/layout/fragment_mine.xml @@ -32,7 +32,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="15dp" - android:text="糕菜菜" + android:text="" android:textColor="@color/black" android:textSize="19sp" android:textStyle="bold" @@ -43,7 +43,7 @@ android:id="@+id/tv_id" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="kid: kaixed" + android:text="" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="@id/ifv_avatar" app:layout_constraintStart_toStartOf="@id/tv_nickname" diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml new file mode 100644 index 0000000..e8569b7 --- /dev/null +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000..75957e6 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_bottom_navigation.xml b/app/src/main/res/layout/layout_bottom_navigation.xml new file mode 100644 index 0000000..2f906e0 --- /dev/null +++ b/app/src/main/res/layout/layout_bottom_navigation.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/popwindows.xml b/app/src/main/res/layout/popwindows.xml index e54eef9..d97e647 100644 --- a/app/src/main/res/layout/popwindows.xml +++ b/app/src/main/res/layout/popwindows.xml @@ -47,6 +47,7 @@ android:textSize="12sp" /> + + + + + + + + diff --git a/app/src/main/res/values/attr.xml b/app/src/main/res/values/attr.xml index 5e6f454..c49317f 100644 --- a/app/src/main/res/values/attr.xml +++ b/app/src/main/res/values/attr.xml @@ -47,4 +47,13 @@ + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index afc3b64..e301c6d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,7 +2,7 @@ #FF000000 #FFFFFFFF - #006EEF + #2D88FE #576B95 #07C160 #E5E5E5 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a0bdd0d..17723e2 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,5 +1,5 @@ - 15dp + 8dp 0.2dp \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5cdbd78..b1165ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,8 +21,6 @@ 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" @@ -30,6 +28,7 @@ window = "1.3.0" kotlin = "1.9.23" coreKtx = "1.13.1" objectbox = "4.0.2" +navigationFragment = "2.8.4" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -41,8 +40,6 @@ pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref 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" } @@ -66,6 +63,7 @@ preference = { module = "androidx.preference:preference", version.ref = "prefere 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" } +androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }