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
Binary file added Screenshot_20250102_214601.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/Chat.kt
Original file line number Diff line number Diff line change
@@ -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}
}
31 changes: 31 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt
Original file line number Diff line number Diff line change
@@ -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<RecyclerView.ViewHolder>() {

private lateinit var list: MutableList<Chat>

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<Chat>) {
list = items
notifyItemInserted(0)
}
}
38 changes: 38 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt
Original file line number Diff line number Diff line change
@@ -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<ViewGroup>(R.id.chat_item_conteiner)
private var avatar = view.findViewById<ImageView>(R.id.avatar)
private var name = view.findViewById<TextView>(R.id.name)
private var message = view.findViewById<TextView>(R.id.message)
private var time = view.findViewById<TextView>(R.id.time)
private var sound_on_off = view.findViewById<ImageView>(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
}
}
78 changes: 78 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/ItemTouchHelper.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
8 changes: 8 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/Listener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package otus.gpb.recyclerview

interface Listener {
fun onItemClick(item: Chat)
fun onSoundClick(item: Chat)
fun onItemSwiped(item: Chat)
fun onLoadMoreItem()
}
98 changes: 97 additions & 1 deletion app/src/main/java/otus/gpb/recyclerview/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Chat>
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<RecyclerView>(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<Chat> {
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<Chat>()
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
}

}
Binary file added app/src/main/res/drawable/avatar_girl_1.png
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_girl_2.png
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_man_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/divider.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1dp" />
<solid android:color="#E0E0E0" />
</shape>
1 change: 1 addition & 0 deletions app/src/main/res/drawable/inbox.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/white" android:pathData="m21.708,0H2.272L0,13l.005,8.493c-.002.669.257,1.298.73,1.771.472.474,1.101.735,1.77.735h18.995c1.378,0,2.5-1.122,2.5-2.5v-8.5L21.708,0ZM3.112,1h17.757l2.123,12h-6.992v2c0,1.103-.897,2-2,2h-4c-1.103,0-2-.897-2-2v-2H1.008L3.112,1Zm18.388,22H2.505c-.401,0-.779-.157-1.062-.441-.284-.284-.439-.662-.438-1.064l-.011-7.495h6.006v1c0,1.654,1.346,3,3,3h4c1.654,0,3-1.346,3-3v-1h6v7.5c0,.827-.673,1.5-1.5,1.5Zm-2.085-13H4.58l.175-1h14.483l.177,1Zm-.531-3H5.106l.175-1h13.426l.177,1Zm-.531-3H5.632l.175-1h12.369l.177,1Z"/></vector>
1 change: 1 addition & 0 deletions app/src/main/res/drawable/sound_off.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/black" android:pathData="m19.732,12l4.146,4.146-.707.707-4.146-4.146-4.146,4.146-.707-.707,4.146-4.146-4.146-4.146.707-.707,4.146,4.146,4.146-4.146.707.707-4.146,4.146ZM5.323,6L12,.585v22.873l-6.678-5.458h-2.822c-1.378,0-2.5-1.122-2.5-2.5v-7c0-1.378,1.122-2.5,2.5-2.5h2.823Zm.354,1h-3.177c-.827,0-1.5.673-1.5,1.5v7c0,.827.673,1.5,1.5,1.5h3.178l5.322,4.35V2.684l-5.323,4.316Z"/></vector>
1 change: 1 addition & 0 deletions app/src/main/res/drawable/sound_on.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"><path android:fillColor="@android:color/black" android:pathData="m12,23.458l-6.679-5.458h-2.821c-1.379,0-2.5-1.122-2.5-2.5v-7c0-1.378,1.121-2.5,2.5-2.5h2.822L12,.585v22.873ZM2.5,7c-.827,0-1.5.673-1.5,1.5v7c0,.827.673,1.5,1.5,1.5h3.179l5.321,4.35V2.684l-5.323,4.316h-3.177Z"/></vector>
18 changes: 11 additions & 7 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/chat_item"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
63 changes: 63 additions & 0 deletions app/src/main/res/layout/chat_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/chat_item_conteiner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:padding="5dp"
android:background="#F8F8F8">

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/avatar"
android:layout_width="50dp"
android:layout_height="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />

<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/time"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="Name" />

<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/time"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/name"
tools:text="Message" />

<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="12:04" />

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/sound_on_off"
android:layout_width="30dp"
android:layout_height="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:src="@drawable/sound_on" />

</androidx.constraintlayout.widget.ConstraintLayout>
Loading