room summary now has constant height (#7145)

This commit is contained in:
Nikita Fedrunov 2022-09-19 15:22:16 +02:00 committed by GitHub
parent 0fea172154
commit 830e5ffa9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 222 additions and 84 deletions

1
changelog.d/7079.bugfix Normal file
View file

@ -0,0 +1 @@
Fixed problem when room list's scroll did jump after rooms placeholders were replaced with rooms summary items

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.utils
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* This observer detects when item was added or moved to the first position of the adapter, while recyclerView is scrolled to the top. This is necessary
* to force recycler to scroll to the top to make such item visible, because by default it will keep items on screen, while adding new item to the top,
* outside of the viewport
* @param layoutManager - [LinearLayoutManager] of the recycler view, which displays items
* @property onItemUpdated - callback to be called, when observer detects event
*/
class FirstItemUpdatedObserver(
layoutManager: LinearLayoutManager,
private val onItemUpdated: () -> Unit
) : RecyclerView.AdapterDataObserver() {
val layoutManager: LinearLayoutManager? by weak(layoutManager)
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
if ((toPosition == 0 || fromPosition == 0) && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) {
onItemUpdated.invoke()
}
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0 && layoutManager?.findFirstCompletelyVisibleItemPosition() == 0) {
onItemUpdated.invoke()
}
}
}

View file

@ -103,6 +103,9 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>(R.layo
@EpoxyAttribute
var showSelected: Boolean = false
@EpoxyAttribute
var useSingleLineForLastEvent: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
@ -122,6 +125,10 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>(R.layo
holder.roomAvatarFailSendingImageView.isVisible = hasFailedSending
renderSelection(holder, showSelected)
holder.roomAvatarPresenceImageView.render(showPresence, userPresence)
if (useSingleLineForLastEvent) {
holder.subtitleView.setLines(1)
}
}
private fun renderDisplayMode(holder: Holder) = when (displayMode) {

View file

@ -51,7 +51,8 @@ class RoomSummaryItemFactory @Inject constructor(
roomChangeMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>,
displayMode: RoomListDisplayMode,
listener: RoomListListener?
listener: RoomListListener?,
singleLineLastEvent: Boolean = false
): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
Membership.INVITE -> {
@ -59,7 +60,7 @@ class RoomSummaryItemFactory @Inject constructor(
createInvitationItem(roomSummary, changeMembershipState, listener)
}
else -> createRoomItem(
roomSummary, selectedRoomIds, displayMode, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }
roomSummary, selectedRoomIds, displayMode, singleLineLastEvent, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }
)
}
}
@ -118,8 +119,9 @@ class RoomSummaryItemFactory @Inject constructor(
roomSummary: RoomSummary,
selectedRoomIds: Set<String>,
displayMode: RoomListDisplayMode,
singleLineLastEvent: Boolean,
onClick: ((RoomSummary) -> Unit)?,
onLongClick: ((RoomSummary) -> Boolean)?
onLongClick: ((RoomSummary) -> Boolean)?,
): VectorEpoxyModel<*> {
val subtitle = getSearchResultSubtitle(roomSummary)
val unreadCount = roomSummary.notificationCount
@ -140,7 +142,7 @@ class RoomSummaryItemFactory @Inject constructor(
} else {
createRoomSummaryItem(
roomSummary, displayMode, subtitle, latestEventTime, typingMessage,
latestFormattedEvent, showHighlighted, showSelected, unreadCount, onClick, onLongClick
latestFormattedEvent, showHighlighted, showSelected, unreadCount, singleLineLastEvent, onClick, onLongClick
)
}
}
@ -155,6 +157,7 @@ class RoomSummaryItemFactory @Inject constructor(
showHighlighted: Boolean,
showSelected: Boolean,
unreadCount: Int,
singleLineLastEvent: Boolean,
onClick: ((RoomSummary) -> Unit)?,
onLongClick: ((RoomSummary) -> Boolean)?
) = RoomSummaryItem_()
@ -177,6 +180,7 @@ class RoomSummaryItemFactory @Inject constructor(
.unreadNotificationCount(unreadCount)
.hasUnreadMessage(roomSummary.hasUnreadMessages)
.hasDraft(roomSummary.userDrafts.isNotEmpty())
.useSingleLineForLastEvent(singleLineLastEvent)
.itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
.itemClickListener { onClick?.invoke(roomSummary) }

View file

@ -16,6 +16,8 @@
package im.vector.app.features.home.room.list
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
@ -23,5 +25,18 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
@EpoxyModelClass
abstract class RoomSummaryItemPlaceHolder : VectorEpoxyModel<RoomSummaryItemPlaceHolder.Holder>(R.layout.item_room_placeholder) {
class Holder : VectorEpoxyHolder()
@EpoxyAttribute
var useSingleLineForLastEvent: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
if (useSingleLineForLastEvent) {
holder.subtitleView.setLines(1)
}
}
class Holder : VectorEpoxyHolder() {
val subtitleView by bind<TextView>(R.id.subtitleView)
}
}

View file

@ -17,18 +17,26 @@
package im.vector.app.features.home.room.list
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.settings.FontScalePreferences
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class RoomSummaryListController(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val displayMode: RoomListDisplayMode
private val displayMode: RoomListDisplayMode,
fontScalePreferences: FontScalePreferences
) : CollapsableTypedEpoxyController<List<RoomSummary>>() {
var listener: RoomListListener? = null
private val shouldUseSingleLine: Boolean
init {
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE
}
override fun buildModels(data: List<RoomSummary>?) {
data?.forEach {
add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener))
add(roomSummaryItemFactory.create(it, emptyMap(), emptySet(), displayMode, listener, shouldUseSingleLine))
}
}
}

View file

@ -20,18 +20,26 @@ import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.settings.FontScalePreferences
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class RoomSummaryPagedController(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val displayMode: RoomListDisplayMode
private val displayMode: RoomListDisplayMode,
fontScalePreferences: FontScalePreferences
) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
), CollapsableControllerExtension {
var listener: RoomListListener? = null
private val shouldUseSingleLine: Boolean
init {
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE
}
var roomChangeMembershipStates: Map<String, ChangeMembershipState>? = null
set(value) {
@ -57,8 +65,14 @@ class RoomSummaryPagedController(
}
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
// for place holder if enabled
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener)
return if (item == null) {
val host = this
RoomSummaryItemPlaceHolder_().apply {
id(currentPosition)
useSingleLineForLastEvent(host.shouldUseSingleLine)
}
} else {
roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), displayMode, listener, shouldUseSingleLine)
}
}
}

View file

@ -17,18 +17,20 @@
package im.vector.app.features.home.room.list
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.settings.FontScalePreferences
import javax.inject.Inject
class RoomSummaryPagedControllerFactory @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val fontScalePreferences: FontScalePreferences
) {
fun createRoomSummaryPagedController(displayMode: RoomListDisplayMode): RoomSummaryPagedController {
return RoomSummaryPagedController(roomSummaryItemFactory, displayMode)
return RoomSummaryPagedController(roomSummaryItemFactory, displayMode, fontScalePreferences)
}
fun createRoomSummaryListController(displayMode: RoomListDisplayMode): RoomSummaryListController {
return RoomSummaryListController(roomSummaryItemFactory, displayMode)
return RoomSummaryListController(roomSummaryItemFactory, displayMode, fontScalePreferences)
}
fun createSuggestedRoomListController(): SuggestedRoomListController {

View file

@ -24,12 +24,14 @@ import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.home.room.list.RoomSummaryItemFactory
import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_
import im.vector.app.features.settings.FontScalePreferences
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class HomeFilteredRoomsController @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
fontScalePreferences: FontScalePreferences
) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
@ -47,6 +49,13 @@ class HomeFilteredRoomsController @Inject constructor(
private var emptyStateData: StateView.State.Empty? = null
private var currentState: StateView.State = StateView.State.Content
private val shouldUseSingleLine: Boolean
init {
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
shouldUseSingleLine = fontScale.scale > FontScalePreferences.SCALE_LARGE
}
override fun addModels(models: List<EpoxyModel<*>>) {
if (models.isEmpty() && emptyStateData != null) {
emptyStateData?.let { emptyState ->
@ -67,7 +76,14 @@ class HomeFilteredRoomsController @Inject constructor(
}
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener)
return if (item == null) {
val host = this
RoomSummaryItemPlaceHolder_().apply {
id(currentPosition)
useSingleLineForLastEvent(host.shouldUseSingleLine)
}
} else {
roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener, shouldUseSingleLine)
}
}
}

View file

@ -24,7 +24,6 @@ import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -36,6 +35,7 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.core.utils.FirstItemUpdatedObserver
import im.vector.app.databinding.FragmentRoomListBinding
import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.list.RoomListAnimator
@ -66,6 +66,7 @@ class HomeRoomListFragment :
private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel()
private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel
private var concatAdapter = ConcatAdapter()
private lateinit var firstItemObserver: FirstItemUpdatedObserver
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var stateRestorer: LayoutManagerStateRestorer
@ -130,6 +131,9 @@ class HomeRoomListFragment :
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
firstItemObserver = FirstItemUpdatedObserver(layoutManager) {
layoutManager.scrollToPosition(0)
}
stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
views.roomListView.layoutManager = layoutManager
views.roomListView.itemAnimator = RoomListAnimator()
@ -158,14 +162,7 @@ class HomeRoomListFragment :
views.roomListView.adapter = concatAdapter
// we need to force scroll when recents/filter tabs are added to make them visible
concatAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) {
layoutManager.scrollToPosition(0)
}
}
})
concatAdapter.registerAdapterDataObserver(firstItemObserver)
}
override fun invalidate() = withState(roomListViewModel) { state ->
@ -233,6 +230,8 @@ class HomeRoomListFragment :
roomsController.listener = null
concatAdapter.unregisterAdapterDataObserver(firstItemObserver)
super.onDestroyView()
}

View file

@ -18,7 +18,7 @@ package im.vector.app.features.home.room.list.home.header
import android.content.res.Resources
import android.util.TypedValue
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.CarouselModelBuilder
import com.airbnb.epoxy.EpoxyController
@ -27,6 +27,7 @@ import com.airbnb.epoxy.carousel
import com.google.android.material.color.MaterialColors
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.FirstItemUpdatedObserver
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.RoomListListener
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -47,22 +48,7 @@ class HomeRoomsHeadersController @Inject constructor(
private var carousel: Carousel? = null
private val carouselAdapterObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
if (toPosition == 0 || fromPosition == 0) {
carousel?.post {
carousel?.layoutManager?.scrollToPosition(0)
}
}
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) {
carousel?.layoutManager?.scrollToPosition(0)
}
}
}
private var carouselAdapterObserver: FirstItemUpdatedObserver? = null
private val recentsHPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
@ -113,25 +99,16 @@ class HomeRoomsHeadersController @Inject constructor(
)
onBind { _, view, _ ->
host.carousel = view
host.unsubscribeAdapterObserver()
host.subscribeAdapterObserver()
val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background)
view.setBackgroundColor(colorSurface)
try {
view.adapter?.registerAdapterDataObserver(host.carouselAdapterObserver)
} catch (e: IllegalStateException) {
// do nothing
}
}
onUnbind { _, view ->
onUnbind { _, _ ->
host.carousel = null
try {
view.adapter?.unregisterAdapterDataObserver(host.carouselAdapterObserver)
} catch (e: IllegalStateException) {
// do nothing
}
host.unsubscribeAdapterObserver()
}
withModelsFrom(recents) { roomSummary ->
@ -150,6 +127,33 @@ class HomeRoomsHeadersController @Inject constructor(
}
}
private fun unsubscribeAdapterObserver() {
carouselAdapterObserver?.let { observer ->
try {
carousel?.adapter?.unregisterAdapterDataObserver(observer)
carouselAdapterObserver = null
} catch (e: IllegalStateException) {
// do nothing
}
}
}
private fun subscribeAdapterObserver() {
(carousel?.layoutManager as? LinearLayoutManager)?.let { layoutManager ->
carouselAdapterObserver = FirstItemUpdatedObserver(layoutManager) {
carousel?.post {
layoutManager.scrollToPosition(0)
}
}.also { observer ->
try {
carousel?.adapter?.registerAdapterDataObserver(observer)
} catch (e: IllegalStateException) {
// do nothing
}
}
}
}
private fun addRoomFilterHeaderItem(
filterChangedListener: ((HomeRoomFilter) -> Unit)?,
filtersList: List<HomeRoomFilter>,

View file

@ -57,6 +57,16 @@ interface FontScalePreferences {
* @return list of values
*/
fun getAvailableScales(): List<FontScaleValue>
companion object {
const val SCALE_TINY = 0.70f
const val SCALE_SMALL = 0.85f
const val SCALE_NORMAL = 1.00f
const val SCALE_LARGE = 1.15f
const val SCALE_LARGER = 1.30f
const val SCALE_LARGEST = 1.45f
const val SCALE_HUGE = 1.60f
}
}
/**
@ -73,13 +83,13 @@ class FontScalePreferencesImpl @Inject constructor(
}
private val fontScaleValues = listOf(
FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny),
FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small),
FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal),
FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large),
FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger),
FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest),
FontScaleValue(6, "FONT_SCALE_HUGE", 1.60f, R.string.huge)
FontScaleValue(0, "FONT_SCALE_TINY", FontScalePreferences.SCALE_TINY, R.string.tiny),
FontScaleValue(1, "FONT_SCALE_SMALL", FontScalePreferences.SCALE_SMALL, R.string.small),
FontScaleValue(2, "FONT_SCALE_NORMAL", FontScalePreferences.SCALE_NORMAL, R.string.normal),
FontScaleValue(3, "FONT_SCALE_LARGE", FontScalePreferences.SCALE_LARGE, R.string.large),
FontScaleValue(4, "FONT_SCALE_LARGER", FontScalePreferences.SCALE_LARGER, R.string.larger),
FontScaleValue(5, "FONT_SCALE_LARGEST", FontScalePreferences.SCALE_LARGEST, R.string.largest),
FontScaleValue(6, "FONT_SCALE_HUGE", FontScalePreferences.SCALE_HUGE, R.string.huge)
)
private val normalFontScaleValue = fontScaleValues[2]

View file

@ -60,6 +60,7 @@ class IncomingShareController @Inject constructor(
roomSummary,
data.selectedRoomIds,
RoomListDisplayMode.FILTERED,
singleLineLastEvent = false,
callback?.let { it::onRoomClicked },
callback?.let { it::onRoomLongClicked }
)

View file

@ -2,7 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="40dp" android:height="40dp"/>
<solid android:color="?vctr_reaction_background_off" />

View file

@ -5,7 +5,6 @@
android:id="@+id/recentRoot"
android:layout_width="84dp"
android:layout_height="wrap_content"
android:background="?vctr_toolbar_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"

View file

@ -190,7 +190,7 @@
android:layout_marginTop="3dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="2"
android:lines="2"
android:textAlignment="viewStart"
android:textColor="?vctr_content_secondary"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -16,7 +16,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@ -29,23 +29,20 @@
</FrameLayout>
<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomAvatarBottomSpace"
android:layout_width="0dp"
android:layout_height="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomAvatarContainer"
tools:layout_marginStart="20dp" />
<View
<TextView
android:id="@+id/roomNameView"
android:layout_width="wrap_content"
android:layout_height="15dp"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="12dp"
android:layout_marginEnd="70dp"
android:background="@drawable/placeholder_shape_8"
android:duplicateParentState="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
@ -53,23 +50,38 @@
app:layout_constraintStart_toEndOf="@id/roomAvatarContainer"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/roomTypingView"
<TextView
android:id="@+id/subtitleView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="20dp"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:layout_marginEnd="8dp"
android:background="@drawable/placeholder_shape_8"
android:ellipsize="end"
android:lines="2"
android:textAlignment="viewStart"
android:textColor="?vctr_content_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/roomNameView"
app:layout_constraintTop_toBottomOf="@id/roomNameView" />
<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomAvatarBottomSpace"
android:layout_width="0dp"
android:layout_height="7dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitleView"
tools:layout_marginStart="120dp" />
<!-- We use vctr_list_separator_system here for a better rendering -->
<View
android:id="@+id/roomDividerView"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?vctr_list_separator_system"
app:layout_constraintTop_toBottomOf="@id/roomAvatarBottomSpace"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />