diff --git a/app/build.gradle b/app/build.gradle index 54e4eac..efbaa80 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ plugins { } android { - compileSdk 34 + compileSdk 35 namespace "otus.gpb.recyclerview" defaultConfig { @@ -27,6 +27,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + buildFeatures + { + viewBinding = true + } kotlinOptions { jvmTarget = '1.8' } @@ -34,11 +38,12 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'com.google.android.material:material:1.7.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.core:core-ktx:1.16.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation 'com.github.bumptech.glide:glide:4.16.0' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef75335..227131a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + = emptyList() + + private lateinit var binding: ActivityMainBinding + private lateinit var adapter: ChatAdapter + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.recyclerView.addItemDecoration(CustomDecorator(this).apply { + setColor(R.color.color_chat_divider_light) + setOffset(R.integer.dividerOffset) + }) + + binding.recyclerView.layoutManager = LinearLayoutManager(this) + adapter = ChatAdapter() + + binding.recyclerView.adapter = adapter + + chatItems = generateList() + adapter.submitList(chatItems) + + binding.recyclerView.addOnScrollListener(object : OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisible = layoutManager.findLastVisibleItemPosition() + + if (lastVisible > totalItemCount - 4) { + chatItems += generateList(chatItems.size) + adapter.submitList(chatItems) + } + } + }) + + val itemTouchCallback = ItemTouchHelperCallback(::getChatItems,::setChatItems).apply { + setSwipeEdgeParams( + resources.getInteger(R.integer.swipeEdgeCornerRadius), + resources.getColor(R.color.color_primary_light), + resources.getColor(R.color.color_chat_background_on_swipe_light) + ) + ResourcesCompat.getDrawable(resources, R.drawable.icon_archive, theme) + ?.let { setArchIcon(it, 20, 20, 20, + resources.getString(R.string.arch_icon_text)) } + } + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) } -} \ No newline at end of file + + private fun getChatItems() = chatItems + private fun setChatItems(list: List) { + chatItems = list + adapter.submitList(list) + } + + private fun getRandomBool() = Random.nextInt(0, 2) > 0 + + @SuppressLint("DefaultLocale") + private fun getRandomTime() : String { + val hour = Random.nextInt(0, 24) + val min = Random.nextInt(0, 60) + return String.format("%d:%02d", hour, min) + } + + private fun randomizeGroupChat(item: GroupChat, offset: Int) : GroupChat { + return GroupChat( + id = item.id + offset, + groupName = item.groupName, + lastAuthor = item.lastAuthor, + lastMessage = item.lastMessage, + avatarUrl = item.avatarUrl, + messagePreviewUrl = item.messagePreviewUrl, + voip = getRandomBool(), + verified = getRandomBool(), + muted = getRandomBool(), + time = getRandomTime(), + checked = getRandomBool(), + read = getRandomBool(), + mentioned = getRandomBool(), + pinned = getRandomBool(), + counter = if (Random.nextInt(0, 5) > 0) 0 else { + Random.nextInt(1, 199) + } + ) + } + + private fun randomizePersonChat(item: PersonChat, offset: Int) : PersonChat { + return PersonChat( + id = item.id + offset, + personName = item.personName, + lastMessage = item.lastMessage, + avatarUrl = item.avatarUrl, + messagePreviewUrl = item.messagePreviewUrl, + checkbox = getRandomBool(), + online = getRandomBool(), + locked = getRandomBool(), + scam = if (Random.nextInt(0, 5) > 0) false else getRandomBool(), + verified = getRandomBool(), + muted = getRandomBool(), + time = getRandomTime(), + checked = getRandomBool(), + read = getRandomBool(), + mentioned = getRandomBool(), + pinned = getRandomBool(), + counter = if (Random.nextInt(0, 5) > 0) 0 else { + Random.nextInt(1, 19) + } + ) + } + + private fun generateList(startId: Int = 0) : List { + if (startId == 0) { + return listOf( + personChatList[0], + groupChatList[0], + personChatList[1], + groupChatList[1], + groupChatList[2], + personChatList[2], + groupChatList[3], + personChatList[3] + ) + } + else { + return listOf( + randomizePersonChat(personChatList[0], startId), + randomizeGroupChat(groupChatList[0], startId), + randomizePersonChat(personChatList[1], startId), + randomizeGroupChat(groupChatList[1], startId), + randomizeGroupChat(groupChatList[2], startId), + randomizePersonChat(personChatList[2], startId), + randomizeGroupChat(groupChatList[3], startId), + randomizePersonChat(personChatList[3], startId) + ) + } + } +} + diff --git a/app/src/main/java/otus/gpb/recyclerview/data/ChatItem.kt b/app/src/main/java/otus/gpb/recyclerview/data/ChatItem.kt new file mode 100644 index 0000000..ad1b35b --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/ChatItem.kt @@ -0,0 +1,5 @@ +package otus.gpb.recyclerview.data + +interface ChatItem { + val id: Int +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/data/GroupChat.kt b/app/src/main/java/otus/gpb/recyclerview/data/GroupChat.kt new file mode 100644 index 0000000..0eab0bc --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/GroupChat.kt @@ -0,0 +1,90 @@ +package otus.gpb.recyclerview.data + +data class GroupChat ( + override var id: Int, + val groupName: String, + val lastAuthor: String, + val lastMessage: String, + val avatarUrl: String, + val messagePreviewUrl: String, + var voip: Boolean, + var verified: Boolean, + var muted: Boolean, + val time: String, + var checked: Boolean, + var read: Boolean, + var mentioned: Boolean, + var pinned: Boolean, + var counter: Int +) : ChatItem + +val groupChatList = listOf( + GroupChat( + id = 1, + groupName = "SnejUgal News", + lastAuthor = "Nikolay", + lastMessage = "F", + avatarUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/Fishfinger_classic_fried_2.jpg/2560px-Fishfinger_classic_fried_2.jpg", + messagePreviewUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Moscow_%288351271825%29.jpg/500px-Moscow_%288351271825%29.jpg", + voip = true, + verified = false, + muted = true, + time = "11:38", + checked = false, + read = false, + mentioned = true, + pinned = true, + counter = 0 + ), + GroupChat( + id = 2, + groupName = "Unknown", + lastAuthor = "", + lastMessage = "!Don't swipe me!", + avatarUrl = "", + messagePreviewUrl = "", + voip = true, + verified = false, + muted = true, + time = "11:38", + checked = false, + read = true, + mentioned = true, + pinned = false, + counter = 0 + ), + GroupChat( + id = 3, + groupName = "just design", + lastAuthor = "You", + lastMessage = "I want pizza", + avatarUrl = "", + messagePreviewUrl = "", + voip = false, + verified = false, + muted = true, + time = "11:38", + checked = true, + read = false, + mentioned = false, + pinned = false, + counter = 0 + ), + GroupChat( + id = 4, + groupName = "Yes. No", + lastAuthor = "Anno", + lastMessage = "I see all :))", + avatarUrl = "", + messagePreviewUrl = "", + voip = false, + verified = true, + muted = false, + time = "11:38", + checked = true, + read = false, + mentioned = false, + pinned = false, + counter = 50 + ) +) \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/data/PersonChat.kt b/app/src/main/java/otus/gpb/recyclerview/data/PersonChat.kt new file mode 100644 index 0000000..d690353 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/PersonChat.kt @@ -0,0 +1,119 @@ +package otus.gpb.recyclerview.data + +data class PersonChat( + override var id: Int, + val personName: String, + val lastMessage: String, + val avatarUrl: String, + val messagePreviewUrl: String, + var checkbox: Boolean, + var online: Boolean, + var locked: Boolean, + var scam: Boolean, + var verified: Boolean, + var muted: Boolean, + val time: String, + var checked: Boolean, + var read: Boolean, + var mentioned: Boolean, + var pinned: Boolean, + var counter: Int +) : ChatItem + +val personChatList = listOf( + /*PersonChat( + id = 99, + personName = "test", + lastMessage = "you are welcome :)", + avatarUrl = "https://upload.wikimedia.org/wikipedia/commons/3/33/Espaguetis_carbonara.jpg", + messagePreviewUrl = "https://upload.wikimedia.org/wikipedia/commons/3/33/Espaguetis_carbonara.jpg", + checkbox = false, + online = true, + locked = false, + scam = false, + verified = true, + muted = false, + time = "11:38", + checked = true, + read = false, + mentioned = true, + pinned = true, + counter = 10 + ),*/ + PersonChat( + id = 1, + personName = "Dima Murantsev", + lastMessage = "you are welcome :)", + avatarUrl = "https://upload.wikimedia.org/wikipedia/commons/3/33/Espaguetis_carbonara.jpg", + messagePreviewUrl = "https://upload.wikimedia.org/wikipedia/commons/d/dc/The_Only_Original_Alfredo_Sauce_with_Butter_and_Parmesano-Reggiano_Cheese.png", + checkbox = false, + online = true, + locked = false, + scam = false, + verified = true, + muted = false, + time = "11:38", + checked = true, + read = false, + mentioned = false, + pinned = true, + counter = 0 + ), + PersonChat( + id = 2, + personName = "Catbird", + lastMessage = "", + avatarUrl = "", + messagePreviewUrl = "", + checkbox = false, + online = false, + locked = false, + scam = false, + verified = false, + muted = false, + time = "11:38", + checked = true, + read = false, + mentioned = false, + pinned = true, + counter = 0 + ), + PersonChat( + id = 3, + personName = "R4IN80W", + lastMessage = "do biest mein sonnechein", + avatarUrl = "", + messagePreviewUrl = "", + checkbox = false, + online = false, + locked = true, + scam = true, + verified = false, + muted = false, + time = "11:38", + checked = false, + read = false, + mentioned = false, + pinned = false, + counter = 0 + ), + PersonChat( + id = 4, + personName = "ShaMsiDDin", + lastMessage = "That's better", + avatarUrl = "", + messagePreviewUrl = "", + checkbox = false, + online = false, + locked = false, + scam = false, + verified = true, + muted = false, + time = "11:38", + checked = true, + read = false, + mentioned = false, + pinned = false, + counter = 0 + ) +) \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ui/ChatAdapter.kt b/app/src/main/java/otus/gpb/recyclerview/ui/ChatAdapter.kt new file mode 100644 index 0000000..0846efe --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ui/ChatAdapter.kt @@ -0,0 +1,189 @@ +package otus.gpb.recyclerview.ui + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import otus.gpb.recyclerview.data.ChatItem +import otus.gpb.recyclerview.R +import otus.gpb.recyclerview.data.GroupChat +import otus.gpb.recyclerview.data.PersonChat +import otus.gpb.recyclerview.databinding.VhGroupItemBinding +import otus.gpb.recyclerview.databinding.VhPersonItemBinding + +class ChatAdapter: ListAdapter(ChatDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + ViewTypes.GROUP.id-> { + val binding = + VhGroupItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + GroupChatViewHolder(binding) + } + + ViewTypes.PERSON.id-> { + val binding = + VhPersonItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + PersonChatViewHolder(binding) + } + + else -> throw IllegalArgumentException(parent.context.getString(R.string.unknown_view_type)) + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is GroupChat -> ViewTypes.GROUP.id //R.layout.vh_group_item + is PersonChat -> ViewTypes.PERSON.id //R.layout.vh_person_item + else -> -1 + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (val item = getItem(position)/*val item = data[position]*/) { + is GroupChat -> (holder as GroupChatViewHolder).bind(item) + is PersonChat -> (holder as PersonChatViewHolder).bind(item) + } + } + + inner class GroupChatViewHolder(private val binding: VhGroupItemBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: GroupChat) { + + if (item.avatarUrl.isNotEmpty()) { + Glide.with(binding.root.context) + .load(item.avatarUrl) + .centerCrop() + .into(binding.imgGroupAvatar) + } else binding.imgGroupAvatar.setImageResource(R.drawable.supervised_user_circle_24px) + + binding.imgVoipBadgeContainer.visibility = if (item.voip) View.VISIBLE + else View.GONE + + binding.txtGroupName.text = item.groupName + + binding.imgVerifiedIcon.visibility = if (item.verified) View.VISIBLE + else View.GONE + + binding.imgMuteIcon.visibility = if (item.muted) View.VISIBLE + else View.GONE + + binding.txtLastAuthor.text = item.lastAuthor + + if (item.messagePreviewUrl.isNotEmpty()) { + binding.imgMessagePreviewContainer.visibility = View.VISIBLE + Glide.with(binding.root.context) + .load(item.messagePreviewUrl) + .centerCrop() + .into(binding.imgMessagePreview) + } else binding.imgMessagePreviewContainer.visibility = View.GONE + + binding.txtLastMessage.text = item.lastMessage + binding.txtTimeValue.text = item.time + + binding.imgCheckedIcon.visibility = if (item.checked && !item.read) View.VISIBLE + else View.GONE + + binding.imgReadIcon.visibility = if (item.read) View.VISIBLE + else View.GONE + + if (item.counter == 0) binding.imgCounterContainer.visibility = View.GONE + else { + binding.imgCounterContainer.visibility = View.VISIBLE + binding.txtCounterContainer.text = item.counter.toString() + } + + binding.imgPinnedIcon.visibility = if (item.pinned) View.VISIBLE + else View.GONE + + binding.imgMentionIconContainer.visibility = if (item.mentioned) View.VISIBLE + else View.GONE + } + } + + inner class PersonChatViewHolder(private val binding: VhPersonItemBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: PersonChat) { + + if (item.avatarUrl.isNotEmpty()) { + Glide.with(binding.root.context) + .load(item.avatarUrl) + .centerCrop() + .into(binding.imgPersonAvatar) + } else binding.imgPersonAvatar.setImageResource(R.drawable.account_circle_24px) + + binding.imgCheckboxBadgeContainer.visibility = if (item.checkbox && !item.online) View.VISIBLE + else View.GONE + + binding.imgOnlineBadgeContainer.visibility = if (item.online) View.VISIBLE + else View.GONE + + binding.imgLockedIcon.visibility = if (item.locked) View.VISIBLE + else View.GONE + + binding.txtPersonName.text = item.personName + + binding.imgScamPatch.visibility = if (item.scam) View.VISIBLE + else View.GONE + + binding.imgVerifiedIcon.visibility = if (item.verified && !item.scam) View.VISIBLE + else View.GONE + + binding.imgMuteIcon.visibility = if (item.muted) View.VISIBLE + else View.GONE + + if (item.messagePreviewUrl.isNotEmpty()) { + binding.imgMessagePreviewContainer.visibility = View.VISIBLE + Glide.with(binding.root.context) + .load(item.messagePreviewUrl) + .centerCrop() + .into(binding.imgMessagePreview) + } else binding.imgMessagePreviewContainer.visibility = View.GONE + + binding.txtLastMessage.text = item.lastMessage + binding.txtTimeValue.text = item.time + + binding.imgCheckedIcon.visibility = if (item.checked && !item.read) View.VISIBLE + else View.GONE + + binding.imgReadIcon.visibility = if (item.read) View.VISIBLE + else View.GONE + + if (item.counter == 0) binding.imgCounterContainer.visibility = View.GONE + else { + binding.imgCounterContainer.visibility = View.VISIBLE + binding.txtCounterContainer.text = item.counter.toString() + } + + binding.imgPinnedIcon.visibility = if (item.pinned) View.VISIBLE + else View.GONE + + binding.imgMentionIconContainer.visibility = if (item.mentioned) View.VISIBLE + else View.GONE + } + } + + enum class ViewTypes(val id: Int) { + GROUP(R.layout.vh_group_item), + PERSON(R.layout.vh_person_item) + } +} + +class ChatDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean { + return when { + oldItem is GroupChat && newItem is GroupChat -> oldItem.id == newItem.id + oldItem is PersonChat && newItem is PersonChat -> oldItem.id == newItem.id + else -> false + } + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ui/CustomDecorator.kt b/app/src/main/java/otus/gpb/recyclerview/ui/CustomDecorator.kt new file mode 100644 index 0000000..bceeac0 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ui/CustomDecorator.kt @@ -0,0 +1,50 @@ +package otus.gpb.recyclerview.ui + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView + +class CustomDecorator(private val context: Context) : DividerItemDecoration(context, VERTICAL) { + + private val bounds = Rect() + private val paint = Paint() + private var offset = 0 + private var color = 0xFF000000.toInt() + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + paint.color = color + + val childCount = parent.childCount + for (index: Int in 0 until childCount) { + val child = parent.getChildAt(index) + parent.getDecoratedBoundsWithMargins(child, bounds) + bounds.left += offset + + val positionCurrent = parent.getChildAdapterPosition(child) + if (positionCurrent != RecyclerView.NO_POSITION) { + val lastElementPosition = parent.adapter?.itemCount?.minus(1) + if (positionCurrent != lastElementPosition) { + c.drawLine( + (bounds.left).toFloat(), + bounds.bottom.toFloat(), + bounds.right.toFloat(), + bounds.bottom.toFloat(), + paint + ) + } + } + } + } + + fun setColor(id: Int) { + color = context.getColor(id) + } + + fun setOffset(id: Int) { //73 + offset = (context.resources.getInteger(id) * Resources.getSystem().displayMetrics.density).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ui/ItemTouchHelperCallback.kt b/app/src/main/java/otus/gpb/recyclerview/ui/ItemTouchHelperCallback.kt new file mode 100644 index 0000000..b213093 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ui/ItemTouchHelperCallback.kt @@ -0,0 +1,153 @@ +package otus.gpb.recyclerview.ui + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.graphics.drawable.toBitmap +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.data.ChatItem + +class ItemTouchHelperCallback(private val getChatItems: ()->List, + private val setChatItems: (List)->Unit): + ItemTouchHelper.Callback() { + + private val density = Resources.getSystem().displayMetrics.density + + private var swipeEdgeCornerRadius = 0 + private var edgeColor = 0xFF000000.toInt() + private var backgroundColor = 0xFF000000.toInt() + private var archIcon: Drawable? = null + private var archIconTopOffset = (25 * density) + private var archIconEndOffset = (20 * density) + private var bitmap: Bitmap? = null + private var showArchIcon = false + private var archIconSize = 0 + private var archIconText = "" + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return makeMovementFlags(0, ItemTouchHelper.LEFT) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + removeItem(viewHolder.layoutPosition) + } + + @RequiresApi(Build.VERSION_CODES.P) + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + val paintOnSwipe = Paint().apply { + setColor(backgroundColor) + } + val paintEdge = Paint().apply { + setColor(edgeColor) + textSize = 30F + } + + if (dX < 0) { + val v = viewHolder.itemView + c.drawRect( + v.right.toFloat() + dX - swipeEdgeCornerRadius, + v.top.toFloat(), + v.right.toFloat(), + v.bottom.toFloat(), + paintOnSwipe + ) + c.drawArc( + v.right.toFloat() + dX - (swipeEdgeCornerRadius*2), + v.top.toFloat(), + v.right.toFloat() + dX, + v.top.toFloat() + (swipeEdgeCornerRadius*2), + 0F, -90F, true, + paintEdge + ) + c.drawRect( + v.right.toFloat() + dX - swipeEdgeCornerRadius, + v.top.toFloat() + swipeEdgeCornerRadius, + v.right.toFloat() + dX, + v.bottom.toFloat() - swipeEdgeCornerRadius, + paintEdge + ) + c.drawArc( + v.right.toFloat() + dX - (swipeEdgeCornerRadius*2), + v.bottom.toFloat() - (swipeEdgeCornerRadius*2), + v.right.toFloat() + dX, + v.bottom.toFloat(), + 0F, 90F, true, + paintEdge + ) + } + if (dX <= -200) { + if (archIcon!= null && archIconSize != 0) { + if (!showArchIcon) { + showArchIcon = true + bitmap = archIcon!!.toBitmap(archIconSize, archIconSize) + } + + if (bitmap != null) { + c.drawBitmap( + bitmap!!, + (viewHolder.itemView.width - (archIconSize + archIconEndOffset)), + viewHolder.itemView.y + archIconTopOffset, + null + ) + c.drawText(archIconText, + (viewHolder.itemView.width - (archIconSize + archIconEndOffset + 10*density)), + viewHolder.itemView.y + archIconSize + 40*density, + paintEdge) + } + } + } + else showArchIcon = false + } + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + + private fun removeItem(idx: Int) { + var list = getChatItems() + if (idx < 0) return + list = list.toMutableList().also { + it.removeAt(idx) + }.toList() + setChatItems(list) + } + + fun setSwipeEdgeParams(radius: Int, edgeColor: Int, backgroundColor: Int) { //20 + swipeEdgeCornerRadius = radius + this.edgeColor = edgeColor + this.backgroundColor = backgroundColor + } + + fun setArchIcon(archIcon: Drawable, topOffset: Int, endOffset: Int, size: Int, text: String) { + this.archIcon = archIcon + archIconTopOffset = (topOffset * density) + archIconEndOffset = (endOffset * density) + archIconSize = (size * density).toInt() + archIconText = text + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/account_circle_24px.xml b/app/src/main/res/drawable/account_circle_24px.xml new file mode 100644 index 0000000..fec33e7 --- /dev/null +++ b/app/src/main/res/drawable/account_circle_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/circle_online.xml b/app/src/main/res/drawable/circle_online.xml new file mode 100644 index 0000000..6d3f693 --- /dev/null +++ b/app/src/main/res/drawable/circle_online.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_archive.xml b/app/src/main/res/drawable/icon_archive.xml new file mode 100644 index 0000000..a2f1f70 --- /dev/null +++ b/app/src/main/res/drawable/icon_archive.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icon_checkbox.xml b/app/src/main/res/drawable/icon_checkbox.xml new file mode 100644 index 0000000..d304278 --- /dev/null +++ b/app/src/main/res/drawable/icon_checkbox.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icon_checked.xml b/app/src/main/res/drawable/icon_checked.xml new file mode 100644 index 0000000..314a6bd --- /dev/null +++ b/app/src/main/res/drawable/icon_checked.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/icon_locked.xml b/app/src/main/res/drawable/icon_locked.xml new file mode 100644 index 0000000..0efa151 --- /dev/null +++ b/app/src/main/res/drawable/icon_locked.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icon_mention.xml b/app/src/main/res/drawable/icon_mention.xml new file mode 100644 index 0000000..10d6fb2 --- /dev/null +++ b/app/src/main/res/drawable/icon_mention.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/icon_mute.xml b/app/src/main/res/drawable/icon_mute.xml new file mode 100644 index 0000000..bd102cc --- /dev/null +++ b/app/src/main/res/drawable/icon_mute.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/icon_pencil.xml b/app/src/main/res/drawable/icon_pencil.xml new file mode 100644 index 0000000..6f135f0 --- /dev/null +++ b/app/src/main/res/drawable/icon_pencil.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/icon_pinned.xml b/app/src/main/res/drawable/icon_pinned.xml new file mode 100644 index 0000000..e270350 --- /dev/null +++ b/app/src/main/res/drawable/icon_pinned.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_read.xml b/app/src/main/res/drawable/icon_read.xml new file mode 100644 index 0000000..cb34362 --- /dev/null +++ b/app/src/main/res/drawable/icon_read.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/icon_verified.xml b/app/src/main/res/drawable/icon_verified.xml new file mode 100644 index 0000000..47c07b3 --- /dev/null +++ b/app/src/main/res/drawable/icon_verified.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/menu_19x13.xml b/app/src/main/res/drawable/menu_19x13.xml new file mode 100644 index 0000000..cf1e3d0 --- /dev/null +++ b/app/src/main/res/drawable/menu_19x13.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/scam_patch.xml b/app/src/main/res/drawable/scam_patch.xml new file mode 100644 index 0000000..f6d9704 --- /dev/null +++ b/app/src/main/res/drawable/scam_patch.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/supervised_user_circle_24px.xml b/app/src/main/res/drawable/supervised_user_circle_24px.xml new file mode 100644 index 0000000..f1fede5 --- /dev/null +++ b/app/src/main/res/drawable/supervised_user_circle_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/voip_badge.xml b/app/src/main/res/drawable/voip_badge.xml new file mode 100644 index 0000000..c2b0bbd --- /dev/null +++ b/app/src/main/res/drawable/voip_badge.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/font/roboto.ttf b/app/src/main/res/font/roboto.ttf new file mode 100644 index 0000000..440843a Binary files /dev/null and b/app/src/main/res/font/roboto.ttf differ diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000..3e87dbd Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d026df..d915e32 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,68 @@ - + + + + + + + + + + + + + + + + + + - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_group_item.xml b/app/src/main/res/layout/vh_group_item.xml new file mode 100644 index 0000000..e339441 --- /dev/null +++ b/app/src/main/res/layout/vh_group_item.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_person_item.xml b/app/src/main/res/layout/vh_person_item.xml new file mode 100644 index 0000000..9596e4e --- /dev/null +++ b/app/src/main/res/layout/vh_person_item.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 114f376..72fc8f5 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,16 +1,6 @@ - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..4bb74f4 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..2208b49 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,18 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF + #FFFFFFFF + #FF517DA2 + #FFD5E8F7 + #FF8D8E90 + #FF222222 + #FF434449 + #FF8D8E90 + #FF95999A + #FF3D95D4 + #FF868686 + #FFC5C9CC + #FFD9D9D9 + #FF32A8E6 + #FF51AEE7 + #FF66A9E0 \ No newline at end of file diff --git a/app/src/main/res/values/integer.xml b/app/src/main/res/values/integer.xml new file mode 100644 index 0000000..9ccc769 --- /dev/null +++ b/app/src/main/res/values/integer.xml @@ -0,0 +1,5 @@ + + + 73 + 20 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d78b1f..b663289 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,26 @@ - RecyclerView + Telegram + All Chats + Work + Bots + Group chat avatar + Person chat avatar + group_name? + person_name? + last_author? + last_message? + time_value? + Voip badge + Verified icon + Mute icon + Pinned icon + Mention icon + Checked icon + Checkbox badge + Locked icon + Scam patch + Scam patch + Unknown view type + FAB description + Archive \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..2c3174d --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2187cf1..b9355d7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,16 +1,23 @@ - \ No newline at end of file diff --git a/build.gradle b/build.gradle index bd20018..dd6d8e3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.7.2' apply false - id 'com.android.library' version '8.7.2' apply false + id 'com.android.application' version '8.10.0' apply false + id 'com.android.library' version '8.10.0' apply false id 'org.jetbrains.kotlin.android' version '2.0.21' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8c7b120..8dd72c2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Nov 05 09:40:34 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists