diff --git a/app/src/main/java/org/futo/circles/App.kt b/app/src/main/java/org/futo/circles/App.kt index 3bad960c7dde5cf95b1671c82975230b9cfa899c..76ad05db221b18fd719f552acccae6901ff64470 100644 --- a/app/src/main/java/org/futo/circles/App.kt +++ b/app/src/main/java/org/futo/circles/App.kt @@ -14,6 +14,7 @@ import org.futo.circles.feature.notifications.FcmHelper import org.futo.circles.feature.notifications.GuardServiceStarter import org.futo.circles.feature.notifications.NotificationUtils import org.futo.circles.feature.notifications.PushRuleTriggerListener +import org.futo.circles.feature.timeline.post.emoji.RecentEmojisProvider import org.matrix.android.sdk.api.session.Session import timber.log.Timber import javax.inject.Inject @@ -63,6 +64,7 @@ class App : Application() { notificationUtils.createNotificationChannels() setupLifecycleObserver() if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) + RecentEmojisProvider.saveDefaultRecentEmojis(applicationContext) } private fun setupLifecycleObserver() { diff --git a/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt b/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt index 928d6c14e60909f5c02171b2aec029976721a727..4b223a6a6b7017e23fb9e71cdb644c30b6fbfda6 100644 --- a/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt +++ b/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt @@ -9,7 +9,6 @@ import androidx.recyclerview.widget.DividerItemDecoration import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import org.futo.circles.core.databinding.FragmentRoomsBinding -import org.futo.circles.core.extensions.bindToFab import org.futo.circles.core.extensions.navigateSafe import org.futo.circles.core.extensions.observeData import org.futo.circles.core.extensions.observeResponse diff --git a/app/src/main/java/org/futo/circles/feature/groups/GroupsFragment.kt b/app/src/main/java/org/futo/circles/feature/groups/GroupsFragment.kt index 29729010dfcd54e72b99d9f884bc99699659e8ce..42994247a7bd66215bede370dd6e53da73e5cbb5 100644 --- a/app/src/main/java/org/futo/circles/feature/groups/GroupsFragment.kt +++ b/app/src/main/java/org/futo/circles/feature/groups/GroupsFragment.kt @@ -9,7 +9,6 @@ import androidx.recyclerview.widget.DividerItemDecoration import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import org.futo.circles.core.databinding.FragmentRoomsBinding -import org.futo.circles.core.extensions.bindToFab import org.futo.circles.core.extensions.navigateSafe import org.futo.circles.core.extensions.observeData import org.futo.circles.core.extensions.observeResponse diff --git a/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt b/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt index 8c0856a46079efb1e2dc663d8a8ada7082d3a9b2..1f29495cca6b999a7a21fbca42626bec78eb2408 100644 --- a/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt +++ b/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt @@ -151,7 +151,7 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli binding.rvTimeline.apply { adapter = listAdapter addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) - MarkAsReadBuffer(this) { viewModel.markEventAsRead(it) } + MarkAsReadBuffer(this.getRecyclerView()) { viewModel.markEventAsRead(it) } } binding.lCreatePost.setUp(object : CreatePostViewListener { override fun onCreatePoll() { @@ -161,7 +161,7 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli override fun onCreatePost() { navigator.navigateToCreatePost(timelineId, args.threadEventId) } - }, binding.rvTimeline, isThread) + }, binding.rvTimeline.getRecyclerView(), isThread) } private fun setupObservers() { diff --git a/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojiBottomSheet.kt b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojiBottomSheet.kt index 091ac893ada940f7057369ff912f6c76e0110f90..7d935e93a587656cdf9f3496d5b7c94c514a8dd7 100644 --- a/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojiBottomSheet.kt +++ b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojiBottomSheet.kt @@ -12,10 +12,9 @@ import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.vanniktech.emoji.Emoji import com.vanniktech.emoji.EmojiTheming -import com.vanniktech.emoji.recent.NoRecentEmoji -import com.vanniktech.emoji.search.SearchEmojiManager -import com.vanniktech.emoji.variant.NoVariantEmoji +import com.vanniktech.emoji.recent.RecentEmojiManager import org.futo.circles.R import org.futo.circles.databinding.BottomSheetEmojiBinding @@ -59,7 +58,7 @@ class EmojiBottomSheet : BottomSheetDialogFragment() { requireView(), { emoji -> emojiView - onEmojiSelected(emoji.unicode) + onEmojiSelected(emoji) }, null, null, EmojiTheming( backgroundColor = ContextCompat.getColor( @@ -83,16 +82,16 @@ class EmojiBottomSheet : BottomSheetDialogFragment() { requireContext(), R.color.gray ) - ), - NoRecentEmoji, SearchEmojiManager(), NoVariantEmoji + ), RecentEmojisProvider.get(requireContext()) ) tearDown() } } } - private fun onEmojiSelected(unicode: String) { - emojiPickerListener?.onEmojiSelected(args.roomId, args.eventId, unicode) + private fun onEmojiSelected(emoji: Emoji) { + emojiPickerListener?.onEmojiSelected(args.roomId, args.eventId, emoji.unicode) + RecentEmojisProvider.addNewEmoji(requireContext(), emoji) dismiss() } diff --git a/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojisTimelineAdapter.kt b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojisTimelineAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9b5e2c5e070d82953d8b496e28158211ca9b9e3 --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojisTimelineAdapter.kt @@ -0,0 +1,25 @@ +package org.futo.circles.feature.timeline.post.emoji + + +import android.view.ViewGroup +import org.futo.circles.core.list.BaseRvAdapter +import org.futo.circles.core.model.ReactionsData + + +class EmojisTimelineAdapter( + private val onEmojiSelected: (ReactionsData) -> Unit +) : BaseRvAdapter<ReactionsData, EmojisTimelineViewHolder>( + DefaultIdEntityCallback() +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + EmojisTimelineViewHolder( + parent, + onEmojiClicked = { position -> onEmojiSelected(getItem(position)) }) + + + override fun onBindViewHolder(holder: EmojisTimelineViewHolder, position: Int) { + holder.bind(getItem(position)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojisTimelineViewHolder.kt b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojisTimelineViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..71986fbac5330fda3c165dfed6af5e6c26e56b6b --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/EmojisTimelineViewHolder.kt @@ -0,0 +1,30 @@ +package org.futo.circles.feature.timeline.post.emoji + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.futo.circles.core.extensions.onClick +import org.futo.circles.core.list.ViewBindingHolder +import org.futo.circles.core.model.ReactionsData +import org.futo.circles.databinding.ListItemTimelineReactionBinding + +class EmojisTimelineViewHolder( + parent: ViewGroup, + private val onEmojiClicked: (Int) -> Unit +) : RecyclerView.ViewHolder(inflate(parent, ListItemTimelineReactionBinding::inflate)) { + + private companion object : ViewBindingHolder + + private val binding = baseBinding as ListItemTimelineReactionBinding + + init { + onClick(binding.emojiChip) { position -> onEmojiClicked(position) } + } + + fun bind(data: ReactionsData) { + binding.emojiChip.apply { + val title = "${data.key} ${data.count}" + text = title + isChecked = data.addedByMe + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/RecentEmojisProvider.kt b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/RecentEmojisProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..c40a3cf0640645c5fd85b2ddec7716d9b855b0c7 --- /dev/null +++ b/app/src/main/java/org/futo/circles/feature/timeline/post/emoji/RecentEmojisProvider.kt @@ -0,0 +1,35 @@ +package org.futo.circles.feature.timeline.post.emoji + +import android.content.Context +import com.vanniktech.emoji.Emoji +import com.vanniktech.emoji.recent.RecentEmojiManager +import com.vanniktech.emoji.search.SearchEmojiManager + +object RecentEmojisProvider { + + private var manager: RecentEmojiManager? = null + + fun get(context: Context) = manager ?: RecentEmojiManager(context).also { manager = it } + + fun saveDefaultRecentEmojis(context: Context) { + val recentManager = get(context) + val recent = recentManager.getRecentEmojis() + if (recent.isEmpty()) return + recentManager.apply { + SearchEmojiManager().apply { + search("100").firstOrNull()?.emoji?.let { emoji -> addEmoji(emoji) } + search("tada").firstOrNull()?.emoji?.let { emoji -> addEmoji(emoji) } + search("grinning").firstOrNull()?.emoji?.let { emoji -> addEmoji(emoji) } + search("thumbsup").firstOrNull()?.emoji?.let { emoji -> addEmoji(emoji) } + } + persist() + } + } + + fun addNewEmoji(context: Context, emoji: Emoji) { + get(context).apply { + addEmoji(emoji) + persist() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/view/PostFooterView.kt b/app/src/main/java/org/futo/circles/view/PostFooterView.kt index 7bec29f38c9cc2df1a6ae180d38755c67705d21f..c3ee70f4fd1f498abac12c0177d08c20381cedec 100644 --- a/app/src/main/java/org/futo/circles/view/PostFooterView.kt +++ b/app/src/main/java/org/futo/circles/view/PostFooterView.kt @@ -3,16 +3,13 @@ package org.futo.circles.view import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater -import android.widget.LinearLayout import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat import androidx.core.view.isVisible -import com.google.android.material.chip.Chip -import org.futo.circles.R import org.futo.circles.core.extensions.setIsVisible -import org.futo.circles.databinding.ViewPostFooterBinding import org.futo.circles.core.model.Post import org.futo.circles.core.model.ReactionsData +import org.futo.circles.databinding.ViewPostFooterBinding +import org.futo.circles.feature.timeline.post.emoji.EmojisTimelineAdapter import org.matrix.android.sdk.api.session.room.powerlevels.Role @@ -27,6 +24,16 @@ class PostFooterView( private var optionsListener: PostOptionsListener? = null private var post: Post? = null private var userPowerLevel: Int = Role.Default.value + private val emojisTimelineAdapter = EmojisTimelineAdapter { reaction -> + post?.let { + optionsListener?.onEmojiChipClicked( + it.postInfo.roomId, + it.id, + reaction.key, + reaction.addedByMe + ) + } + } init { setupViews() @@ -45,6 +52,7 @@ class PostFooterView( btnLike.setOnClickListener { post?.let { optionsListener?.onShowEmoji(it.postInfo.roomId, it.id) } } + rvEmojis.adapter = emojisTimelineAdapter } } @@ -76,38 +84,8 @@ class PostFooterView( } private fun bindReactionsList(reactions: List<ReactionsData>) { - binding.chipsScrollView.setIsVisible(reactions.isNotEmpty()) - with(binding.lReactions) { - removeAllViews() - val layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT - ) - layoutParams.setMargins(0, 0, 4, 0) - - reactions.forEach { reaction -> - val title = "${reaction.key} ${reaction.count}" - addView(Chip(context).apply { - text = title - setOnClickListener { - post?.let { - optionsListener?.onEmojiChipClicked( - it.postInfo.roomId, - it.id, - reaction.key, - reaction.addedByMe - ) - } - } - isCheckable = true - isCheckedIconVisible = false - chipBackgroundColor = - ContextCompat.getColorStateList(context, R.color.emoji_chip_background) - setEnsureMinTouchTargetSize(false) - isChecked = reaction.addedByMe - }, layoutParams) - } - - } + binding.rvEmojis.setIsVisible(reactions.isNotEmpty()) + emojisTimelineAdapter.submitList(reactions) } private fun areUserAbleToPost() = userPowerLevel >= Role.Default.value diff --git a/app/src/main/res/layout/dialog_fragment_timeline.xml b/app/src/main/res/layout/dialog_fragment_timeline.xml index 4ba30ce488477c00142667dd28c74f03b32d06cf..3f86ea45d08a1bcbeaf172e3799e35ae79f4929d 100644 --- a/app/src/main/res/layout/dialog_fragment_timeline.xml +++ b/app/src/main/res/layout/dialog_fragment_timeline.xml @@ -26,7 +26,7 @@ app:layout_constraintTop_toBottomOf="@id/toolbar" /> - <androidx.recyclerview.widget.RecyclerView + <org.futo.circles.core.view.LoadingRecyclerView android:id="@+id/rvTimeline" android:layout_width="match_parent" android:layout_height="0dp" diff --git a/app/src/main/res/layout/fragment_people.xml b/app/src/main/res/layout/fragment_people.xml index f8723fa1629336ac142d34a61d806fc392d62e40..df4bc56c2b7fa56aaa626cfbc4ba6abad29c43a8 100644 --- a/app/src/main/res/layout/fragment_people.xml +++ b/app/src/main/res/layout/fragment_people.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <androidx.recyclerview.widget.RecyclerView + <org.futo.circles.core.view.LoadingRecyclerView android:id="@+id/rvUsers" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/app/src/main/res/layout/list_item_timeline_reaction.xml b/app/src/main/res/layout/list_item_timeline_reaction.xml new file mode 100644 index 0000000000000000000000000000000000000000..e34a4347e8cfbfb50ae0983e200cda369474ca1c --- /dev/null +++ b/app/src/main/res/layout/list_item_timeline_reaction.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingHorizontal="1dp" + android:paddingVertical="2dp"> + + <com.google.android.material.chip.Chip + android:id="@+id/emojiChip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:checkable="true" + app:checkedIconVisible="false" + app:chipBackgroundColor="@color/emoji_chip_background" + app:ensureMinTouchTargetSize="false" /> +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/view_post_footer.xml b/app/src/main/res/layout/view_post_footer.xml index a1bef382bcd6e0957329fc08897dc33422a3cf7a..9fcbace7d2b583f367f11c70c2bb1182b054dafd 100644 --- a/app/src/main/res/layout/view_post_footer.xml +++ b/app/src/main/res/layout/view_post_footer.xml @@ -7,23 +7,17 @@ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> - <HorizontalScrollView - android:id="@+id/chipsScrollView" + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/rvEmojis" android:layout_width="0dp" android:layout_height="0dp" + android:orientation="horizontal" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="@id/btnLike" app:layout_constraintEnd_toStartOf="@id/btnLike" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/btnLike"> + app:layout_constraintTop_toTopOf="@id/btnLike" /> - <LinearLayout - android:id="@+id/lReactions" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:orientation="horizontal" - android:paddingVertical="2dp" /> - - </HorizontalScrollView> <com.google.android.material.button.MaterialButton android:id="@+id/btnLike" @@ -43,7 +37,7 @@ app:iconTint="@color/button_src_state_color" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/btnReply" - app:layout_constraintStart_toEndOf="@id/chipsScrollView" + app:layout_constraintStart_toEndOf="@id/rvEmojis" app:layout_constraintTop_toBottomOf="@id/divider" app:tint="@color/button_src_state_color" /> @@ -60,9 +54,9 @@ android:textAlignment="gravity" android:textColor="@color/button_src_state_color" android:textSize="14sp" - app:iconPadding="2dp" app:icon="@drawable/ic_reply" app:iconGravity="textStart" + app:iconPadding="2dp" app:iconSize="20dp" app:iconTint="@color/button_src_state_color" app:layout_constraintBottom_toBottomOf="@id/btnLike" diff --git a/core/src/main/java/org/futo/circles/core/model/ReactionsData.kt b/core/src/main/java/org/futo/circles/core/model/ReactionsData.kt index 5c8fc71f39f2abec33d37b8efbdf26bfb4fcbc3d..a10ef9a59dce6fd47b940af700598302e9577311 100644 --- a/core/src/main/java/org/futo/circles/core/model/ReactionsData.kt +++ b/core/src/main/java/org/futo/circles/core/model/ReactionsData.kt @@ -1,7 +1,11 @@ package org.futo.circles.core.model +import org.futo.circles.core.list.IdEntity + data class ReactionsData( val key: String, val count: Int, val addedByMe: Boolean -) \ No newline at end of file +) : IdEntity<String> { + override val id: String = key +} \ No newline at end of file diff --git a/core/src/main/java/org/futo/circles/core/view/LoadingRecyclerView.kt b/core/src/main/java/org/futo/circles/core/view/LoadingRecyclerView.kt new file mode 100644 index 0000000000000000000000000000000000000000..47fd9f1bb0895e395cc6734fa2db17213288ab07 --- /dev/null +++ b/core/src/main/java/org/futo/circles/core/view/LoadingRecyclerView.kt @@ -0,0 +1,60 @@ +package org.futo.circles.core.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.google.android.material.floatingactionbutton.FloatingActionButton +import org.futo.circles.core.databinding.ViewLoadingRecyclerBinding +import org.futo.circles.core.extensions.bindToFab +import org.futo.circles.core.extensions.gone +import org.futo.circles.core.extensions.visible +import org.matrix.android.sdk.api.extensions.tryOrNull + +class LoadingRecyclerView( + context: Context, + attrs: AttributeSet? = null +) : ConstraintLayout(context, attrs) { + + private val binding = ViewLoadingRecyclerBinding.inflate(LayoutInflater.from(context), this) + + private val dataObserver = object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + binding.vLoading.gone() + tryOrNull { binding.rvList.adapter?.unregisterAdapterDataObserver(this) } + } + } + + var adapter: RecyclerView.Adapter<*>? = null + get() = binding.rvList.adapter + set(value) { + binding.rvList.adapter = value + field = value + setupDataObserver() + } + + + fun addItemDecoration(decoration: ItemDecoration) { + binding.rvList.addItemDecoration(decoration) + } + + fun getRecyclerView() = binding.rvList + + fun bindToFab(fab: FloatingActionButton) = binding.rvList.bindToFab(fab) + + private fun setupDataObserver() { + with(binding) { + val initialCount = rvList.adapter?.itemCount ?: 0 + if (initialCount == 0) { + vLoading.visible() + rvList.adapter?.registerAdapterDataObserver(dataObserver) + } else { + tryOrNull { rvList.adapter?.unregisterAdapterDataObserver(dataObserver) } + vLoading.gone() + } + } + } +} \ No newline at end of file diff --git a/core/src/main/res/layout/fragment_rooms.xml b/core/src/main/res/layout/fragment_rooms.xml index 4d85a6f00db6f3457310e3c30b07f4199c907657..f1ecdf1d176c6ac299d5f3fe9181818f663ee305 100644 --- a/core/src/main/res/layout/fragment_rooms.xml +++ b/core/src/main/res/layout/fragment_rooms.xml @@ -1,11 +1,11 @@ <?xml version="1.0" encoding="utf-8"?> <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" - xmlns:tools="http://schemas.android.com/tools"> + android:layout_height="match_parent"> - <androidx.recyclerview.widget.RecyclerView + <org.futo.circles.core.view.LoadingRecyclerView android:id="@+id/rvRooms" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/core/src/main/res/layout/fragment_select_users.xml b/core/src/main/res/layout/fragment_select_users.xml index 20fa86ed9d85a05815adaf9fc3e07918e186824c..47fb23940e6dd8d148843373abbabf086572ce20 100644 --- a/core/src/main/res/layout/fragment_select_users.xml +++ b/core/src/main/res/layout/fragment_select_users.xml @@ -38,7 +38,7 @@ app:layout_constraintTop_toBottomOf="@id/rvSelectedUsers" tools:visibility="visible" /> - <androidx.recyclerview.widget.RecyclerView + <org.futo.circles.core.view.LoadingRecyclerView android:id="@+id/rvUsers" android:layout_width="0dp" android:layout_height="0dp" diff --git a/core/src/main/res/layout/view_loading_recycler.xml b/core/src/main/res/layout/view_loading_recycler.xml new file mode 100644 index 0000000000000000000000000000000000000000..85d892941ee43b9f95a2a8e81d78a2fb6844daae --- /dev/null +++ b/core/src/main/res/layout/view_loading_recycler.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge 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="wrap_content" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/rvList" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + + <ProgressBar + android:id="@+id/vLoading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/rvList" + app:layout_constraintEnd_toEndOf="@id/rvList" + app:layout_constraintStart_toStartOf="@id/rvList" + app:layout_constraintTop_toTopOf="@id/rvList" + tools:visibility="visible" /> + +</merge> \ No newline at end of file