diff --git a/Screenshot_20250102_214601.png b/Screenshot_20250102_214601.png new file mode 100644 index 0000000..383cd05 Binary files /dev/null and b/Screenshot_20250102_214601.png differ diff --git a/app/src/main/java/otus/gpb/recyclerview/Chat.kt b/app/src/main/java/otus/gpb/recyclerview/Chat.kt new file mode 100644 index 0000000..a82423f --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/Chat.kt @@ -0,0 +1,13 @@ +package otus.gpb.recyclerview + +import android.graphics.drawable.Drawable + +data class Chat ( + val name: String, + val message: String, + val time: String, + val avatar: Drawable? = null, + var sound: Sound = Sound.ON + ){ + enum class Sound{ ON, OFF} +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt new file mode 100644 index 0000000..dd0aa77 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt @@ -0,0 +1,31 @@ +package otus.gpb.recyclerview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +class ChatAdapter(private val listener: Listener) : RecyclerView.Adapter() { + + private lateinit var list: MutableList + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.chat_item, parent, false) + return ChatViewHolder(view, listener) + } + + override fun getItemCount(): Int = list.size + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = list[position] + (holder as ChatViewHolder).bind(item) + + if(position == list.size - 1) { + listener.onLoadMoreItem() + } + } + + fun setItems(items: MutableList) { + list = items + notifyItemInserted(0) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt new file mode 100644 index 0000000..101e233 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt @@ -0,0 +1,38 @@ +package otus.gpb.recyclerview + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView + +class ChatViewHolder( + private val view: View, + val listener: Listener +) : RecyclerView.ViewHolder(view) { + + private var root = view.findViewById(R.id.chat_item_conteiner) + private var avatar = view.findViewById(R.id.avatar) + private var name = view.findViewById(R.id.name) + private var message = view.findViewById(R.id.message) + private var time = view.findViewById(R.id.time) + private var sound_on_off = view.findViewById(R.id.sound_on_off) + + lateinit var item :Chat + + fun bind(item: Chat){ + root.setOnClickListener { listener.onItemClick(item) } + item.avatar?.let{ avatar.setImageDrawable(it) } + name.text = item.name + message.text = item.message + time.text = item.time + if( item.sound == Chat.Sound.ON ){ + sound_on_off.setImageDrawable(ResourcesCompat.getDrawable(view.resources, R.drawable.sound_on,null)) + } else { + sound_on_off.setImageDrawable(ResourcesCompat.getDrawable(view.resources, R.drawable.sound_off,null)) + } + sound_on_off.setOnClickListener { listener.onSoundClick(item) } + this.item = item + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ItemTouchHelper.kt b/app/src/main/java/otus/gpb/recyclerview/ItemTouchHelper.kt new file mode 100644 index 0000000..68033ac --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ItemTouchHelper.kt @@ -0,0 +1,78 @@ +package otus.gpb.recyclerview + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView + +class ItemTouchCallbacks(context : Context) : ItemTouchHelper.Callback(){ + + private val inbox_icon = ResourcesCompat.getDrawable(context.getResources(), R.drawable.inbox,null) + private val paint = Paint().apply { setARGB(0xFF, 0xA0,0xA0,0xFF) } + private val rect = Rect() + private lateinit var bitmap :Bitmap + private var inbox_icon_size :Int = 0 + private var once = false + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return makeMovementFlags(0, ItemTouchHelper.LEFT) + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + + // При Swipe влево освободившееся место заполнить голубым фоном с иконкой архивирования + if( isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + + rect.set(viewHolder.itemView.width + dX.toInt(), + viewHolder.itemView.y.toInt(), + viewHolder.itemView.width + viewHolder.itemView.paddingRight, + viewHolder.itemView.y.toInt() + viewHolder.itemView.height) + + c.drawRect(rect, paint) + + if( inbox_icon!= null ) { + if( !once ){ + once = true + inbox_icon_size = viewHolder.itemView.height / 2 + bitmap = inbox_icon.toBitmap(inbox_icon_size, inbox_icon_size) + } + + c.drawBitmap( + bitmap, + (viewHolder.itemView.width - inbox_icon_size - inbox_icon_size / 2).toFloat(), + viewHolder.itemView.y + inbox_icon_size / 2, + null + ) + } + } + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + TODO("Not yet implemented") + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + (viewHolder as ChatViewHolder).listener.onItemSwiped(viewHolder.item) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/Listener.kt b/app/src/main/java/otus/gpb/recyclerview/Listener.kt new file mode 100644 index 0000000..79dddd8 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/Listener.kt @@ -0,0 +1,8 @@ +package otus.gpb.recyclerview + +interface Listener { + fun onItemClick(item: Chat) + fun onSoundClick(item: Chat) + fun onItemSwiped(item: Chat) + fun onLoadMoreItem() +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index e2cdca7..c68639d 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -2,11 +2,107 @@ package otus.gpb.recyclerview import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), Listener { + + private lateinit var list :MutableList + private lateinit var adapter :ChatAdapter + private lateinit var recyclerView :RecyclerView + private var pagination :Int = 1 + private val list_generate_size = 40 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + adapter = ChatAdapter(this) + recyclerView = findViewById(R.id.recyclerView) + val decorator = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + AppCompatResources.getDrawable(this, R.drawable.divider)?.let { decorator.setDrawable(it) } + recyclerView.addItemDecoration(decorator) + ItemTouchHelper(ItemTouchCallbacks(this)).attachToRecyclerView(recyclerView) + recyclerView.adapter = adapter + list = generateList() + adapter.setItems(this.list) + } + + // Клик на сообщении чата. + override fun onItemClick(item: Chat) { + Toast.makeText(this, item.name + "\n" + item.message, Toast.LENGTH_SHORT).show() } + + // Клик на иконке sound_on_off + // Меняет иконку "sound_on <-> sound_off" у участника чата + override fun onSoundClick(item: Chat) { + + val sound = if ( item.sound == Chat.Sound.ON ) + Chat.Sound.OFF + else + Chat.Sound.ON + + for( i in list ) + { + if( i.name == item.name) + { + i.sound = sound + } + } + + recyclerView.post { + adapter.notifyDataSetChanged() // Прямой вызов notifyDataSetChanged() приводит к исключению "Cannot call this method while RecyclerView is computing a layout or scrolling" + } + } + + // Удаляет сообщение из чата + override fun onItemSwiped(item: Chat) { + val index = list.indexOf(item) + list.removeAt(index) + + recyclerView.post { + adapter.notifyItemRemoved(index) // Прямой вызов notifyItemRemoved() приводит к исключению "Cannot call this method while RecyclerView is computing a layout or scrolling" + } + } + + // Подгружает 40 новых сообщений в чат + override fun onLoadMoreItem() { + val position = list.size - 1 + val newList = generateList() + list+= newList + + recyclerView.post { + adapter.notifyItemRangeInserted(position, newList.size) // Прямой вызов notifyItemRangeInserted() приводит к исключению "Cannot call this method while RecyclerView is computing a layout or scrolling" + } + } + + private fun generateList(): MutableList { + data class Avatar(val resId: Int, val name: String, val sound :Chat.Sound) + val avatars = listOf( + Avatar(R.drawable.avatar_man_1, "Александр", Chat.Sound.ON), + Avatar(R.drawable.avatar_girl_1, "Виктория", Chat.Sound.ON), + Avatar(R.drawable.avatar_girl_2, "Светлана", Chat.Sound.OFF) + ) + + val list = mutableListOf() + repeat(list_generate_size) { + val i = (0..2).random() + val item = Chat( + avatars[i].name, + "Привет, как дела?", + String.format("%02d:%02d", pagination, it), //Время. Часы - это номер порции добавленных итемов, а минуты - это порядковый номер итема чата в текущей "порции" + ResourcesCompat.getDrawable(getResources(), avatars[i].resId,null), + avatars[i].sound ) + list.add(item) + + } + + pagination++ + return list + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar_girl_1.png b/app/src/main/res/drawable/avatar_girl_1.png new file mode 100644 index 0000000..31169bd Binary files /dev/null and b/app/src/main/res/drawable/avatar_girl_1.png differ diff --git a/app/src/main/res/drawable/avatar_girl_2.png b/app/src/main/res/drawable/avatar_girl_2.png new file mode 100644 index 0000000..28da2d1 Binary files /dev/null and b/app/src/main/res/drawable/avatar_girl_2.png differ diff --git a/app/src/main/res/drawable/avatar_man_1.png b/app/src/main/res/drawable/avatar_man_1.png new file mode 100644 index 0000000..2abd831 Binary files /dev/null and b/app/src/main/res/drawable/avatar_man_1.png differ diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml new file mode 100644 index 0000000..d60ecee --- /dev/null +++ b/app/src/main/res/drawable/divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/inbox.xml b/app/src/main/res/drawable/inbox.xml new file mode 100644 index 0000000..dad6abf --- /dev/null +++ b/app/src/main/res/drawable/inbox.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/sound_off.xml b/app/src/main/res/drawable/sound_off.xml new file mode 100644 index 0000000..34e4c83 --- /dev/null +++ b/app/src/main/res/drawable/sound_off.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/sound_on.xml b/app/src/main/res/drawable/sound_on.xml new file mode 100644 index 0000000..9345994 --- /dev/null +++ b/app/src/main/res/drawable/sound_on.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d026df..a4b659d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,17 @@ - + + android:layout_height="match_parent" + android:orientation="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/chat_item"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_item.xml b/app/src/main/res/layout/chat_item.xml new file mode 100644 index 0000000..00bc3e6 --- /dev/null +++ b/app/src/main/res/layout/chat_item.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index bd20018..6818023 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.7.3' apply false + id 'com.android.library' version '8.7.3' apply false id 'org.jetbrains.kotlin.android' version '2.0.21' apply false }