-
Notifications
You must be signed in to change notification settings - Fork 63
Homework #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Homework #56
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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) | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| 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, | ||
|
|
||
| ) |
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| } |
| 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> |
| 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> |
| 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> |
| 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> |
There was a problem hiding this comment.
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 учитывает текущее состояние адаптера