Fixed performances issue by replacing suggestions recyclerview by linearlayout...

This commit is contained in:
Sylvain Berfini 2023-08-29 17:15:55 +02:00
parent 3c94068910
commit ab151cc409
10 changed files with 312 additions and 321 deletions

View file

@ -1,66 +0,0 @@
package org.linphone.ui.main.calls.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.ContactListCellBinding
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.Event
class SuggestionsListAdapter(
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<ContactAvatarModel, RecyclerView.ViewHolder>(SuggestionDiffCallback()) {
val contactClickedEvent: MutableLiveData<Event<ContactAvatarModel>> by lazy {
MutableLiveData<Event<ContactAvatarModel>>()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: ContactListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.contact_list_cell,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(getItem(position))
}
inner class ViewHolder(
val binding: ContactListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
@UiThread
fun bind(contactModel: ContactAvatarModel) {
with(binding) {
model = contactModel
lifecycleOwner = viewLifecycleOwner
binding.setOnClickListener {
contactClickedEvent.value = Event(contactModel)
}
executePendingBindings()
}
}
}
private class SuggestionDiffCallback : DiffUtil.ItemCallback<ContactAvatarModel>() {
override fun areItemsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean {
return true
}
}
}

View file

@ -32,9 +32,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallStartFragmentBinding
import org.linphone.ui.main.calls.adapter.SuggestionsListAdapter
import org.linphone.ui.main.calls.viewmodel.StartCallViewModel
import org.linphone.ui.main.calls.viewmodel.SuggestionsListViewModel
import org.linphone.ui.main.contacts.adapter.ContactsListAdapter
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
@ -61,15 +59,7 @@ class StartCallFragment : GenericFragment() {
R.id.startCallFragment
)
private val suggestionsListViewModel: SuggestionsListViewModel by navGraphViewModels(
R.id.startCallFragment
)
private lateinit var contactsAdapter: ContactsListAdapter
private lateinit var suggestionsAdapter: SuggestionsListAdapter
private var contactsListReady = false
private var suggestionsListReady = false
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
@ -122,59 +112,32 @@ class StartCallFragment : GenericFragment() {
}
}
binding.contactsList.layoutManager = LinearLayoutManager(requireContext())
suggestionsAdapter = SuggestionsListAdapter(viewLifecycleOwner)
binding.suggestionsList.setHasFixedSize(true)
binding.suggestionsList.adapter = suggestionsAdapter
suggestionsAdapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
viewModel.onSuggestionClickedEvent.observe(viewLifecycleOwner) {
it.consume { address ->
coreContext.postOnCoreThread {
coreContext.startCall(address)
}
}
}
binding.suggestionsList.layoutManager = LinearLayoutManager(requireContext())
binding.contactsList.layoutManager = LinearLayoutManager(requireContext())
contactsListViewModel.contactsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts list is ready with [${it.size}] items")
contactsAdapter.submitList(it) {
// Otherwise list won't show until keyboard is opened for example...
binding.contactsList.requestLayout()
}
contactsAdapter.submitList(it)
viewModel.emptyContactsList.value = it.isEmpty()
if (suggestionsListReady && !contactsListReady) {
Log.i("$TAG Suggestions list is also ready, start postponed enter transition")
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
Log.i("$TAG Suggestions list is also ready, start postponed enter transition")
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
contactsListReady = true
}
suggestionsListViewModel.suggestionsList.observe(viewLifecycleOwner) {
Log.i("$TAG Suggestions list is ready with [${it.size}] items")
suggestionsAdapter.submitList(it) {
// Otherwise list won't show until keyboard is opened for example...
binding.suggestionsList.requestLayout()
}
viewModel.emptySuggestionsList.value = it.isEmpty()
if (contactsListReady && !suggestionsListReady) {
Log.i("$TAG Contacts list is also ready, start postponed enter transition")
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
suggestionsListReady = true
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
contactsListViewModel.applyFilter(filter)
suggestionsListViewModel.applyFilter(filter)
viewModel.applyFilter(filter)
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.calls.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import org.linphone.core.Address
import org.linphone.utils.LinphoneUtils
class SuggestionModel @WorkerThread constructor(
val address: Address,
private val onClicked: ((Address) -> Unit)? = null
) {
companion object {
private const val TAG = "[Suggestion Model]"
}
val name = LinphoneUtils.getDisplayName(address)
val initials = LinphoneUtils.getInitials(name)
@UiThread
fun onClicked() {
onClicked?.invoke(address)
}
}

View file

@ -20,18 +20,183 @@
package org.linphone.ui.main.calls.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.ArrayList
import org.linphone.LinphoneApplication
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.ContactsListener
import org.linphone.core.Address
import org.linphone.core.Friend
import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.ui.main.calls.model.SuggestionModel
import org.linphone.ui.main.model.isInSecureMode
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class StartCallViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Start Call ViewModel]"
}
val searchFilter = MutableLiveData<String>()
val emptyContactsList = MutableLiveData<Boolean>()
val emptySuggestionsList = MutableLiveData<Boolean>()
val suggestionsList = MutableLiveData<ArrayList<SuggestionModel>>()
val onSuggestionClickedEvent: MutableLiveData<Event<Address>> by lazy {
MutableLiveData<Event<Address>>()
}
private var currentFilter = ""
private var previousFilter = "NotSet"
private var limitSearchToLinphoneAccounts = true
private lateinit var magicSearch: MagicSearch
private val magicSearchListener = object : MagicSearchListenerStub() {
@WorkerThread
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
Log.i("$TAG Magic search contacts available")
processMagicSearchResults(magicSearch.lastSearch)
}
}
private val contactsListener = object : ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
Log.i("$TAG Contacts have been (re)loaded, updating list")
applyFilter(
currentFilter,
if (limitSearchToLinphoneAccounts) LinphoneApplication.corePreferences.defaultDomain else "",
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(),
MagicSearch.Aggregation.Friend
)
}
}
init {
coreContext.postOnCoreThread { core ->
val defaultAccount = core.defaultAccount
limitSearchToLinphoneAccounts = defaultAccount?.isInSecureMode() ?: false
coreContext.contactsManager.addListener(contactsListener)
magicSearch = core.createMagicSearch()
magicSearch.limitedSearch = false
magicSearch.addListener(magicSearchListener)
}
applyFilter(currentFilter)
}
@UiThread
override fun onCleared() {
coreContext.postOnCoreThread {
magicSearch.removeListener(magicSearchListener)
coreContext.contactsManager.removeListener(contactsListener)
}
super.onCleared()
}
@UiThread
fun clearFilter() {
searchFilter.value = ""
}
@WorkerThread
fun processMagicSearchResults(results: Array<SearchResult>) {
Log.i("$TAG Processing [${results.size}] results")
val list = arrayListOf<SuggestionModel>()
for (result in results) {
val address = result.address
if (address != null) {
val friend = coreContext.core.findFriend(address)
// We don't want Friends here as they would also be in contacts list
if (friend == null) {
// If user-input generated result (always last) already exists, don't show it again
if (result.sourceFlags == MagicSearch.Source.Request.toInt()) {
val found = list.find {
it.address.weakEqual(address)
}
if (found != null) {
Log.i(
"$TAG Result generated from user input is a duplicate of an existing solution, preventing double"
)
continue
}
}
val model = SuggestionModel(address) {
onSuggestionClickedEvent.value = Event(it)
}
list.add(model)
}
}
}
suggestionsList.postValue(list)
Log.i("$TAG Processed [${results.size}] results, extracted [${list.size}] suggestions")
}
@UiThread
fun applyFilter(filter: String) {
coreContext.postOnCoreThread {
applyFilter(
filter,
if (limitSearchToLinphoneAccounts) LinphoneApplication.corePreferences.defaultDomain else "",
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(),
MagicSearch.Aggregation.Friend
)
}
}
@WorkerThread
private fun applyFilter(
filter: String,
domain: String,
sources: Int,
aggregation: MagicSearch.Aggregation
) {
if (previousFilter.isNotEmpty() && (
previousFilter.length > filter.length ||
(previousFilter.length == filter.length && previousFilter != filter)
)
) {
magicSearch.resetSearchCache()
}
currentFilter = filter
previousFilter = filter
Log.i(
"$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]"
)
magicSearch.getContactsListAsync(
filter,
domain,
sources,
aggregation
)
}
@WorkerThread
private fun createFriendFromSearchResult(searchResult: SearchResult): Friend {
val friend = coreContext.core.createFriend()
val address = searchResult.address
if (address != null) {
friend.address = address
friend.name = LinphoneUtils.getDisplayName(address)
friend.refKey = address.asStringUriOnly().hashCode().toString()
}
return friend
}
}

View file

@ -1,190 +0,0 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.calls.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.contacts.ContactsListener
import org.linphone.core.Friend
import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.model.isInSecureMode
import org.linphone.utils.LinphoneUtils
class SuggestionsListViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Suggestions List ViewModel]"
}
val suggestionsList = MutableLiveData<ArrayList<ContactAvatarModel>>()
private var currentFilter = ""
private var previousFilter = "NotSet"
private var limitSearchToLinphoneAccounts = true
private lateinit var magicSearch: MagicSearch
private val magicSearchListener = object : MagicSearchListenerStub() {
@WorkerThread
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
Log.i("$TAG Magic search contacts available")
processMagicSearchResults(magicSearch.lastSearch)
}
}
private val contactsListener = object : ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
Log.i("$TAG Contacts have been (re)loaded, updating list")
applyFilter(
currentFilter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(),
MagicSearch.Aggregation.Friend
)
}
}
init {
coreContext.postOnCoreThread { core ->
val defaultAccount = core.defaultAccount
limitSearchToLinphoneAccounts = defaultAccount?.isInSecureMode() ?: false
coreContext.contactsManager.addListener(contactsListener)
magicSearch = core.createMagicSearch()
magicSearch.limitedSearch = false
magicSearch.addListener(magicSearchListener)
}
applyFilter(currentFilter)
}
@UiThread
override fun onCleared() {
coreContext.postOnCoreThread {
magicSearch.removeListener(magicSearchListener)
coreContext.contactsManager.removeListener(contactsListener)
}
super.onCleared()
}
@WorkerThread
fun processMagicSearchResults(results: Array<SearchResult>) {
Log.i("$TAG Processing [${results.size}] results")
suggestionsList.value.orEmpty().forEach(ContactAvatarModel::destroy)
val list = arrayListOf<ContactAvatarModel>()
for (result in results) {
val address = result.address
if (address != null) {
val friend = coreContext.core.findFriend(address)
// We don't want Friends here as they would also be in contacts list
if (friend == null) {
// If user-input generated result (always last) already exists, don't show it again
if (result.sourceFlags == MagicSearch.Source.Request.toInt()) {
val found = list.find {
it.friend.address?.weakEqual(address) == true
}
if (found != null) {
Log.i(
"$TAG Result generated from user input is a duplicate of an existing solution, preventing double"
)
continue
}
}
val fakeFriend = createFriendFromSearchResult(result)
val model = ContactAvatarModel(fakeFriend)
model.noAlphabet.postValue(true)
list.add(model)
}
}
}
suggestionsList.postValue(list)
Log.i("$TAG Processed [${results.size}] results, extracted [${list.size}] suggestions")
}
@UiThread
fun applyFilter(filter: String) {
coreContext.postOnCoreThread {
applyFilter(
filter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(),
MagicSearch.Aggregation.Friend
)
}
}
@WorkerThread
private fun applyFilter(
filter: String,
domain: String,
sources: Int,
aggregation: MagicSearch.Aggregation
) {
if (previousFilter.isNotEmpty() && (
previousFilter.length > filter.length ||
(previousFilter.length == filter.length && previousFilter != filter)
)
) {
magicSearch.resetSearchCache()
}
currentFilter = filter
previousFilter = filter
Log.i(
"$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]"
)
magicSearch.getContactsListAsync(
filter,
domain,
sources,
aggregation
)
}
@WorkerThread
private fun createFriendFromSearchResult(searchResult: SearchResult): Friend {
val friend = coreContext.core.createFriend()
val address = searchResult.address
if (address != null) {
friend.address = address
friend.name = LinphoneUtils.getDisplayName(address)
friend.refKey = address.asStringUriOnly().hashCode().toString()
}
return friend
}
}

View file

@ -51,8 +51,6 @@ class ContactAvatarModel @WorkerThread constructor(val friend: Friend) {
val firstContactStartingByThatLetter = MutableLiveData<Boolean>()
val noAlphabet = MutableLiveData<Boolean>()
val showTrust = MutableLiveData<Boolean>()
private val friendListener = object : FriendListenerStub() {

View file

@ -201,6 +201,12 @@ fun ImageView.setPresenceIcon(presence: ConsolidatedPresence?) {
setImageResource(icon)
}
@UiThread
@BindingAdapter("avatarInitials")
fun AvatarView.loadInitials(initials: String?) {
avatarInitials = initials.orEmpty()
}
@UiThread
@BindingAdapter("accountAvatar")
fun AvatarView.loadAccountAvatar(account: AccountModel?) {

View file

@ -145,7 +145,7 @@
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@drawable/illu"
android:visibility="gone"
android:visibility="@{viewModel.emptyContactsList &amp;&amp; viewModel.suggestionsList.size() == 0 ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@id/no_contacts_nor_suggestion_label"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
@ -161,7 +161,7 @@
android:layout_height="wrap_content"
android:text="No suggestion and no contact for the moment..."
android:textSize="16sp"
android:visibility="gone"
android:visibility="@{viewModel.emptyContactsList &amp;&amp; viewModel.suggestionsList.size() == 0 ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_contacts_nor_suggestion_image" />
@ -169,7 +169,7 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/all_contacts_label"
style="@style/default_text_style_800"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
@ -180,23 +180,26 @@
android:visibility="@{viewModel.emptyContactsList ? View.GONE : View.VISIBLE}"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintVertical_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/group_call_icon"
app:layout_constraintBottom_toTopOf="@id/contacts_list"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:visibility="@{viewModel.emptyContactsList ? View.GONE : View.VISIBLE}"
app:layout_constrainedHeight="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/all_contacts_label"
app:layout_constraintBottom_toTopOf="@id/suggestions_label" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/suggestions_label"
style="@style/default_text_style_800"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
@ -204,17 +207,22 @@
android:padding="5dp"
android:text="Suggestions"
android:textSize="16sp"
android:visibility="@{viewModel.emptySuggestionsList ? View.GONE : View.VISIBLE}"
android:visibility="@{viewModel.suggestionsList.size() == 0 ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/contacts_list"
app:layout_constraintBottom_toTopOf="@id/suggestions_list" />
app:layout_constraintBottom_toTopOf="@id/suggestions_list"/>
<androidx.recyclerview.widget.RecyclerView
<LinearLayout
android:id="@+id/suggestions_list"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="@{viewModel.emptySuggestionsList ? View.GONE : View.VISIBLE}"
android:orientation="vertical"
entries="@{viewModel.suggestionsList}"
layout="@{@layout/call_suggestion_list_cell}"
app:layout_constrainedHeight="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/suggestions_label"
app:layout_constraintBottom_toBottomOf="parent" />

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="model"
type="org.linphone.ui.main.calls.model.SuggestionModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> model.onClicked()}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/cell_background"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp">
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_list_cell_size"
android:layout_height="@dimen/avatar_list_cell_size"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
android:background="@drawable/shape_avatar_background"
avatarInitials="@{model.initials, default=`JD`}"
app:avatarViewPlaceholder="@drawable/contact_avatar"
app:avatarViewInitialsBackgroundColor="@color/blue_outgoing_message"
app:avatarViewInitialsTextColor="@color/gray_9"
app:avatarViewInitialsTextSize="16sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.name, default=`John Doe`}"
android:textSize="14sp"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginEnd="10dp"
android:background="@color/blue_outgoing_message"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -45,7 +45,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{model.firstLetter, default=`A`}"
android:visibility="@{model.noAlphabet ? View.GONE : model.firstContactStartingByThatLetter ? View.VISIBLE : View.INVISIBLE}"
android:visibility="@{model.firstContactStartingByThatLetter ? View.VISIBLE : View.INVISIBLE}"
android:textColor="@color/gray_10"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"