diff --git a/app/build.gradle b/app/build.gradle index c1b2a86..2970c9f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,10 @@ android { kotlinOptions { jvmTarget = '1.8' } + + buildFeatures { + viewBinding true + } } dependencies { diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index e2cdca7..697fac1 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -1,12 +1,94 @@ package otus.gpb.recyclerview +import android.graphics.Paint import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.callback.SwipeCallBack +import otus.gpb.recyclerview.converter.ContactConverter +import otus.gpb.recyclerview.databinding.ActivityMainBinding +import otus.gpb.recyclerview.model.ContactViewModel class MainActivity : AppCompatActivity() { + val viewModel = ContactViewModel() + private lateinit var binding: ActivityMainBinding + private lateinit var converter: ContactConverter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + subscribe() + } + + fun subscribe() { + viewModel.contacts.observe(this) { contacts -> + // Update UI with the new count value + converter.addList(contacts) + } + + configureRecycler() + setupSwipe(binding.recyclerView) + viewModel.load() + } + + fun configureRecycler() { + converter = ContactConverter() + binding.recyclerView.addItemDecoration(getListRecyclerDecoration()) + binding.recyclerView.adapter = converter + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val visibleItemCount = layoutManager.childCount + val totalItemCount = layoutManager.itemCount + val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + + if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount + && firstVisibleItemPosition >= 0 + ) { + viewModel.load() + } + } + }) + + } + + private fun setupSwipe(recyclerView: RecyclerView) { + val iconMarginDpRight = 29 + val iconMarginDpTop = 16 + val background = Paint() + val icon = getDrawable(R.drawable.archive_24px) + + background.color = getColor(R.color.backgroundDelete) + val callback = SwipeCallBack( + density = resources.displayMetrics.density, + scaledDensity = resources.displayMetrics.scaledDensity, + iconMarginDpRight = iconMarginDpRight, + iconMarginDpTop = iconMarginDpTop, + swipeAction = { + val position = it.adapterPosition + converter.remove(position) + }, + background = background, + icon = icon + ) + val itemTouchHelper = ItemTouchHelper(callback) + itemTouchHelper.attachToRecyclerView(recyclerView) + } + + private fun getListRecyclerDecoration(): RecyclerView.ItemDecoration { + val dividerDrawable = + AppCompatResources.getDrawable(this, R.drawable.separator) + return DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply { + dividerDrawable?.let { + setDrawable(it) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/callback/SwipeCallBack.kt b/app/src/main/java/otus/gpb/recyclerview/callback/SwipeCallBack.kt new file mode 100644 index 0000000..8a0e967 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/callback/SwipeCallBack.kt @@ -0,0 +1,113 @@ +package otus.gpb.recyclerview.callback + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.drawable.Drawable +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView + +class SwipeCallBack( + private val density: Float, + private val scaledDensity: Float, + private val iconMarginDpRight: Int, + private val iconMarginDpTop: Int, + private val swipeAction: (viewHolder: RecyclerView.ViewHolder) -> Unit, + private val background: Paint, + private val icon: Drawable?, + dragDirs: Int = 0, + swipeDirs: Int = ItemTouchHelper.LEFT +) : ItemTouchHelper.SimpleCallback(dragDirs, swipeDirs) { + + private fun sizeDpInPixel(sizeDp: Int): Int { + return (density * sizeDp + 0.5f).toInt() + } + + private fun sizePixelInDp(sizePixel: Int): Int { + return (sizePixel / density + 0.5f).toInt() + } + + private fun pxToSp(px: Float): Float{ + return px/scaledDensity + } + private fun spToPx(sp: Float): Float{ + return sp * scaledDensity + } + + // не передвигаем вверх вниз + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + swipeAction.invoke(viewHolder) + } + + override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { + return 0.7f + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + + val itemView = viewHolder.itemView +// val itemHeight = itemView.bottom - itemView.top + + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + if (dX <= 0f) { + var iconTop = 0 + var iconLeft = 0 + var iconRight = 0 + var iconBottom = 0 + icon?.let { + + iconTop = itemView.top + sizeDpInPixel(iconMarginDpTop) + iconRight = itemView.right - sizeDpInPixel(iconMarginDpRight) + iconBottom = iconTop + icon.intrinsicHeight + iconLeft = iconRight - icon.intrinsicWidth + c.drawRect( + itemView.right.toFloat() + dX, + itemView.top.toFloat(), + itemView.right.toFloat(), + itemView.bottom.toFloat(), + background + ) + icon.setBounds(iconLeft,iconTop,iconRight,iconBottom) + icon.draw(c) + } + val textPaint = Paint() + textPaint.color = Color.WHITE + val deleteTextSize = spToPx(13F) + textPaint.textSize = deleteTextSize + + + val textWidth = textPaint.measureText("Archive") + val textY = itemView.bottom - sizeDpInPixel(16).toFloat() + val textX = itemView.right - sizeDpInPixel(20).toFloat() - textWidth + + + c.drawText("Archive", textX, textY, textPaint) + } + } + super.onChildDraw( + c, + recyclerView, + viewHolder, + dX, + dY, + actionState, + isCurrentlyActive + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/converter/ContactConverter.kt b/app/src/main/java/otus/gpb/recyclerview/converter/ContactConverter.kt new file mode 100644 index 0000000..5655776 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/converter/ContactConverter.kt @@ -0,0 +1,133 @@ +package otus.gpb.recyclerview.converter + +import android.text.format.DateUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.databinding.ChatBinding +import otus.gpb.recyclerview.model.Contact +import otus.gpb.recyclerview.model.MessageStatusEnum +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +class ContactConverter : RecyclerView.Adapter() { + private var items: MutableList = mutableListOf() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatItemViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = ChatBinding.inflate(inflater, parent, false) + return ChatItemViewHolder(binding) + } + + override fun onBindViewHolder(holder: ChatItemViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount(): Int { + return items.size + } + + fun setList(list: List) { + items.clear() + addList(list) + } + fun addList(list: List) { + items.addAll(list) + notifyDataSetChanged() + } + + fun remove(position: Int) { + items.removeAt(position) + notifyItemRemoved(position) + } + + inner class ChatItemViewHolder(private val binding: ChatBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: Contact) { + binding.title.text = item.title + binding.img.setImageResource(item.imgRes) + binding.isMuted.visibility = if (item.isMuted) View.VISIBLE else View.GONE + + binding.status.visibility = if (item.isScum ) View.VISIBLE else View.GONE + binding.statusValue.visibility = if (item.isScum) View.VISIBLE else View.GONE + + if (item.message != null) { + binding.subject.visibility = View.VISIBLE + binding.lastMessageContainer.visibility = View.VISIBLE + + if (!item.subject.isEmpty()) { + binding.subject.visibility = View.VISIBLE + binding.subject.text = item.subject + } else { + binding.subject.visibility = View.GONE + } + binding.lastMessageText.text = item.message.title + + if (item.message.hasImg) { + binding.lastMessageImg.visibility = View.VISIBLE + } else { + binding.lastMessageImg.visibility = View.GONE + } + } else { + binding.subject.visibility = View.GONE + binding.lastMessageContainer.visibility = View.GONE + } + + when (item.message?.status) { + + MessageStatusEnum.SEND -> { + binding.messageSendAndRead.visibility = View.GONE + binding.messageSendAndUnread.visibility = View.VISIBLE + } + + MessageStatusEnum.DELIVERED -> { + binding.messageSendAndUnread.visibility = View.GONE + binding.messageSendAndRead.visibility = View.GONE + } + + MessageStatusEnum.READ -> { + binding.messageSendAndRead.visibility = View.VISIBLE + binding.messageSendAndUnread.visibility = View.GONE + } + } + + + + if (item.unReadCount > 0) { + binding.unreadBudge.visibility = View.VISIBLE + binding.unreadValue.text = "${item.unReadCount}" + } else { + binding.unreadBudge.visibility = View.GONE + } + + binding.date.text = item.message?.date?.let { getTime(it) } + } + } + + fun getTime(date: Date): String { + + + if (DateUtils.isToday(date.time)) { + return SimpleDateFormat("hh:mm a", Locale.ENGLISH).format(date) + } else if (isDateInCurrentWeek(date)) { + val outFormat = SimpleDateFormat("EEE",Locale.ENGLISH) + return outFormat.format(date) + } else { + return SimpleDateFormat("MMM dd",Locale.ENGLISH).format(date) + } + } + + fun isDateInCurrentWeek(date: Date?): Boolean { + val currentCalendar: Calendar = Calendar.getInstance() + val week: Int = currentCalendar.get(Calendar.WEEK_OF_YEAR) + val year: Int = currentCalendar.get(Calendar.YEAR) + val targetCalendar: Calendar = Calendar.getInstance() + if (date != null) { + targetCalendar.time = date + } + val targetWeek: Int = targetCalendar.get(Calendar.WEEK_OF_YEAR) + val targetYear: Int = targetCalendar.get(Calendar.YEAR) + return week == targetWeek && year == targetYear + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/model/Contact.kt b/app/src/main/java/otus/gpb/recyclerview/model/Contact.kt new file mode 100644 index 0000000..2f361dd --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/model/Contact.kt @@ -0,0 +1,33 @@ +package otus.gpb.recyclerview.model + +import androidx.annotation.DrawableRes +import otus.gpb.recyclerview.R +import kotlin.random.Random + +data class Contact( + val id: String, + @DrawableRes val imgRes: Int, + val title: String, + val subject: String, + val isMuted: Boolean, + val message: Message?, + val unReadCount: Int, + val isScum: Boolean, +){ + companion object { + fun getRandom(): Contact { + return Contact( + id = java.util.UUID.randomUUID().toString(), + imgRes = R.drawable.avatar, + title = listOf("Very", "bad", "title").random(), + subject = listOf("John", "Mary", "Beaver").random() + .plus(" ") + .plus(listOf("Smith", "Telegram", "Apple").random()), + isMuted = Random.nextBoolean(), + message = Message.getRandom(), + unReadCount = (0..1000).random(), + isScum = Random.nextBoolean() + ) + } + } +} diff --git a/app/src/main/java/otus/gpb/recyclerview/model/ContactViewModel.kt b/app/src/main/java/otus/gpb/recyclerview/model/ContactViewModel.kt new file mode 100644 index 0000000..3d06677 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/model/ContactViewModel.kt @@ -0,0 +1,17 @@ +package otus.gpb.recyclerview.model + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class ContactViewModel:ViewModel() { + var contacts = MutableLiveData>() + fun load(){ + var items: MutableList = emptyList().toMutableList() + + for (ii in 0..9){ + items.add(Contact.getRandom()) + } + + contacts.value = items + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/model/Message.kt b/app/src/main/java/otus/gpb/recyclerview/model/Message.kt new file mode 100644 index 0000000..218e7f4 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/model/Message.kt @@ -0,0 +1,34 @@ +package otus.gpb.recyclerview.model + +import java.util.Date +import kotlin.random.Random + +data class Message( + val title: String, + val hasImg: Boolean, + val status: MessageStatusEnum, + val date: Date +) { + + companion object { + fun getRandom(): Message { + return Message( + title = listOf("BanG Bang", "Hello","Hi","Salut!").random(), + Random.nextBoolean(), + listOf( + MessageStatusEnum.READ, + MessageStatusEnum.DELIVERED, + MessageStatusEnum.SEND + ).random(), + listOf( + Date(), + Date(System.currentTimeMillis() + (0..1000).random() * 60 * 60 * 1000) + ).random() + ) + } + } +} + + + + diff --git a/app/src/main/java/otus/gpb/recyclerview/model/MessageStatusEnum.kt b/app/src/main/java/otus/gpb/recyclerview/model/MessageStatusEnum.kt new file mode 100644 index 0000000..155d39b --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/model/MessageStatusEnum.kt @@ -0,0 +1,7 @@ +package otus.gpb.recyclerview.model + +enum class MessageStatusEnum { + SEND, + DELIVERED, + READ +} \ No newline at end of file diff --git a/app/src/main/res/drawable/archive_24px.xml b/app/src/main/res/drawable/archive_24px.xml new file mode 100644 index 0000000..a7d0bf2 --- /dev/null +++ b/app/src/main/res/drawable/archive_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/avatar.xml b/app/src/main/res/drawable/avatar.xml new file mode 100644 index 0000000..276b7c0 --- /dev/null +++ b/app/src/main/res/drawable/avatar.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..691e71b --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_read.xml b/app/src/main/res/drawable/ic_read.xml new file mode 100644 index 0000000..5cc222d --- /dev/null +++ b/app/src/main/res/drawable/ic_read.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_verified.xml b/app/src/main/res/drawable/ic_verified.xml new file mode 100644 index 0000000..2c03c43 --- /dev/null +++ b/app/src/main/res/drawable/ic_verified.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/mute_icon.xml b/app/src/main/res/drawable/mute_icon.xml new file mode 100644 index 0000000..680474c --- /dev/null +++ b/app/src/main/res/drawable/mute_icon.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/separator.xml b/app/src/main/res/drawable/separator.xml new file mode 100644 index 0000000..6839e48 --- /dev/null +++ b/app/src/main/res/drawable/separator.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status.xml b/app/src/main/res/drawable/status.xml new file mode 100644 index 0000000..158b67e --- /dev/null +++ b/app/src/main/res/drawable/status.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/unread.xml b/app/src/main/res/drawable/unread.xml new file mode 100644 index 0000000..16e3a5d --- /dev/null +++ b/app/src/main/res/drawable/unread.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/volume_variant_off.xml b/app/src/main/res/drawable/volume_variant_off.xml new file mode 100644 index 0000000..8ccd09d --- /dev/null +++ b/app/src/main/res/drawable/volume_variant_off.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d026df..53a4233 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,20 @@ + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:orientation="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> \ 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..b033972 --- /dev/null +++ b/app/src/main/res/layout/chat.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..ccdbc90 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,5 @@ #FF018786 #FF000000 #FFFFFFFF + #66A9E0 \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..986bc14 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + 16sp + 15sp + 14sp + + + + + + + \ No newline at end of file