Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion app/src/main/java/otus/gpb/recyclerview/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,129 @@
package otus.gpb.recyclerview

import ChatAdapter
import CustomDividerItemDecoration
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import otus.gpb.recyclerview.models.Chat
import androidx.core.graphics.toColorInt

class MainActivity : AppCompatActivity() {
private val chatAdapter = ChatAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)

recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = chatAdapter

val dividerItemDecoration = CustomDividerItemDecoration(this, R.drawable.divider_line)
recyclerView.addItemDecoration(dividerItemDecoration)

setupSwipe(recyclerView)

chatAdapter.submitList(getMockData())
}

private fun setupSwipe(recyclerView: RecyclerView) {
val swipeCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {

override fun onMove(
rv: RecyclerView,
vh: RecyclerView.ViewHolder,
t: RecyclerView.ViewHolder
) = false

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

При использовании DiffUtil и анимаций adapterPosition может возвращать NO_POSITION или некорректное значение. bindingAdapterPosition учитывает текущее состояние адаптера

val currentList = chatAdapter.currentList.toMutableList()
currentList.removeAt(position)
chatAdapter.submitList(currentList)
}

override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val itemView = viewHolder.itemView
val paint = Paint()
if (dX < 0) {
paint.color = "#66A9E0".toColorInt()
val background = RectF(
itemView.right.toFloat() + dX,
itemView.top.toFloat(),
itemView.right.toFloat(),
itemView.bottom.toFloat()
)
c.drawRect(background, paint)

if (Math.abs(dX) > 100) {
val icon =
ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_archive)
icon?.let {
val iconSize = (itemView.height * 0.37).toInt()
val iconMarginRight = 58

val iconTop = itemView.top + (itemView.height / 2) - iconSize
val iconBottom = iconTop + iconSize
val iconRight = itemView.right - iconMarginRight
val iconLeft = iconRight - iconSize

it.setBounds(iconLeft, iconTop, iconRight, iconBottom)
it.setTint(Color.WHITE)
it.draw(c)

paint.color = Color.WHITE
paint.textSize = 28f
paint.isAntiAlias = true
paint.textAlign = Paint.Align.CENTER

val textX = (iconLeft + iconRight) / 2f
val textY = iconBottom + 33f
c.drawText("Archive", textX, textY, paint)
}
}
}

super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}

val itemTouchHelper = ItemTouchHelper(swipeCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}

private fun getMockData(): List<Chat> {
return listOf(
Chat(1, "Elon", "I love X", "14:00", isScam = false,isMuted =false, isOutgoing = false,avatarRes = R.drawable.avatar_elon),
Chat(2, "OTUS Bacic-android-2025-07", "Иди делать домашку", "Wed", isScam = false,isMuted =false,isOutgoing = false,avatarRes = R.drawable.avatar_otus),
Chat(3, "Arnold", "I'll be back", "14:45", isVerified = true, isOutgoing = false,avatarRes = R.drawable.avatar_arnold),
Chat(4, "Telegram Support", "Login code: 123", "12:00", isVerified = true, isRead = true, isOutgoing = true,avatarRes = R.drawable.avatar_telega),
Chat(5, "Crypto King", "Give me money", "Yesterday", isScam = true, isRead = false, isOutgoing = true,avatarRes = R.drawable.avatar_bitcoin),
Chat(6, "Классная гусеница", "Ок", "10:00", isMuted = true, isOutgoing = false,avatarRes = R.drawable.avatar_cool)
)
}
}
}
16 changes: 16 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/models/Chat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package otus.gpb.recyclerview.models

data class Chat(
val id: Int,
val title: String,
val lastMessage: String,
val time: String,
val isScam: Boolean = false,
val isMuted: Boolean = false,
val isOutgoing: Boolean,
val avatar: String? = null,
val isVerified: Boolean = false,
val isRead: Boolean = false,
val avatarRes: Int,

)
60 changes: 60 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/ui/ChatAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import otus.gpb.recyclerview.R
import otus.gpb.recyclerview.models.Chat

class ChatAdapter : ListAdapter<Chat, ChatAdapter.ChatViewHolder>(ChatDiffCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_chat, parent, false)
return ChatViewHolder(view)
}

override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
holder.bind(getItem(position))
}

class ChatViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val lastMessage = itemView.findViewById<TextView>(R.id.LastMessage)
private val time = itemView.findViewById<TextView>(R.id.Time)
private val verified = itemView.findViewById<ImageView>(R.id.Verified)
private val scam = itemView.findViewById<TextView>(R.id.Scam)
private val mute = itemView.findViewById<ImageView>(R.id.Mute)

fun bind(chat: Chat) {
val title = itemView.findViewById<TextView>(R.id.Title)
val statusIcon = itemView.findViewById<ImageView>(R.id.Delivered)
val avatarImageView = itemView.findViewById<ImageView>(R.id.Avatar)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Каждый раз вызывается findViewById для поиска title, statusIcon и avatarImageView, хотя часть View уже найдена в конструкторе. Это неэффективно при прокрутке списка.

Вынесите поиск всех View (title, statusIcon, avatarImageView) в конструктор ChatViewHolder и сохраните их как поля класса. В bind() просто используйте уже найденные ссылки. Это стандартная практика оптимизации ViewHolder в RecyclerView.

avatarImageView.setImageResource(chat.avatarRes)
title.text = chat.title
lastMessage.text = chat.lastMessage
time.text = chat.time
verified.visibility = if (chat.isVerified) View.VISIBLE else View.GONE
scam.visibility = if (chat.isScam) View.VISIBLE else View.GONE
mute.visibility = if (chat.isMuted) View.VISIBLE else View.GONE
if (chat.isOutgoing) {
statusIcon.visibility = View.VISIBLE
val resId = if (chat.isRead) R.drawable.ic_status_read else R.drawable.ic_status_delivered
statusIcon.setImageResource(resId)
} else {
statusIcon.visibility = View.GONE
}
}
}
}

class ChatDiffCallback : DiffUtil.ItemCallback<Chat>() {
override fun areItemsTheSame(oldItem: Chat, newItem: Chat): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Chat, newItem: Chat): Boolean {
return oldItem == newItem
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView

class CustomDividerItemDecoration(context: Context, resId: Int) : RecyclerView.ItemDecoration() {
private var divider: Drawable? = ContextCompat.getDrawable(context, resId)

override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft
val right = parent.width - parent.paddingRight

val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + (divider?.intrinsicHeight ?: 0)

divider?.let {
it.setBounds(left, top, right, bottom)
it.draw(c)
}
}
}

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.set(0, 0, 0, divider?.intrinsicHeight ?: 0)
}
}
Binary file added app/src/main/res/drawable/avatar_arnold.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/avatar_bitcoin.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/avatar_cool.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/avatar_elon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/avatar_otus.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/avatar_telega.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions app/src/main/res/drawable/divider_line.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="82dp">
<shape>
<size android:height="0.5dp" />
<solid android:color="#D9D9D9" />
</shape>
</inset>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_archive.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="27dp"
android:height="27dp"
android:viewportWidth="27"
android:viewportHeight="27">
<path
android:pathData="M24,21.292V7.415C24,7.133 24,6.4 23.661,6.061L19.935,2.338C19.823,2.226 19.461,2 18.919,2H8.081C7.539,2 7.177,2.226 7.065,2.338L3.339,6.061C3,6.4 3,7.133 3,7.415V21.292C3,22.788 4.213,24 5.71,24H21.29C22.787,24 24,22.788 24,21.292ZM18.919,3.692H8.081L6.048,5.723H20.952L18.919,3.692ZM15.532,12.154V13.507H18.455C18.751,13.507 18.899,13.866 18.69,14.075L13.5,19.261L8.31,14.075C8.1,13.866 8.249,13.507 8.545,13.507H11.468V12.154C11.468,11.612 11.919,11.477 12.145,11.477H14.855C15.397,11.477 15.532,11.928 15.532,12.154Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>
13 changes: 13 additions & 0 deletions app/src/main/res/drawable/ic_badge_muted.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="9dp"
android:height="11dp"
android:viewportWidth="9"
android:viewportHeight="11">
<path
android:pathData="M0,3.934C0,3.654 0.212,3.427 0.472,3.427H1.283L6.142,8.939V10.291C6.142,10.982 5.67,11.242 5.197,10.735L2.097,7.832H0.472C0.212,7.832 0,7.604 0,7.325V3.934Z"
android:fillColor="#BDC1C4"
android:fillType="evenOdd"/>
<path
android:pathData="M0.563,0.002C0.477,0.019 0.397,0.061 0.331,0.124C0.266,0.187 0.218,0.269 0.194,0.359C0.169,0.45 0.169,0.546 0.192,0.637C0.216,0.728 0.263,0.81 0.327,0.874L8.16,9.285C8.202,9.34 8.254,9.385 8.313,9.417C8.372,9.45 8.437,9.468 8.503,9.472C8.569,9.476 8.635,9.464 8.697,9.439C8.759,9.413 8.815,9.374 8.862,9.324C8.909,9.274 8.945,9.213 8.969,9.147C8.992,9.081 9.003,9.01 8.999,8.939C8.996,8.868 8.979,8.798 8.948,8.735C8.918,8.671 8.876,8.616 8.825,8.571L6.942,6.55V1.015C6.942,0.034 6.03,0.044 5.365,0.65L3.222,2.556L0.992,0.16C0.943,0.104 0.883,0.061 0.817,0.034C0.75,0.006 0.679,-0.005 0.608,0.002C0.593,0.001 0.578,0.001 0.563,0.002Z"
android:fillColor="#BDC1C4"/>
</vector>
17 changes: 17 additions & 0 deletions app/src/main/res/drawable/ic_badge_pinned.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M13.5,25C19.851,25 25,19.851 25,13.5C25,7.149 19.851,2 13.5,2C7.149,2 2,7.149 2,13.5C2,19.851 7.149,25 13.5,25ZM13.5,24C19.299,24 24,19.299 24,13.5C24,7.701 19.299,3 13.5,3C7.701,3 3,7.701 3,13.5C3,19.299 7.701,24 13.5,24Z"
android:fillColor="#868686"
android:fillType="evenOdd"/>
<path
android:pathData="M13.528,16.481C13.291,16.235 12.91,16.201 12.633,16.4L9.779,18.451C9.749,18.473 9.721,18.497 9.692,18.521C9.644,18.562 9.589,18.595 9.531,18.619C9.455,18.65 9.374,18.667 9.291,18.667C9.209,18.667 9.128,18.65 9.052,18.619C8.976,18.588 8.908,18.542 8.85,18.484C8.792,18.426 8.746,18.357 8.714,18.281C8.683,18.205 8.667,18.124 8.667,18.042C8.667,17.96 8.683,17.878 8.714,17.803C8.746,17.727 8.792,17.658 8.85,17.6L11.054,14.891C11.278,14.616 11.255,14.214 11,13.966L9.565,12.568C9.291,12.301 9.287,11.858 9.605,11.644C10.217,11.234 11.198,10.821 12.616,10.856C12.814,10.861 13.009,10.79 13.149,10.65L15.25,8.55L15.648,8.152C15.916,7.884 16.35,7.884 16.618,8.152L17.016,8.55L18.783,10.316L19.181,10.714C19.449,10.982 19.449,11.417 19.181,11.685L18.783,12.083L16.703,14.163C16.551,14.315 16.482,14.53 16.5,14.744C16.628,16.25 16.164,17.174 15.718,17.748C15.482,18.052 15.031,18.039 14.765,17.763L13.528,16.481Z"
android:fillColor="#868686"/>
<path
android:pathData="M13.528,16.481C13.291,16.235 12.91,16.201 12.633,16.4L9.779,18.451C9.749,18.473 9.721,18.497 9.692,18.521C9.644,18.562 9.589,18.595 9.531,18.619C9.455,18.65 9.374,18.667 9.291,18.667C9.209,18.667 9.128,18.65 9.052,18.619C8.976,18.588 8.908,18.542 8.85,18.484C8.792,18.426 8.746,18.357 8.714,18.281C8.683,18.205 8.667,18.124 8.667,18.042C8.667,17.96 8.683,17.878 8.714,17.803C8.746,17.727 8.792,17.658 8.85,17.6L11.054,14.891C11.278,14.616 11.255,14.214 11,13.966L9.565,12.568C9.291,12.301 9.287,11.858 9.605,11.644C10.217,11.234 11.198,10.821 12.616,10.856C12.814,10.861 13.009,10.79 13.149,10.65L15.25,8.55L15.648,8.152C15.916,7.884 16.35,7.884 16.618,8.152L17.016,8.55L18.783,10.316L19.181,10.714C19.449,10.982 19.449,11.417 19.181,11.685L18.783,12.083L16.703,14.163C16.551,14.315 16.482,14.53 16.5,14.744C16.628,16.25 16.164,17.174 15.718,17.748C15.482,18.052 15.031,18.039 14.765,17.763L13.528,16.481Z"
android:fillColor="#868686"
android:fillType="evenOdd"/>
</vector>
Loading