diff --git a/app/build.gradle b/app/build.gradle index 54e4eac..09efae7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,10 +26,16 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + + } kotlinOptions { jvmTarget = '1.8' } + + viewBinding { + enabled = true + } } dependencies { @@ -38,7 +44,11 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - testImplementation 'junit:junit:4.13.2' + //implementation 'com.github.telegram:telegram-design-system:7.8.1' + implementation 'com.github.bumptech.glide:glide:4.15.1' + //kapt 'com.github.bumptech.glide:compiler:4.15.1' // For annotation processing if required testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} \ No newline at end of file +} + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef75335..04f2779 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + + + + + + \ No newline at end of file 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..1c3fa89 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/Chat.kt @@ -0,0 +1,138 @@ +package otus.gpb.recyclerview + +interface Chat { + override fun equals(other: Any?): Boolean +} + +// Parent interface +interface DataChat: Chat { + val id: Int + val lastUserName: String + val lastMessage: String + val time: String + val isVerified: Boolean + val isScam: Boolean + val unreadMessageCount: Int + val avatarUrl: String? + val lastAnswererUrl: String? + val isPinned: Boolean + var isMute: Boolean + var isSpeeching: Boolean + var isTyping: Boolean + var isUnreadedAnswerToYou: Boolean + var isAnswered: Boolean + var isOpponnentReaded: Boolean + var isLock: Boolean + +} + +// Child class for group chat +data class GroupChat( + override val id: Int, + val title: String, + override val lastUserName: String, + override val lastMessage: String, + override val time: String, + override val isVerified: Boolean = true, + override val isScam: Boolean = false, + override val unreadMessageCount: Int = 0, + override val avatarUrl: String? = null, + override val lastAnswererUrl: String? = null, + override val isPinned: Boolean = false, + override var isMute: Boolean = false, + override var isSpeeching: Boolean = false, + override var isTyping: Boolean = false, + override var isUnreadedAnswerToYou: Boolean = false, + override var isAnswered: Boolean = false, + override var isOpponnentReaded: Boolean = false, + override var isLock: Boolean = false + + +) : DataChat { + + // Custom equals implementation for GroupChat + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GroupChat) return false + return id == other.id && + lastUserName == other.lastUserName && + lastMessage == other.lastMessage && + time == other.time && + isVerified == other.isVerified && + isScam == other.isScam && + unreadMessageCount == other.unreadMessageCount && + avatarUrl == other.avatarUrl && + lastAnswererUrl == other.lastAnswererUrl && + isPinned == other.isPinned && + isMute == other.isMute && + isSpeeching == other.isSpeeching && + isTyping == other.isTyping && + isUnreadedAnswerToYou == other.isUnreadedAnswerToYou && + isAnswered == other.isAnswered && + isOpponnentReaded == other.isOpponnentReaded && + isLock == other.isLock + } + + override fun hashCode(): Int { + return id.hashCode() + } +} + +// Child class for user chat +data class UserChat( + override val id: Int, + override val lastUserName: String, + override val lastMessage: String, + override val time: String, + override val isVerified: Boolean = true, + override val isScam: Boolean = false, + override val unreadMessageCount: Int = 0, + override val avatarUrl: String? = null, + override val lastAnswererUrl: String? = null, + override val isPinned: Boolean = false, + override var isMute: Boolean = false, + override var isSpeeching: Boolean = false, + override var isTyping: Boolean = false, + override var isUnreadedAnswerToYou: Boolean = false, + override var isAnswered: Boolean = false, + override var isOpponnentReaded: Boolean = false, + override var isLock: Boolean = false +) : DataChat { + + // Custom equals implementation for UserChat + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UserChat) return false + return id == other.id && + lastUserName == other.lastUserName && + lastMessage == other.lastMessage && + time == other.time && + isVerified == other.isVerified && + isScam == other.isScam && + unreadMessageCount == other.unreadMessageCount && + avatarUrl == other.avatarUrl && + lastAnswererUrl == other.lastAnswererUrl && + isPinned == other.isPinned && + isMute == other.isMute && + isSpeeching == other.isSpeeching && + isTyping == other.isTyping && + isUnreadedAnswerToYou == other.isUnreadedAnswerToYou && + isAnswered == other.isAnswered && + isOpponnentReaded == other.isOpponnentReaded && + isLock == other.isLock + } + + override fun hashCode(): Int { + return id.hashCode() + } +} + +object ChatLoading : Chat { + override fun equals(other: Any?): Boolean { + return this === other + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } +} \ 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..a63266f --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt @@ -0,0 +1,186 @@ +package otus.gpb.recyclerview + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CircleCrop +import otus.gpb.recyclerview.databinding.ItemChatBinding + + +class ChatAdapter( + private val context: Context, + private val onArchiveChat: (Chat) -> Unit // Новый callback для архивирования +) : ListAdapter(ChatDiffCallback()) { + + companion object { + private const val VIEW_TYPE_DATA = 0 + private const val VIEW_TYPE_LOADING = 1 + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is DataChat -> VIEW_TYPE_DATA + is ChatLoading -> VIEW_TYPE_LOADING + else -> throw IllegalArgumentException(context.getString(R.string.invalid_view_type)) + } + } + + inner class ChatViewHolder(private val binding: ItemChatBinding) : + ViewHolder(binding.root) { + + fun bind(chat: Chat) { + if (chat is DataChat) { + // Установка данных в зависимости от типа чата + when (chat) { + is GroupChat -> { + binding.chatTopic.text = chat.title + binding.chatLastUser.text = chat.lastUserName + binding.chatLastUser.visibility = View.VISIBLE + } + + is UserChat -> { + binding.chatTopic.text = chat.lastUserName + binding.chatLastUser.visibility = View.GONE + } + } + + binding.chatLastMessage.text = chat.lastMessage + binding.chatTime.text = chat.time + + // Отображение иконок статусов + binding.speechingStatusIcon.visibility = + if (chat.isSpeeching) View.VISIBLE else View.GONE + binding.muteChannelIcon.visibility = if (chat.isMute) View.VISIBLE else View.GONE + + binding.lockIcon.visibility = if (chat.isLock) View.VISIBLE else View.GONE + binding.scamIcon.visibility = if (chat.isScam) View.VISIBLE else View.GONE + + binding.verifiedIcon.visibility = if (chat.isVerified) View.VISIBLE else View.GONE + + binding.unreadCount.visibility = + if (chat.unreadMessageCount > 0) View.VISIBLE else View.GONE + chat.unreadMessageCount.toString().also { binding.unreadCount.text = it } + + binding.typingIcon.visibility = if (chat.isTyping) View.VISIBLE else View.GONE + binding.typing.visibility = if (chat.isTyping) View.VISIBLE else View.GONE + binding.chatLastMessage.visibility = if (chat.isTyping) View.GONE else View.VISIBLE + + binding.answeredToYouStatus.visibility = + if (chat.isUnreadedAnswerToYou) View.VISIBLE else View.GONE + + binding.answeredIcon.visibility = + if (chat.isAnswered && !chat.isOpponnentReaded) View.VISIBLE else View.GONE + binding.answeredAndReadedIcon.visibility = + if (chat.isAnswered && chat.isOpponnentReaded) View.VISIBLE else View.GONE + binding.pinnedStatus.visibility = if (chat.isPinned) View.VISIBLE else View.GONE + + val targetSize = 500 // Размер в пикселях, до которого нужно увеличить изображение + // Загрузка аватара + if (chat.avatarUrl != null) { + Glide.with(binding.chatAvatar.context) + .load(chat.avatarUrl) + .transform( + ResizeAndCropTransformation(targetSize), + CircleCrop() + ) // Обрезаем и применяем круг + .into(binding.chatAvatar) + } else { + binding.chatAvatar.setImageResource(R.drawable.ic_placeholder_avatar) + } + + // Загрузка аватара ответившего + if (chat.lastAnswererUrl != null) { + Glide.with(binding.answererIcon.context) + .load(chat.lastAnswererUrl) + //.circleCrop() + .into(binding.answererIcon) + binding.answererIcon.visibility = View.VISIBLE + } else { + binding.answererIcon.setImageResource(android.R.color.transparent) + binding.answererIcon.visibility = View.GONE + } + + // Обработчик нажатия на кнопку архивирования + binding.archiveButton.setOnClickListener { + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + val chatObj = currentList[position] + // Обработка архивации + onArchiveChat(chatObj) + // Скрыть шторку + val archiveLayout = + binding.root.findViewById(R.id.archiveButtonLayout) + archiveLayout.visibility = View.GONE + notifyItemChanged(position) + } + } + + + binding.root.setOnClickListener { + val archiveLayout = + binding.root.findViewById(R.id.archiveButtonLayout) + val itemLayout = binding.root.findViewById(R.id.chatItemLayout) + if (archiveLayout.visibility == View.VISIBLE) { + // Анимация возврата в исходное положение + itemLayout.animate() + .translationX(0f) + .setDuration(500) + .withEndAction { + // После завершения анимации скрываем шторку + archiveLayout.visibility = View.GONE + // Обновляем элемент только после завершения анимации + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + notifyItemChanged(position) + } + } + .start() + + } + } + } + } + } + + inner class LoadingViewHolder(view: View) : ViewHolder(view) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return when (viewType) { + VIEW_TYPE_DATA -> { + val binding = ItemChatBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ChatViewHolder(binding) + } + VIEW_TYPE_LOADING -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_loading, parent, false) + LoadingViewHolder(view) + } + else -> throw IllegalArgumentException(context.getString(R.string.invalid_view_type)) + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + when (holder) { + is ChatViewHolder -> holder.bind(getItem(position) as DataChat) + is LoadingViewHolder -> { + // Здесь ничего не нужно делать, если загрузка не требует биндинга + } + else -> throw IllegalArgumentException(context.getString(R.string.invalid_view_type)) + } + } + +} + +class ChatDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Chat, newItem: Chat): Boolean + = (oldItem as? DataChat)?.id == (newItem as? DataChat)?.id + override fun areContentsTheSame(oldItem: Chat, newItem: Chat): Boolean + = oldItem == newItem +} diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index e2cdca7..2067e67 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -1,12 +1,482 @@ package otus.gpb.recyclerview -import androidx.appcompat.app.AppCompatActivity +import android.graphics.Canvas import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import android.widget.FrameLayout +import android.widget.Toast +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.navigation.NavigationView +import kotlin.math.abs +import kotlin.random.Random + class MainActivity : AppCompatActivity() { + private lateinit var chatAdapter: ChatAdapter + private var itemTouchHelper: ItemTouchHelper? = null + + private lateinit var drawerLayout: DrawerLayout + private lateinit var navigationView: NavigationView + private lateinit var toolbar: MaterialToolbar + + private var isLoading = false + private var lastChatId: Int = 1; + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + + this.title = getString(R.string.app_title) + + // Initialize views + drawerLayout = findViewById(R.id.drawer_layout) + navigationView = findViewById(R.id.navigation_view) + toolbar = findViewById(R.id.toolbar) + + // Set up toolbar + setSupportActionBar(toolbar) + + // Add navigation drawer toggle + val toggle = ActionBarDrawerToggle( + this, drawerLayout, toolbar, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close + ) + toggle.drawerArrowDrawable.color = ContextCompat.getColor(this, R.color.colorOnPrimary) + drawerLayout.addDrawerListener(toggle) + toggle.syncState() + + // Handle navigation menu item clicks + navigationView.setNavigationItemSelectedListener { menuItem -> + when (menuItem.itemId) { + R.id.nav_home -> { + Toast.makeText(this, getString(R.string.home_selected), Toast.LENGTH_SHORT).show() + } + R.id.nav_settings -> { + Toast.makeText(this, getString(R.string.settings_selected), Toast.LENGTH_SHORT).show() + } + R.id.nav_about -> { + Toast.makeText(this,getString(R.string.about_selected), Toast.LENGTH_SHORT).show() + } + } + drawerLayout.closeDrawer(GravityCompat.START) + true + } + + val fab = findViewById(R.id.fab) + fab.setOnClickListener { + // Обработка нажатия на кнопку + Toast.makeText( + this, + getString(R.string.write_new_message), + Toast.LENGTH_SHORT + ).show() + // Здесь вы можете добавить логику для открытия нового экрана или действия + } + + + val recyclerView: RecyclerView = findViewById(R.id.recycler_view) + + val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + divider.setDrawable(ContextCompat.getDrawable(this, R.drawable.recycler_view_devider)!!) + recyclerView.addItemDecoration(divider) + + chatAdapter = ChatAdapter( + context = this, + onArchiveChat = { chat -> + // Обработка архивирования + val updatedList = chatAdapter.currentList.toMutableList() + updatedList.remove(chat) + chatAdapter.submitList(updatedList) + Toast.makeText(this, "$chat archived", Toast.LENGTH_SHORT).show() + } + ) + + recyclerView.layoutManager = LinearLayoutManager(this) + recyclerView.adapter = chatAdapter + + val itemTouchHelperCallback = object : ItemTouchHelper.Callback() { + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + // Позволяем свайпы влево + return makeMovementFlags(0, ItemTouchHelper.LEFT) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + // Перемещение элементов отключено + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val position = viewHolder.adapterPosition + if (position == RecyclerView.NO_POSITION) return + + if (direction == ItemTouchHelper.LEFT) { + // Длинный свайп (удаление) + val updatedList = chatAdapter.currentList.toMutableList() + updatedList.removeAt(position) + chatAdapter.submitList(updatedList) + Toast.makeText(this@MainActivity, "Deleted item at position $position", Toast.LENGTH_SHORT).show() + } + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + val itemView = viewHolder.itemView + + // Получаем смещение dX (оно будет отрицательным для свайпа влево) + val translationX = abs(dX) + + // Устанавливаем шторку в видимое состояние, если свайп достаточно длинный + val archiveLayout = itemView.findViewById(R.id.archiveButtonLayout) + if (translationX >= itemView.width * 0.8f) { + // Длинный свайп (удаление) + archiveLayout.visibility = View.GONE + } else { + // Полусвайп (показ шторки) + archiveLayout.visibility = View.VISIBLE + + } + + // Выполняем стандартный рендеринг + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + val archiveLayout = viewHolder.itemView.findViewById(R.id.archiveButtonLayout) + val itemLayout = viewHolder.itemView.findViewById(R.id.chatItemLayout) + + // Проверяем, видима ли шторка + if (archiveLayout.visibility != View.GONE) { + // Анимация возврата в исходное положение + // Сброс состояния + itemLayout.postDelayed({ + itemLayout.animate() + .translationX(0f) + .setDuration(500) + .withEndAction { + // После завершения анимации скрываем шторку + archiveLayout.visibility = View.GONE + + // Обновляем элемент только после завершения анимации + val position = viewHolder.adapterPosition + if (position != RecyclerView.NO_POSITION) { + chatAdapter.notifyItemChanged(position) + } + } + .start() + }, 3000) + + } + } + + override fun isItemViewSwipeEnabled(): Boolean = true + } + + itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback) + itemTouchHelper?.attachToRecyclerView(recyclerView) + + + // Слушатель для пагинации + 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 (!isLoading && (visibleItemCount + firstVisibleItemPosition) >= totalItemCount + && firstVisibleItemPosition >= 0 + ) { + loadMoreItems() + } + } + }) + + // Загрузить данные + loadMoreItems() + + } + + + private fun loadMoreItems() { + // Проверяем, не загружаются ли уже данные + if (isLoading) return + isLoading = true + val list = chatAdapter.currentList.toMutableList() + // Добавляем элемент загрузки в список + if (!list.contains(ChatLoading)) { + list.add(ChatLoading) + chatAdapter.submitList(list.toList()) // Обновляем адаптер + } + // Эмуляция загрузки данных + Handler(Looper.getMainLooper()).postDelayed({ + // Убираем индикатор загрузки + list.remove(ChatLoading) + + list.addAll(loadChats()) + // Используем submitList для обновления адаптера + chatAdapter.submitList(list.toList()) + isLoading = false + }, 1500) // Задержка для эмуляции сети + } + + + private fun loadChats(): List { + return listOf( + UserChat( + id = lastChatId++, + lastUserName = "Alice", + lastMessage = "Hi there!", + time = "12:30", + isVerified = true, + avatarUrl = "https://gravatar.com/avatar/478f6b94ddcde12416b90f22d7588cab?s=400&d=robohash&r=x", + isScam = false, + unreadMessageCount = 0, + isMute = false, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isPinned = true, + isOpponnentReaded = false, + isAnswered = true + ), + GroupChat( + id = lastChatId++, + title = "Mentors", + lastUserName = "Bob", + lastMessage = "How are you?", + time = "11:15", + isVerified = false, + avatarUrl = "https://gravatar.com/avatar/1ae9a8dbb50baebe44d7b96d012a73d5?s=400&d=robohash&r=x", + isScam = false, + unreadMessageCount = Random.nextInt(30), + isMute = false, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = true, + isAnswered = true, + isLock = false, + isPinned = true, + lastAnswererUrl = "https://i.pinimg.com/736x/c9/fe/fb/c9fefb489d5fdc792ae324103255edd2.jpg" + + ), + UserChat( + id = lastChatId++, + lastUserName = "Charlie", + lastMessage = "Meeting at 3?", + time = "Yesterday", + isVerified = true, + avatarUrl = "https://gravatar.com/avatar/f07d785a9ee32506a7a077f25466f98b?s=400&d=robohash&r=x", + isScam = false, + unreadMessageCount = Random.nextInt(15), + isMute = true, + isSpeeching = true, + isTyping = false, + isUnreadedAnswerToYou = true + ), + UserChat( + id = lastChatId++, + lastUserName = "Marilyn", + lastMessage = "Tomorow?", + time = "Yesterday", + isVerified = false, + avatarUrl = "https://i.pinimg.com/736x/75/26/5b/75265b439fb335a418e79b13b447b1a6.jpg", + isScam = false, + unreadMessageCount = Random.nextInt(50), + isMute = false, + isSpeeching = false, + isTyping = true, + isUnreadedAnswerToYou = false, + isAnswered = false, + isOpponnentReaded = false + ), + UserChat( + id = lastChatId++, + lastUserName = "Elvis", + lastMessage = "Dear! Not today.", + time = "Yesterday", + isVerified = true, + avatarUrl = "https://www.kino-teatr.ru/news/20390/183753.jpg", + isScam = false, + unreadMessageCount = 0, + isMute = false, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isAnswered = true, + isOpponnentReaded = true + ), + GroupChat( + id = lastChatId++, + title = "Cat's and mouse", + lastUserName = "Catzilla", + lastMessage = "Good morning!", + time = "06:07", + isVerified = false, + avatarUrl = "https://s9.travelask.ru/uploads/post/000/031/157/main_image/facebook-bf918145c8d2cee688d53ee7112500d3.jpg", + isScam = true, + unreadMessageCount = 0, + isMute = false, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = true, + isAnswered = true, + isLock = true, + lastAnswererUrl = "https://i.pinimg.com/originals/52/c6/65/52c665df0515dd447eb92544374cf543.jpg" + + ), + GroupChat( + id = lastChatId++, + title = "StarLine Tours", + lastUserName = "Bear Grils", + lastMessage = "Let's go!", + time = "10:09", + isVerified = false, + avatarUrl = "https://avatars.mds.yandex.net/i?id=c7269ba0c5fc64e968daedd67f497d1d82453fcf-7760894-images-thumbs&n=13", + isScam = false, + unreadMessageCount = Random.nextInt(150), + isMute = false, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isAnswered = true, + isLock = false, + lastAnswererUrl = "https://avatars.mds.yandex.net/get-kinopoisk-image/1898899/78473e64-0a54-46ba-87ad-94b2822e9aaf/1920x" + + ), + GroupChat( + id = lastChatId++, + title = "News & Facts", + lastUserName = "Truth Reporter", + lastMessage = "Aliens already here!", + time = "23:20", + isVerified = false, + avatarUrl = "https://avatars.mds.yandex.net/i?id=a81133d9a76c76f1504ca65334107711af3e3b92-10465625-images-thumbs&n=13", + isScam = false, + unreadMessageCount = Random.nextInt(301), + isMute = true, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isAnswered = true, + isLock = false, + lastAnswererUrl = "https://www.kino-teatr.ru/news/20390/183753.jpg" + + ), + GroupChat( + id = lastChatId++, + title = "Food", + lastUserName = "Supplier", + lastMessage = "Food for wealth.", + time = "21:41", + isVerified = false, + avatarUrl = "https://avatars.mds.yandex.net/i?id=e1258c5d321183ada452bdb6f7283115a439ce89-10246451-images-thumbs&n=13", + isScam = false, + unreadMessageCount = Random.nextInt(350), + isMute = true, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isAnswered = true, + isLock = false, + lastAnswererUrl = "https://www.axial.net/wp-content/uploads/2016/07/naturalfood.jpg" + + ), + GroupChat( + id = lastChatId++, + title = "Fitness", + lastUserName = "Trainer", + lastMessage = "Let's go to training!", + time = "22:15", + isVerified = false, + avatarUrl = "https://cdn.culture.ru/images/cf6e22be-dddf-55cf-bef4-4dde1e64fa63", + isScam = false, + unreadMessageCount = Random.nextInt(360), + isMute = true, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isAnswered = true, + isLock = false, + lastAnswererUrl = "https://avatars.mds.yandex.net/i?id=6ce40ad4578dddf9bf81d1a52f44b5f027ee78a7-4078102-images-thumbs&n=13" + ), + GroupChat( + id = lastChatId++, + title = "Technology News", + lastUserName = "Bober", + lastMessage = "Ready for new apple!", + time = "Fri", + isVerified = true, + avatarUrl = "https://avatars.mds.yandex.net/i?id=fc179096458914d6908cd807b942bf73cf8b74cc-3663718-images-thumbs&n=13", + isScam = false, + unreadMessageCount = Random.nextInt(3000), + isMute = true, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isAnswered = true, + isLock = false, + lastAnswererUrl = "https://avatars.mds.yandex.net/i?id=67e2837139254b993171edce157f551949602e2b-12714815-images-thumbs&n=13" + ), + GroupChat( + id = lastChatId++, + title = "Fashion News", + lastUserName = "Pavlin", + lastMessage = "Exclusive for you!", + time = "Fri", + isVerified = true, + avatarUrl = "https://avatars.mds.yandex.net/i?id=cc4788280c0d75f6882102161b08737ac79c7973-4262069-images-thumbs&n=13", + isScam = false, + unreadMessageCount = Random.nextInt(40), + isMute = true, + isSpeeching = false, + isTyping = false, + isUnreadedAnswerToYou = false, + isAnswered = true, + isLock = false, + lastAnswererUrl = "https://avatars.mds.yandex.net/i?id=3e4a09ab3c6b398ac222fe28ef03953e043d48b7b0a55b2b-12999039-images-thumbs&n=13" + + ) + + + ) } -} \ No newline at end of file + +} + + + + diff --git a/app/src/main/java/otus/gpb/recyclerview/ResizeToMinSideTransformation.kt b/app/src/main/java/otus/gpb/recyclerview/ResizeToMinSideTransformation.kt new file mode 100644 index 0000000..65a12f1 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ResizeToMinSideTransformation.kt @@ -0,0 +1,36 @@ +package otus.gpb.recyclerview + +import android.graphics.Bitmap +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.security.MessageDigest + +class ResizeAndCropTransformation(private val targetSize: Int) : BitmapTransformation() { + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap { + // Определяем масштаб для увеличения изображения до нужного размера + val scale = targetSize.toFloat() / minOf(toTransform.width, toTransform.height) + + val resizedWidth = (toTransform.width * scale).toInt() + val resizedHeight = (toTransform.height * scale).toInt() + + // Масштабируем изображение + val resizedBitmap = Bitmap.createScaledBitmap(toTransform, resizedWidth, resizedHeight, true) + + // Обрезаем до квадрата + val minSide = minOf(resizedWidth, resizedHeight) + val left = (resizedWidth - minSide) / 2 + val top = (resizedHeight - minSide) / 2 + + return Bitmap.createBitmap(resizedBitmap, left, top, minSide, minSide) + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(("ResizeAndCropTransformation:$targetSize").toByteArray()) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_border.xml b/app/src/main/res/drawable/circle_border.xml new file mode 100644 index 0000000..b4b96d0 --- /dev/null +++ b/app/src/main/res/drawable/circle_border.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_border_blue_20.xml b/app/src/main/res/drawable/circle_border_blue_20.xml new file mode 100644 index 0000000..ef218a2 --- /dev/null +++ b/app/src/main/res/drawable/circle_border_blue_20.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_border_silver_10.xml b/app/src/main/res/drawable/circle_border_silver_10.xml new file mode 100644 index 0000000..ebdb013 --- /dev/null +++ b/app/src/main/res/drawable/circle_border_silver_10.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..43eb47e --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_archive.xml b/app/src/main/res/drawable/ic_archive.xml new file mode 100644 index 0000000..bee2e39 --- /dev/null +++ b/app/src/main/res/drawable/ic_archive.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_at.xml b/app/src/main/res/drawable/ic_at.xml new file mode 100644 index 0000000..690f919 --- /dev/null +++ b/app/src/main/res/drawable/ic_at.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_blue_verified.xml b/app/src/main/res/drawable/ic_blue_verified.xml new file mode 100644 index 0000000..552bd9e --- /dev/null +++ b/app/src/main/res/drawable/ic_blue_verified.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bubles.xml b/app/src/main/res/drawable/ic_bubles.xml new file mode 100644 index 0000000..e36ba82 --- /dev/null +++ b/app/src/main/res/drawable/ic_bubles.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_green_check.xml b/app/src/main/res/drawable/ic_green_check.xml new file mode 100644 index 0000000..0950241 --- /dev/null +++ b/app/src/main/res/drawable/ic_green_check.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_green_double_check.xml b/app/src/main/res/drawable/ic_green_double_check.xml new file mode 100644 index 0000000..320454b --- /dev/null +++ b/app/src/main/res/drawable/ic_green_double_check.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..f8acc37 --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 0000000..475684d --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 0000000..ab2a883 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_main_menu.xml b/app/src/main/res/drawable/ic_main_menu.xml new file mode 100644 index 0000000..32b6ead --- /dev/null +++ b/app/src/main/res/drawable/ic_main_menu.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mute.xml b/app/src/main/res/drawable/ic_mute.xml new file mode 100644 index 0000000..7d09270 --- /dev/null +++ b/app/src/main/res/drawable/ic_mute.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_nav_header_avatar.xml b/app/src/main/res/drawable/ic_nav_header_avatar.xml new file mode 100644 index 0000000..9d783a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_header_avatar.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pinned.xml b/app/src/main/res/drawable/ic_pinned.xml new file mode 100644 index 0000000..bc08f6f --- /dev/null +++ b/app/src/main/res/drawable/ic_pinned.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_placeholder_avatar.xml b/app/src/main/res/drawable/ic_placeholder_avatar.xml new file mode 100644 index 0000000..03d0044 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_avatar.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_scam.xml b/app/src/main/res/drawable/ic_scam.xml new file mode 100644 index 0000000..b739235 --- /dev/null +++ b/app/src/main/res/drawable/ic_scam.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..b305691 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_status_speech.xml b/app/src/main/res/drawable/ic_status_speech.xml new file mode 100644 index 0000000..4f367f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_status_speech.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recycler_view_devider.xml b/app/src/main/res/drawable/recycler_view_devider.xml new file mode 100644 index 0000000..8972b1a --- /dev/null +++ b/app/src/main/res/drawable/recycler_view_devider.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_background.xml b/app/src/main/res/drawable/rounded_background.xml new file mode 100644 index 0000000..d320df6 --- /dev/null +++ b/app/src/main/res/drawable/rounded_background.xml @@ -0,0 +1,7 @@ + + + + + \ 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..82ab7a4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,73 @@ - - + + android:layout_height="match_parent"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_chat.xml b/app/src/main/res/layout/item_chat.xml new file mode 100644 index 0000000..4529823 --- /dev/null +++ b/app/src/main/res/layout/item_chat.xml @@ -0,0 +1,373 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_loading.xml b/app/src/main/res/layout/item_loading.xml new file mode 100644 index 0000000..80c40b0 --- /dev/null +++ b/app/src/main/res/layout/item_loading.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nav_header.xml b/app/src/main/res/layout/nav_header.xml new file mode 100644 index 0000000..6159d78 --- /dev/null +++ b/app/src/main/res/layout/nav_header.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/navigation_menu.xml b/app/src/main/res/menu/navigation_menu.xml new file mode 100644 index 0000000..46b7a58 --- /dev/null +++ b/app/src/main/res/menu/navigation_menu.xml @@ -0,0 +1,15 @@ + + + + + + \ 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..1849f80 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,16 +1,18 @@ - + - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..08d9831 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ 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..873f527 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,48 @@ #FF018786 #FF000000 #FFFFFFFF + + #2196F3 + #1976D2 + #81C784 + #388E3C + #64B5F6 + #A5D6A7 + #4CAF50 + + + #2c75b0 + #1a5b8e + #ffffff + + #F5F5F5 + #66A9E0 + #F5F5F5 + + #f5f5f5 + #000000 + #757575 + + #d3d3d3 + + #9e9e9e + #FFFFFF + + + #1f2a36 + #0c1520 + #ffffff + + #1a1a1a + #a0a0a0 + #ffffff + + #1a1a1a + #e0e0e0 + #a0a0a0 + + #212121 + #9e9e9e + #121212 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d78b1f..791ec94 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,42 @@ RecyclerView + description + User Name + 12:34 + Last message in chat + Speech status + Mute channel + Last user + Archive + End of shadow + Answered + Pinned + Verified + Answered + Answered and opponent readed + Scam + Lock channel + Topic + Margin topic + Typing + typing + Answerer + 101011 + Main menu + Telegram + Home + Exit + Username + Avatar + Navigation drawer open + Navigation drawer close + Settings + About + Telegram + Home selected + Settings selected + About selected + Invalid view type + New message + Write new message \ 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..0d2c4cc --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2187cf1..d35017b 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,16 +1,18 @@ - + - \ No newline at end of file diff --git a/app/src/test/java/otus/gpb/recyclerview/ExampleUnitTest.kt b/app/src/test/java/otus/gpb/recyclerview/ExampleUnitTest.kt index 5ce306c..9d2c85f 100644 --- a/app/src/test/java/otus/gpb/recyclerview/ExampleUnitTest.kt +++ b/app/src/test/java/otus/gpb/recyclerview/ExampleUnitTest.kt @@ -1,17 +1,14 @@ package otus.gpb.recyclerview -import org.junit.Test - -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). + * + * class ExampleUnitTest { + * @Test + * fun addition_isCorrect() { + * assertEquals(4, 2 + 2) + * } + * } */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index bd20018..c1baa7e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ // 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 } task clean(type: Delete) { delete rootProject.buildDir -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index cd0519b..b793761 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,7 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true + + +android.suppressUnsupportedCompileSdk=1 \ No newline at end of file