diff --git a/app/build.gradle b/app/build.gradle index 54e4eac..c196df9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,9 +35,11 @@ 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.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.activity:activity-ktx:1.9.3' + implementation 'androidx.recyclerview:recyclerview:1.3.2' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 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..c388294 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt @@ -0,0 +1,54 @@ +package otus.gpb.recyclerview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import java.util.Collections + +class ChatAdapter() : ListAdapter(DiffUtilItem()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.chat, parent, false) + return ChatViewHolder(view) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = currentList.getOrNull(position) + if (item != null) { + (holder as ChatViewHolder).bind(item) + } + } + + fun exchange(fromPosition: Int, toPosition: Int) { + val list = currentList.toMutableList() + if (fromPosition < toPosition) { + for (index in fromPosition until toPosition) { + Collections.swap(list, index, index + 1) + } + } else { + for (index in fromPosition downTo toPosition + 1) { + Collections.swap(list, index, index - 1) + } + } + submitList(list) + } + + fun removeItem(fromPosition: Int) { + val list = currentList.toMutableList() + list.removeAt(fromPosition) + submitList(list) + } +} + +class DiffUtilItem : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean { + return oldItem.id == newItem.id + } + + 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/ChatItem.kt b/app/src/main/java/otus/gpb/recyclerview/ChatItem.kt new file mode 100644 index 0000000..4b5354e --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatItem.kt @@ -0,0 +1,9 @@ +package otus.gpb.recyclerview +data class ChatItem( + val id: Int, + val date: String, + val name: String, + val surname: String, + val message: String, + val background: Int +) \ 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..3483ae2 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt @@ -0,0 +1,30 @@ +package otus.gpb.recyclerview + +import android.annotation.SuppressLint +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView + +class ChatViewHolder( + private val view: View, +) : RecyclerView.ViewHolder(view) { + + init { + println(this) + } + + private val name: TextView by lazy { view.findViewById(R.id.name) } + private val avatar: TextView by lazy { view.findViewById(R.id.avatar) } + private val message: TextView by lazy { view.findViewById(R.id.message) } + private val date: TextView by lazy { view.findViewById(R.id.date) } + + @SuppressLint("SetTextI18n") + fun bind(item: ChatItem) { + println("bind item ${item.id}") + name.text = "${item.name} ${item.surname}" + message.text = item.message + date.text = item.date + avatar.text = item.name.substring(0, 1) + item.surname.substring(0,1) + avatar.setBackgroundColor(item.background) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ColorGenerator.kt b/app/src/main/java/otus/gpb/recyclerview/ColorGenerator.kt new file mode 100644 index 0000000..0d9ce40 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ColorGenerator.kt @@ -0,0 +1,13 @@ +package otus.gpb.recyclerview + +import android.graphics.Color +import java.util.Random + +object ColorGenerator { + + fun generateColor(): Int { + val nextInt = Random().nextInt(0xffffff + 1) + val colorCode = String.format("#%06x", nextInt) + return Color.parseColor(colorCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/CustomDecorator.kt b/app/src/main/java/otus/gpb/recyclerview/CustomDecorator.kt new file mode 100644 index 0000000..ffbdc9a --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/CustomDecorator.kt @@ -0,0 +1,40 @@ +package otus.gpb.recyclerview + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +class CustomDecorator : ItemDecoration() { + + private val bounds = Rect() + private val paint = Paint().apply { + color = Color.GRAY + strokeWidth = 1f + } + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDraw(canvas, parent, state) + + val childCount = parent.childCount + for (index in 0 until childCount) { + val child = parent.getChildAt(index) + parent.getDecoratedBoundsWithMargins(child, bounds) + + val positionCurrent = parent.getChildAdapterPosition(child) + if (positionCurrent != RecyclerView.NO_POSITION) { + val lastElementPosition = parent.adapter?.itemCount?.minus(1) + if (positionCurrent != lastElementPosition) { + canvas.drawLine( + bounds.left.toFloat() + 150f, + bounds.bottom.toFloat(), + bounds.right.toFloat(), + bounds.bottom.toFloat(), + paint + ) + } + } + } + } +} \ 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..9f6f2e5 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ItemTouchHelper.kt @@ -0,0 +1,37 @@ +package otus.gpb.recyclerview + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView + +class ItemTouchCallback : ItemTouchHelper.Callback() { + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + return makeMovementFlags( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.LEFT + ) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + viewHolder.chatAdapter.exchange( + viewHolder.bindingAdapterPosition, + target.bindingAdapterPosition + ) + return true + } + + override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { + return 0.25f + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + viewHolder.chatAdapter.removeItem(viewHolder.absoluteAdapterPosition) + } + + private val RecyclerView.ViewHolder.chatAdapter: ChatAdapter + get() = bindingAdapter as? ChatAdapter ?: error("Not ChatAdapter") +} \ 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..42daca4 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -1,12 +1,68 @@ package otus.gpb.recyclerview -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton +import kotlin.random.Random class MainActivity : AppCompatActivity() { + private val adapter: ChatAdapter by lazy { ChatAdapter() } + private var list: List = emptyList() + private val names: List = listOf("Sam", "Paul", "Jean", "Jo", "Jacob", "Winter", "Summer", "Olly") + private val surnames: List = listOf("Brown", "Potter", "Sol", "Dew", "Moss", "Triple", "Kenzie", "Martin") + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() setContentView(R.layout.activity_main) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + val recyclerView = findViewById(R.id.list) + recyclerView.addItemDecoration( + CustomDecorator() + ) + + ItemTouchHelper(ItemTouchCallback()).attachToRecyclerView(recyclerView) + + recyclerView.adapter = adapter + list = generateList(15) + adapter.submitList(list) + + findViewById(R.id.loadMore).setOnClickListener { addChats() } + } + + private fun addChats() { + val currentList = list.toMutableList() + currentList.addAll(generateList(5)) + adapter.submitList(currentList) + } + + private fun generateList(number: Int) = run { + val list = mutableListOf() + repeat(number) { + val nameIdx = Random.nextInt(0, names.size - 1) + val surnameIdx = Random.nextInt(0, surnames.size - 1) + val personItem = ChatItem( + id = it, + name = names[nameIdx], + surname = surnames[surnameIdx], + message = "Some message from ${names[nameIdx]}", + date = "12:05", + background = ColorGenerator.generateColor() + ) + list.add(personItem) + } + + list.toList() } } \ 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..7152104 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,34 @@ - + android:layout_height="match_parent" + android:orientation="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="5" + tools:listitem="@layout/chat" /> - \ No newline at end of file + + diff --git a/app/src/main/res/layout/chat.xml b/app/src/main/res/layout/chat.xml new file mode 100644 index 0000000..8940d83 --- /dev/null +++ b/app/src/main/res/layout/chat.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + \ 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..ed45b05 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ -