mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-21 13:48:11 +00:00
Improved app fluidity
This commit is contained in:
parent
41de644945
commit
00b92a61b4
13 changed files with 218 additions and 72 deletions
|
|
@ -34,7 +34,7 @@ class ZrtpSasConfirmationDialogModel @UiThread constructor(
|
|||
) : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "[ZRTP SAS Confirmation Dialog]"
|
||||
private const val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
}
|
||||
|
||||
val message = MutableLiveData<String>()
|
||||
|
|
@ -55,24 +55,24 @@ class ZrtpSasConfirmationDialogModel @UiThread constructor(
|
|||
|
||||
// TODO: improve algo?
|
||||
val rnd = Random()
|
||||
val randomLetters1 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
|
||||
val randomLetters1 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[
|
||||
rnd.nextInt(
|
||||
alphabet.length
|
||||
ALPHABET.length
|
||||
)
|
||||
]}"
|
||||
val randomLetters2 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
|
||||
val randomLetters2 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[
|
||||
rnd.nextInt(
|
||||
alphabet.length
|
||||
ALPHABET.length
|
||||
)
|
||||
]}"
|
||||
val randomLetters3 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
|
||||
val randomLetters3 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[
|
||||
rnd.nextInt(
|
||||
alphabet.length
|
||||
ALPHABET.length
|
||||
)
|
||||
]}"
|
||||
val randomLetters4 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
|
||||
val randomLetters4 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[
|
||||
rnd.nextInt(
|
||||
alphabet.length
|
||||
ALPHABET.length
|
||||
)
|
||||
]}"
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.participants.observe(viewLifecycleOwner) { items ->
|
||||
adapter.submitList(items)
|
||||
Log.i("$TAG Participants list updated with [${items.size}] items")
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@
|
|||
package org.linphone.ui.main.fragment
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.view.doOnPreDraw
|
||||
|
|
@ -53,7 +55,16 @@ abstract class AbstractMainFragment : GenericFragment() {
|
|||
|
||||
abstract fun onDefaultAccountChanged()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
postponeEnterTransition()
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
fun setViewModel(abstractMainViewModel: AbstractMainViewModel) {
|
||||
(view?.parent as? ViewGroup)?.doOnPreDraw {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
|
||||
viewModel = abstractMainViewModel
|
||||
|
||||
viewModel.openDrawerMenuEvent.observe(viewLifecycleOwner) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.meetings.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.databinding.MeetingParticipantListCellBinding
|
||||
import org.linphone.ui.main.meetings.model.ParticipantModel
|
||||
|
||||
class MeetingParticipantsAdapter : ListAdapter<ParticipantModel, RecyclerView.ViewHolder>(
|
||||
MeetingParticipantDiffCallback()
|
||||
) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val binding: MeetingParticipantListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.meeting_participant_list_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
binding.lifecycleOwner = parent.findViewTreeLifecycleOwner()
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: MeetingParticipantListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@UiThread
|
||||
fun bind(participantModel: ParticipantModel) {
|
||||
with(binding) {
|
||||
model = participantModel
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MeetingParticipantDiffCallback : DiffUtil.ItemCallback<ParticipantModel>() {
|
||||
override fun areItemsTheSame(oldItem: ParticipantModel, newItem: ParticipantModel): Boolean {
|
||||
return oldItem.sipUri == newItem.sipUri
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ParticipantModel, newItem: ParticipantModel): Boolean {
|
||||
return oldItem.avatarModel.id == newItem.avatarModel.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +76,8 @@ class EditMeetingFragment : SlidingPaneChildFragment() {
|
|||
|
||||
val conferenceUri = args.conferenceUri
|
||||
Log.i("$TAG Found conference URI [$conferenceUri] in arguments")
|
||||
viewModel.loadExistingConferenceInfoFromUri(conferenceUri)
|
||||
val conferenceInfo = sharedViewModel.displayedMeeting
|
||||
viewModel.findConferenceInfo(conferenceInfo, conferenceUri)
|
||||
|
||||
binding.setBackClickListener {
|
||||
goBack()
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.databinding.DataBindingUtil
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.MeetingFragmentBinding
|
||||
|
|
@ -44,6 +45,7 @@ import org.linphone.databinding.MeetingPopupMenuBinding
|
|||
import org.linphone.ui.GenericActivity
|
||||
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
|
||||
import org.linphone.ui.main.history.model.ConfirmationDialogModel
|
||||
import org.linphone.ui.main.meetings.adapter.MeetingParticipantsAdapter
|
||||
import org.linphone.ui.main.meetings.viewmodel.MeetingViewModel
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.Event
|
||||
|
|
@ -56,10 +58,18 @@ class MeetingFragment : SlidingPaneChildFragment() {
|
|||
|
||||
private lateinit var binding: MeetingFragmentBinding
|
||||
|
||||
private lateinit var adapter: MeetingParticipantsAdapter
|
||||
|
||||
private lateinit var viewModel: MeetingViewModel
|
||||
|
||||
private val args: MeetingFragmentArgs by navArgs()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = MeetingParticipantsAdapter()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
|
@ -91,7 +101,16 @@ class MeetingFragment : SlidingPaneChildFragment() {
|
|||
Log.i(
|
||||
"$TAG Looking up for conference with SIP URI [$uri]"
|
||||
)
|
||||
viewModel.findConferenceInfo(uri)
|
||||
val conferenceInfo = sharedViewModel.displayedMeeting
|
||||
viewModel.findConferenceInfo(conferenceInfo, uri)
|
||||
|
||||
binding.participants.isNestedScrollingEnabled = false
|
||||
binding.participants.setHasFixedSize(false)
|
||||
binding.participants.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
if (binding.participants.adapter != adapter) {
|
||||
binding.participants.adapter = adapter
|
||||
}
|
||||
|
||||
binding.setBackClickListener {
|
||||
goBack()
|
||||
|
|
@ -144,6 +163,11 @@ class MeetingFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.participants.observe(viewLifecycleOwner) { items ->
|
||||
adapter.submitList(items)
|
||||
Log.i("$TAG Participants list updated with [${items.size}] items")
|
||||
}
|
||||
|
||||
viewModel.conferenceInfoDeletedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
Log.i("$TAG Meeting info has been deleted successfully")
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ class MeetingsListFragment : AbstractMainFragment() {
|
|||
adapter.meetingClickedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { model ->
|
||||
Log.i("$TAG Show conversation with ID [${model.id}]")
|
||||
sharedViewModel.displayedMeeting = model.conferenceInfo
|
||||
val action = MeetingFragmentDirections.actionGlobalMeetingFragment(model.id)
|
||||
binding.meetingsNavContainer.findNavController().navigate(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import org.linphone.core.Participant
|
|||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class MeetingModel @WorkerThread constructor(private val conferenceInfo: ConferenceInfo) {
|
||||
class MeetingModel @WorkerThread constructor(val conferenceInfo: ConferenceInfo) {
|
||||
companion object {
|
||||
private const val TAG = "[Meeting Model]"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,16 +20,11 @@
|
|||
package org.linphone.ui.main.meetings.model
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.ui.main.contacts.model.ContactAvatarModel
|
||||
|
||||
class ParticipantModel @WorkerThread constructor(address: Address, val isOrganizer: Boolean) {
|
||||
val avatarModel = MutableLiveData<ContactAvatarModel>()
|
||||
val sipUri = address.asStringUriOnly()
|
||||
|
||||
init {
|
||||
val avatar = coreContext.contactsManager.getContactAvatarModelForAddress(address)
|
||||
avatarModel.postValue(avatar)
|
||||
}
|
||||
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,9 +130,26 @@ class MeetingViewModel @UiThread constructor() : ViewModel() {
|
|||
}
|
||||
|
||||
@UiThread
|
||||
fun findConferenceInfo(uri: String) {
|
||||
fun findConferenceInfo(meeting: ConferenceInfo?, uri: String) {
|
||||
coreContext.postOnCoreThread { core ->
|
||||
if (meeting != null && ::conferenceInfo.isInitialized && meeting == conferenceInfo) {
|
||||
Log.i("$TAG ConferenceInfo object already in memory, skipping")
|
||||
conferenceInfoFoundEvent.postValue(Event(true))
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
val address = Factory.instance().createAddress(uri)
|
||||
|
||||
if (meeting != null && (!::conferenceInfo.isInitialized || conferenceInfo != meeting)) {
|
||||
if (address != null && meeting.uri?.equal(address) == true) {
|
||||
Log.i("$TAG ConferenceInfo object available in sharedViewModel, using it")
|
||||
conferenceInfo = meeting
|
||||
configureConferenceInfo()
|
||||
conferenceInfoFoundEvent.postValue(Event(true))
|
||||
return@postOnCoreThread
|
||||
}
|
||||
}
|
||||
|
||||
if (address != null) {
|
||||
val found = core.findConferenceInformationFromUri(address)
|
||||
if (found != null) {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
private lateinit var conferenceScheduler: ConferenceScheduler
|
||||
|
||||
private lateinit var conferenceInfoToEdit: ConferenceInfo
|
||||
private lateinit var conferenceInfo: ConferenceInfo
|
||||
|
||||
private val conferenceSchedulerListener = object : ConferenceSchedulerListenerStub() {
|
||||
@WorkerThread
|
||||
|
|
@ -106,9 +106,9 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() {
|
|||
}
|
||||
ConferenceScheduler.State.Ready -> {
|
||||
val conferenceAddress = conferenceScheduler.info?.uri
|
||||
if (::conferenceInfoToEdit.isInitialized) {
|
||||
if (::conferenceInfo.isInitialized) {
|
||||
Log.i(
|
||||
"$TAG Conference info [${conferenceInfoToEdit.uri?.asStringUriOnly()}] has been updated"
|
||||
"$TAG Conference info [${conferenceInfo.uri?.asStringUriOnly()}] has been updated"
|
||||
)
|
||||
} else {
|
||||
Log.i(
|
||||
|
|
@ -210,61 +210,42 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() {
|
|||
}
|
||||
|
||||
@UiThread
|
||||
fun loadExistingConferenceInfoFromUri(conferenceUri: String) {
|
||||
fun findConferenceInfo(meeting: ConferenceInfo?, uri: String) {
|
||||
coreContext.postOnCoreThread { core ->
|
||||
val conferenceAddress = core.interpretUrl(conferenceUri, false)
|
||||
if (conferenceAddress == null) {
|
||||
Log.e("$TAG Failed to parse conference URI [$conferenceUri], abort")
|
||||
if (meeting != null && ::conferenceInfo.isInitialized && meeting == conferenceInfo) {
|
||||
Log.i("$TAG ConferenceInfo object already in memory, skipping")
|
||||
configureConferenceInfo()
|
||||
}
|
||||
|
||||
val address = Factory.instance().createAddress(uri)
|
||||
|
||||
if (meeting != null && (!::conferenceInfo.isInitialized || conferenceInfo != meeting)) {
|
||||
if (address != null && meeting.uri?.equal(address) == true) {
|
||||
Log.i("$TAG ConferenceInfo object available in sharedViewModel, using it")
|
||||
conferenceInfo = meeting
|
||||
configureConferenceInfo()
|
||||
return@postOnCoreThread
|
||||
}
|
||||
}
|
||||
|
||||
if (address == null) {
|
||||
Log.e("$TAG Failed to parse conference URI [$address], abort")
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
val conferenceInfo = core.findConferenceInformationFromUri(conferenceAddress)
|
||||
val conferenceInfo = core.findConferenceInformationFromUri(address)
|
||||
if (conferenceInfo == null) {
|
||||
Log.e(
|
||||
"$TAG Failed to find a conference info matching URI [${conferenceAddress.asString()}], abort"
|
||||
"$TAG Failed to find a conference info matching URI [${address.asString()}], abort"
|
||||
)
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
conferenceInfoToEdit = conferenceInfo
|
||||
this.conferenceInfo = conferenceInfo
|
||||
Log.i(
|
||||
"$TAG Found conference info matching URI [${conferenceInfo.uri?.asString()}] with subject [${conferenceInfo.subject}]"
|
||||
)
|
||||
subject.postValue(conferenceInfo.subject)
|
||||
description.postValue(conferenceInfo.description)
|
||||
|
||||
isBroadcastSelected.postValue(false) // TODO FIXME: not implemented yet
|
||||
|
||||
startHour = 0
|
||||
startMinutes = 0
|
||||
endHour = 0
|
||||
endMinutes = 0
|
||||
startTimestamp = conferenceInfo.dateTime * 1000 /* Linphone timestamps are in seconds */
|
||||
endTimestamp = (conferenceInfo.dateTime + conferenceInfo.duration) * 1000 /* Linphone timestamps are in seconds */
|
||||
Log.i(
|
||||
"$TAG Loaded start date is [$startTimestamp], loaded end date is [$endTimestamp]"
|
||||
)
|
||||
computeDateLabels()
|
||||
computeTimeLabels()
|
||||
updateTimezone()
|
||||
|
||||
val list = arrayListOf<SelectedAddressModel>()
|
||||
for (participant in conferenceInfo.participantInfos) {
|
||||
val address = participant.address
|
||||
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
|
||||
address
|
||||
)
|
||||
val model = SelectedAddressModel(address, avatarModel) { model ->
|
||||
// onRemoveFromSelection
|
||||
removeModelFromSelection(model)
|
||||
}
|
||||
list.add(model)
|
||||
Log.i("$TAG Loaded participant [${address.asStringUriOnly()}]")
|
||||
}
|
||||
Log.i(
|
||||
"$TAG [${list.size}] participants loaded from found conference info"
|
||||
)
|
||||
participants.postValue(list)
|
||||
configureConferenceInfo()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -433,14 +414,13 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() {
|
|||
Log.i(
|
||||
"$TAG Updating ${if (isBroadcastSelected.value == true) "broadcast" else "meeting"}"
|
||||
)
|
||||
if (!::conferenceInfoToEdit.isInitialized) {
|
||||
if (!::conferenceInfo.isInitialized) {
|
||||
Log.e("No conference info to edit found!")
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
operationInProgress.postValue(true)
|
||||
|
||||
val conferenceInfo = conferenceInfoToEdit
|
||||
conferenceInfo.subject = subject.value
|
||||
conferenceInfo.description = description.value
|
||||
|
||||
|
|
@ -479,6 +459,48 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun configureConferenceInfo() {
|
||||
if (::conferenceInfo.isInitialized) {
|
||||
subject.postValue(conferenceInfo.subject)
|
||||
description.postValue(conferenceInfo.description)
|
||||
|
||||
isBroadcastSelected.postValue(false) // TODO FIXME: not implemented yet
|
||||
|
||||
startHour = 0
|
||||
startMinutes = 0
|
||||
endHour = 0
|
||||
endMinutes = 0
|
||||
startTimestamp = conferenceInfo.dateTime * 1000 /* Linphone timestamps are in seconds */
|
||||
endTimestamp =
|
||||
(conferenceInfo.dateTime + conferenceInfo.duration) * 1000 /* Linphone timestamps are in seconds */
|
||||
Log.i(
|
||||
"$TAG Loaded start date is [$startTimestamp], loaded end date is [$endTimestamp]"
|
||||
)
|
||||
computeDateLabels()
|
||||
computeTimeLabels()
|
||||
updateTimezone()
|
||||
|
||||
val list = arrayListOf<SelectedAddressModel>()
|
||||
for (participant in conferenceInfo.participantInfos) {
|
||||
val address = participant.address
|
||||
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
|
||||
address
|
||||
)
|
||||
val model = SelectedAddressModel(address, avatarModel) { model ->
|
||||
// onRemoveFromSelection
|
||||
removeModelFromSelection(model)
|
||||
}
|
||||
list.add(model)
|
||||
Log.i("$TAG Loaded participant [${address.asStringUriOnly()}]")
|
||||
}
|
||||
Log.i(
|
||||
"$TAG [${list.size}] participants loaded from found conference info"
|
||||
)
|
||||
participants.postValue(list)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun removeModelFromSelection(model: SelectedAddressModel) {
|
||||
val newList = arrayListOf<SelectedAddressModel>()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.annotation.UiThread
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ConferenceInfo
|
||||
import org.linphone.core.Friend
|
||||
import org.linphone.ui.main.chat.model.MessageModel
|
||||
import org.linphone.utils.Event
|
||||
|
|
@ -137,6 +138,8 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
/* Meetings related */
|
||||
|
||||
var displayedMeeting: ConferenceInfo? = null // Prevents the need to go look for the conference info
|
||||
|
||||
val forceRefreshMeetingsListEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,19 +305,17 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:tint="?attr/color_main2_600"/>
|
||||
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/participants"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical"
|
||||
entries="@{viewModel.participants}"
|
||||
layout="@{@layout/meeting_participant_list_cell}"
|
||||
android:nestedScrollingEnabled="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/separator_4"
|
||||
app:layout_constraintStart_toEndOf="@id/participants_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue